UPDATE: Below is an original post regarding Ember Data Relationships that I believe are more related to a problem with the EmberFire adapter and how hasMany array records are handled. It seems the local store in Ember Data is updating appropriately, however these records are not being pushed into the appropriate Firebase database after the store is updated.
I've added the emberfire and firebase tags to hopefully find an answer in that community. Any help is appreciated and I'm happy to provide additional information as needed.
I have two models that are defined with hasMany and belongsTo relationships. Here is the first "frameworks" model:
// -- Frameworks Model
import DS from 'ember-data';
const { attr, hasMany } = DS;
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
questions: { serialize: 'ids' }
}
});
export default DS.Model.extend({
name: attr('string'),
reports: hasMany('report'),
questions: hasMany('question', {async: true}),
includedFramework: attr('string'),
reportOnlyOnce: attr('string', { defaultValue: false }),
frameBuilder: attr('string', {defaultValue: "none"}),
createdAt: attr('string'),
createdBy: attr('string'),
updatedAt: attr('string'),
updatedBy: attr('string')
});
And the second "question" model:
// -- Question Model
import DS from 'ember-data';
const { attr, hasMany, belongsTo } = DS;
export default DS.Model.extend({
framework: belongsTo('frameworks', {async: true}),
detail: attr('string'),
type: attr('string'),
createdAt: attr('string'),
createdBy: attr('string'),
position: attr('number'),
options: hasMany('option')
});
On my route, I pull in the model for the framework (in this case defining the model ID for testing):
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
let id = params.report_id;
var self = this;
return Ember.RSVP.hash({
report: this.store.find('report', id),
general: this.store.find('frameworks', '-KITSAxjdE5TiVdk-aPC')
})
}
});
For some reason, this framework has an array stored in questions that has 4 of the 6 different "questions" that should belong to it. I don't know why 4 of the stored in the framework model as well and 2 of them did not. In my template for the page:
{{#each model.general.questions as |question|}}
{{#paper-item class="md-1-line"}}
<p class="question">
{{question.detail}}
</p>
<p class="option">
{{#if (eq question.type "text")}}
{{paper-input value=question.id onChange=(action (mut question.id))}}
{{/if}}
</p>
{{/paper-item}}
{{#each model.general.questions as |question|}} works great for the 4 questions that are actually stored in the framework model for this ID, but not the other 2. The other 2 questions do have the belongsTo relationship referencing the correct framework.
I have tried adding a serializer in the framework model (as you can see above) but there was no change.
I have also tried manually adding the question to the framework store when a new question is created with:
framework.get('questions').then(function(question) {
return question.pushObject(newQuestion);
})
but no change with that either.
Any thoughts are welcome, I'm happy to provide any other relevant code snippets as well.
This issue seems to be related to the way EmberFire responds to models with a belongsTo and hasMany relationships where the "parent" model with a hasMany reference to the child models does not update automatically when a child model is created.
An issue has been submitted on GitHub to continue investigating how to resolve this issue: https://github.com/firebase/emberfire/issues/412
Related
I've updated ember-data from 1.0.-beta.19.2 to 1.13.16 and my model "sheet" has a hasMany relationship ({ async: true, embedded: 'always' }) to another model "elements", which itself has a hasMany relationship to another model "steps" ({ async: true}). If I want to access steps in an observer in the elements model, it has no attributes or values. I've tried to make steps non async, because I don't know if that makes sense in an embedded record, but that didn't help.
My serializers for these models look like this
sheet:
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
isNewSerializerAPI: true,
attrs: {
elements: { embedded: 'always' },
parent: { serialize: 'ids', deserialize: 'records' }
}
});
element:
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
isNewSerializerAPI: true,
attrs: {
steps: { embedded: 'always' }
}
});
The reason why it didn't work after the update (without changing anything in the code) was a bit tricky to find. The steps model had no serializer defined, and automatically generated serializers default to isNewSerializerAPI: true. So the solution was just creating the file serializers/steps.js:
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
});
I have a nested model as such:
var School = DS.Model.extend({
classrooms: DS.hasMany('classroom', {async: true})
});
var Classroom = DS.Model.extend({
students: DS.hasMany('student', {async: true}),
school: DS.belongsTo('school', {async: true})
});
var Student = DS.Model.extend({
name: DS.attr('string'),
classroom: DS.belongsTo('classroom', {async: true})
});
I am using firebase as a backend, and I understand it is advisable to denormalize the schema for efficiency sake. Is there any utility in explicitly specifying the relationship
var Student = DS.Model.extend({
school: DS.belongsTo('school', {async: true});
});
for the Student model, even though this is implied by each Student belonging to a Classroom and each Classroom belonging to a School?
No. You shouldn't need to provide nested relationship information unless model in question is directly accessible from the root object (hope that makes sense).
The only "benefit" of doing so is that in your case the student model will be loaded when the school data is being loaded rather than waiting until the classroom data is loaded. However, this will provide a data structure that is not reflective of your intentions and so I wouldn't advocate doing this.
I would use a computed property or a binding to get what you're after - something like
school: Ember.computed.alias('classroom.school')
schoolBinding: 'classroom.school'
It wouldn't be a DS.belongsTo because the data wouldn't actually contain a school property
In the app user has two associations - billing address and company address which are identical
//address
export default DS.Model.extend({
email: DS.attr('string'),
country: DS.attr('string'),
zip: DS.attr('string'),
user: DS.belongsTo('user')
//...
});
//user
export default DS.Model.extend({
name: DS.attr('string'),
company: DS.belongsTo('address'),
billing: DS.belongsTo('address')
//...
});
How to pull this kind of trick?
What I would do is use a Polymorphic relationship. The steps would be (I'm assuming ember-cli, if not the gist is the same):
ember generate model address
ember generate model company-address
ember generate model billing-address
In models/address.js you define the actual address model:
export default DS.Model.extend({
email: DS.attr('string'),
country: DS.attr('string'),
zip: DS.attr('string'),
user: DS.belongsTo('user')
//...
});
Then in the other models you extend from the parent model (polymorphism saves the day!).
company-address
import Address from './address';
export default Address.extend({
// Yeah, its empty.
});
billing-address
import Address from './address';
export default Address.extend({
// But, you could add custom fields here and
// still share the ones with the parent
// Polymorphism.
});
Now in your user model you do this:
export default DS.Model.extend({
name: DS.attr('string'),
company: DS.belongsTo('address', {polymorphic: true}),
billing: DS.belongsTo('address', {polymorphic: true})
});
There is no free lunch though, your JSON has to change its format now:
{ name: 'foobar',
company: {
// company fields or id if async
type: 'company-address'
},
billing: {
// billing fields or id if async
type: 'billing-address'
}
}
You are looking for the inverse options.
You need two fields on the Address model, one referring to the company with this address, one referring to the user with this address. Then, specify which is which with the inverse option.
//address
export default DS.Model.extend({
...
billing: DS.belongsTo('user', { inverse: 'billing' })
company: DS.belongsTo('user', { inverse: 'company' })
});
//user
export default DS.Model.extend({
...
company: DS.belongsTo('address'),
billing: DS.belongsTo('address')
});
See http://emberjs.com/guides/models/defining-models#toc_explicit-inverses.
For what you are trying to do, there is no reason whatsoever to define two separate derived models simply in order to get the associations set up properly. inverse does exactly what you need.
How do I define multiple relationships between two objects in Ember? For example A user can be either a staff or a customer the differentiator is the userType property. If a customer buys a product, the product object need to have link to the customer that bought it and the staff that facilitated the sale.
Here is a basic version of a data model that would suffice for what you have described. You can adapt it for your needs. I have used the app global haven't specified whether you are using ember-cli.
App.User = DS.Model.extend({
name: DS.attr('string'),
userType: DS.attr('string')
});
App.Order = DS.Model.extend({
orderedBy: DS.belongsTo('user'),
facilitatedBy: DS.belongsTo('user')
});
App.Product = DS.Model.extend({
name: DS.attr('string')
});
A useful tool for creating a first pass of a data model when you are unsure of the ember syntax is Ember Data Model Maker. You can use it to see how you should set up your model definitions and then modify them later.
I've got Post and Comment models:
App.Post = DS.Model.extend({
body: DS.attr('string'),
comments: DS.hasMany('comment')
});
App.Comment = DS.Model.extend({
body: DS.attr('string'),
post: DS.belongsTo('post')
})
When I create a new comment and save it:
var comment = this.store.createRecord('comment');
comment.set('post', post);
comment.save();
it makes request with post: 1 data inside the request. How to change it to make request with post_id: 1 instead?
I think this is likely because you are using the default Ember data adapter which is the RESTAdapter.
The RESTAdapter has different conventions than the standard ActiveModel::Serializer conventions which is why it would be sending post: 1 instead of post_id: 1
Try switching your default adapter to the ActiveModelAdapter
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
namespace: 'api'
})
Doc can be found here http://emberjs.com/api/data/classes/DS.ActiveModelAdapter.html