Why is a "ManyToOne" relation treated differently when serializing in ember-data? - ember.js

My question is highly related to "Ember Data: Saving relationships" but I don't care about embedding - I just want a working OneToMany (bi-directional) relationship. Take for example the following models:
App.Child = DS.Model.extend({
name: DS.attr('string'),
toys: DS.hasMany('toy'),
});
App.Toy = DS.Model.extend({
name: DS.attr('string'),
child: DS.belongsTo('child')
});
and the following object creations/saves:
var store = this.get('store');
store.createRecord('child', {name: 'Herbert'}).save().then(child => {
return store.createRecord('toy', {name: 'Kazoo', child: child}).save())
}).then(toy => {
child.get('toys').pushObject(toy);
return child.save();
});
I would expect the child, when serialized, to reference the toy. E.g. something like
{
'name': 'Herbert',
'toys': [ 1 ]
}
But it doesn't. Because this is a "manyToOne" relation ship and ember-data won't serialize these: https://github.com/emberjs/data/blob/v1.0.0-beta.18/packages/ember-data/lib/serializers/json-serializer.js#L656
If you make it a ManyToNone relation by removing the belongsTo it will work but you will lose the back reference.
Why is this special behaviour? Why is ManyToOne that different from ManyToNne or ManyToMany that it deserves such special treatment?
Where is this behaviour documented? I totally missed it and assumed it was a bug in the Serializer / Adapter I'm using.
What is the correct way to achieve my desired serialization?

I ended up creating my own, trivially modified serializer:
import EmberPouch from 'ember-pouch';
export default EmberPouch.Serializer.extend({
serializeHasMany: function(snapshot, json, relationship) {
// make sure hasMany relationships are serialized.
// http://stackoverflow.com/questions/20714858/ember-data-saving-relationships
var key = relationship.key;
if (this._canSerialize(key)) {
var payloadKey;
// if provided, use the mapping provided by `attrs` in
// the serializer
payloadKey = this._getMappedKey(key);
if (payloadKey === key && this.keyForRelationship) {
payloadKey = this.keyForRelationship(key, "hasMany", "serialize");
}
var relationshipType = snapshot.type.determineRelationshipType(relationship);
if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany' || relationshipType === "manyToOne") {
json[payloadKey] = snapshot.hasMany(key, { ids: true });
// TODO support for polymorphic manyToNone and manyToMany relationships
}
}
},
});
the main difference being that it also accepts manyToOne relations,
and use it in application/adapter.js:
export default EmberPouch.Adapter.extend({
defaultSerializer: "pouchserial",
....
In this specific (pouch) case it would probably be better to store the reference to the parent on the child only so the parent doesn't need updates when children are added (which also avoids conflicts).

Related

Best Practice for Creating New Record with belongsTo Relationship

I am wondering about the best practice for creating a new record in Ember with createRecord() and then persisting it to the API? Specifically, should Ember's POST request generally be a single JSON that embeds all the model's relationships, or is it customary to POST each relationship individually?
In my code, I'm not able to get a single JSON, so I'm wondering if I'm missing the "Ember Way" or (more likely) I have a mistake in my code?
DETAILS:
Here are the details of my setup. I have two models:
/models/OrgUser.js:
DS.Model.extend({
...
orgPerson: DS.belongsTo('org-person', { inverse: 'org-user', async: true, embedded: 'always' }),
});
/models/OrgPerson.js:
DS.Model.extend({
...
orgUser: DS.belongsTo('org-user'),
})
I'm attempting to create a new user on the "Create New User" page. The route for that page is below. Is this the best place to call createRecord() for my new models?
/routes/org-users/add.js:
Ember.Route.extend({
model: function() {
var orgPerson = this.store.createRecord('org-person');
var orgUser = this.store.createRecord('org-user' );
orgUser.set('orgPerson', orgPerson);
return orgUser;
},
...
}
Using Chrome console to look at the orgUser object after I call set shows no evidence at all that I have added anything to orgUser. The "Ember" tab of Chrome Debug Tools does reflect the relationship, though.
On my "Create New User" page, my input fields all correspond to both OrgUser properties and OrgUser.OrgPerson properties. Here's an example:
/templates/org-users/add.hbs
...
{{input value=username}} // a property of OrgUser
{{input value=orgPerson.firstName}} // a property of OrgUser.orgPerson
...
In my route, when I go to save() Ember Data POSTs only the orgUser JSON with a null value for orgPerson. I'd like it to embed the orgPerson serialized object in the orgPerson property.
/routes/org-users/add.js:
Ember.Route.extend({
...
actions: {
submitForm: function() {
...
this.currentModel.save().then( onSuccess ).catch( onFailure );
...
}
}
});
This results in a POST request with the following body:
{
"orgUser":{
"username":"MyUsername",
"orgPerson":null,
"id":null
}
Note that orgPerson is null. Thanks in advance for any assistance!
UPDATE: Once again, I think I will need to take a fresh look at my serializer. Here's how it's currently defined.
/serializers/application.js:
DS.RESTSerializer.extend({
// Use the default approach to serializing, but add the id property
serialize: function(record, options) {
var json = this._super.apply(this, arguments);
json.id = record.id;
return json;
},
serializeBelongsTo: function(record, json, relationship) {
var key = relationship.key;
key = this.keyForRelationship ? this.keyForRelationship(key, 'belongsTo') : key;
var data = record.get('data');
if (relationship.options.embedded && relationship.options.embedded === 'always') {
json[key] = data[relationship.key] ? data[relationship.key].serialize( { includeId: true } ) : null;
}
else {
json[key] = data[relationship.key] ? data[relationship.key].get('id') : null;
}
if (relationship.options.polymorphic) {
this.serializePolymorphicType(record, json, relationship);
}
}
});
Per #Kingpin2k's comment, there appears to be some ambiguity (and bugs!) on how best to handle serialize() for a belongsTo relationship. My serializer customization above works great for records that are obtained through this.store.find(), but now I need to enable them for createRecord(). Additional suggestions, pointers are welcome!
It's a bug. https://github.com/emberjs/data/issues/1542#issuecomment-49443496
A workaround is to get the async belongsTo record before attempting to save (It tricks Ember Data into initializing it). In your case you could do it in the model hook.
model: function() {
var orgPerson = this.store.createRecord('org-person');
var orgUser = this.store.createRecord('org-user');
orgUser.set('orgPerson', orgPerson);
return orgUser.get('orgPerson').then(function(){
return orgUser;
});
},
So, I finally figured this out. With the release of Ember-Data-1.0.0-Beta.9, http://emberjs.com/blog/2014/08/18/ember-data-1-0-beta-9-released.html, the EmbeddedRecordsMixin has been introduced. This pretty much solves all my issues!
So, I wound up doing the following:
Upgraded to Ember-Data-1.0.0-Beta.9
Deleted my serializeBelongsTo customization from my serializer
I now define a custom serializer for each model using the EmbeddedRecordsMixin as documented at http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html.
This wound up working perfectly, because I get full declarative control over how and when my records are embedded.
Special thanks to #Kingpin2k for helping me realize my serializer was the problem and for the discussion to help me understand the options.

Having trouble setting Ember objects to both contain references to each other

Answered
I've solved the problem, it has to do with my adapter, and I'll post more in an answer,
but here's a link to a problem I'm having with the fix Ember get not getting certain attribute
Original Question
I have the following code:
customerSignUp: function () {
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir'
});
var model = this.get('model');
model.set('customer', customer);
customer.save().then(function() {
model.save().then(function() {
customer.set('user', model);
customer.save();
});
});
}
With the following backing models:
App.User = App.Person.extend({
name: DS.attr('string'), // Actually their primary email.
customer: DS.belongsTo('customer', {async: true })
});
App.Customer = DS.Model.extend({
user: DS.belongsTo('user', {async: true}),
description: DS.attr('string')
});
(App.Person just passes along some naming conventions)
The customerSignUp function is trying to get both objects to refer to the other, so that either one could get attributes from the other as needed (the user model is planned to have more relationships like this in the future, so that one user can have multiple "roles" on the site).
The problem is that I can't get both to stably refer to each other. With this current implementation the model (user) points to the customer just fine, but the customer for some reason simply has it's user field set to <computed> in the Ember Debugger, and the record saved in the database doesn't even have a user field. It seems to me like some of my saves are overwriting the values or changing the underlying objects so they're no longer true? Honestly I'm just confused.
I've tried all kinds of different orderings of the saves and sets, but so far only one at a time works, or if both work, it's because I haven't saved one of them to the database. Any advice? Is this even necessary? Will the customer object have access to it's user even if there isn't an id explicitly stored in the customer?
Could this have to do with the adapter I'm using?
Update
With this implementation of customerSignUp:
customerSignUp: function () {
var model = this.get('model');
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir',
user: model
});
customer.save().then(function() {
model.set('customer', customer);
model.save();
});
}
The customer relationship on the user is set for a moment, but then resets to null. The user relationship is just <computed> and I don't think was ever set.
It had to do with my adapter.
This function:
serializeBelongsTo: function(record, json, relationship) {
console.log("serializeBelongsTo");
var attribute, belongsTo, key;
attribute = relationship.options.attribute || "id";
console.log(attribute);
key = relationship.key;
console.log(key);
belongsTo = Ember.get(record, key);
console.log(belongsTo);
if (Ember.isNone(belongsTo)) {
return;
}
var content = belongsTo.content;
console.log(content);
var temp = Ember.get(belongsTo, attribute);
console.log(temp);
json[key] = temp;
console.log(json);
if (relationship.options.polymorphic) {
return json[key + "_type"] = belongsTo.constructor.typeKey;
}
else {
return json;
}
returns a value of undefined from Ember.get(belongsTo, attribute) even though belongsTo and attribute are correctly set, although belongsTo has it's data buried in a content object, which this SO post details.

How to rollback changes to HasMany in ember-data

I have following models:
App.Parent = DS.Model.extend({
foo: DS.attr('string'),
children: DS.hasMany('child', {async: true})
});
App.Child = DS.Model.extend({
bar: DS.attr('string')
});
I filled them with some fixture data:
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Parent.FIXTURES = [
{
id:0,
foo: 'parent',
children: [0,1]
}
];
App.Child.FIXTURES = [
{
id: 0,
bar: 'child 0'
},
{
id: 1,
bar: 'child 1'
},
{
id: 2,
bar: 'child 2'
}
];
After I do some changes to the children relation, how can I rollback children relation to its latest saved state?
I push new child to manyArray in the following way:
this.store.find('child', 2).then(function(child){
model.get('children').pushObject(child);
})
This does change the relation (I see new child in my view), but parent record does not become dirty. Thus, when I try model.rollback() it does nothing. I also tried solution I found here How to rollback relationship changes in EmberData which is adding model.send('becomeDirty') before rollback, but it does not help.
Maybe I am adding children to my relation in a wrong way?
Thanks!
I used this to rollback related records that are dirty.
App.ParentRoute = Ember.Route.extend
model: ->
#get('store').createRecord('parent', child: #get('store').createRecord('child'))
actions:
willTransition: (transition) ->
rollbackRecords(#)
rollbackRecords = (context) ->
if context.get("controller.content.isDirty")
relationships = Ember.get(App[context.get('controller').resourceName], "relationshipsByName")
content = context.get('controller.content')
relationships.forEach (name, relationship) ->
relatedModel = content.get(name)
relatedModel.rollback() if relatedModel? and relatedModel.get('isDirty')
content.rollback()
true
Here is some code that will rollback a model, as well as it's relationships that I use:
var model = this.get('model');
var relatedModel, relatedModels;
model.constructor.eachRelationship(function (key, relationship) {
if (relationship.kind === 'hasMany') {
relatedModels = model.get(key);
if (relatedModels) {
relatedModels.invoke('rollback'); //since this is an array, need to call the rollback function differently
}
}
else {
relatedModel = model.get(key);
if (relatedModel) {
relatedModel.rollback();
}
}
});
model.rollback();
Hope this helps
I believe the other answers listed here only partially solve this problem. If you add a new related model or remove an existing one, rollback should reverse those as well and I believe the other answers don't address this. Here is a complete solution that provides proper dirty checking and a complete rollback for hasMany and belongsTo relationships:
https://stackoverflow.com/a/27184207/188740
I like to do the good ol' model.reload()
- it'll blow away all changes you haven't synced with your server though.

Ember Data 1.0.0: what is expected format for belongsTo relationship

I have the following models:
App.Publication = DS.Model.extend({
title: DS.attr('string'),
bodytext: DS.attr('string'),
author: DS.belongsTo('author')
});
App.Author = DS.Model.extend({
name: DS.attr('string')
});
And the folowing json data:
{
"publications": [
{
id: '1',
title: 'first title',
bodytext: 'first body',
author_id: 100
},
{
id: '2',
title: 'second title',
bodytext: 'second post',
author_id: 200
}
];
}
In Ember Data RC12 this worked (you could specify author_id OR author in the json and the publication would always have the correct author linked).
In Ember Data 1.0.0 this no longer works; author is always null.
In some documents I found that - since I am using "author_id" in the json data (and not simply author) - I need to specify the key in the model; thus:
author: DS.belongsTo('author', { key: 'author_id' })
This however does not work; the author in the publication remains null.
The only solution I see for now is to implement a custom serializer and override the author_id to author (via normailzeId); I cannot change my backend data structure ... thus:
App.MySerializer = DS.RESTSerializer.extend({
//Custom serializer used for all models
normalizeId: function (hash) {
hash.author = hash.author_id;
delete hash.author_id;
return hash;
}
});
Is the above the correct way ?
Ember Data 1.0 no longer does any payload normalization by default. The key configuration for DS.belongsTo has been removed as well so you will have to implement a custom serializer.
normalizeId is an internal serializer function used for converting primary keys to always be available at id. You shouldn't override this.
Instead, you can override the keyForRelationship method which is provided for this purpose.
You could use something like the following:
App.ApplicationSerializer = DS.RESTSerializer.extend({
keyForRelationship: function(rel, kind) {
if (kind === 'belongsTo') {
var underscored = rel.underscore();
return underscored + "_id";
} else {
var singular = rel.singularize();
var underscored = singular.underscore();
return underscored + "_ids";
}
}
});
Note: I've also renamed the serializer to App.ApplicationSerializer so that it will be used as the default serializer for your application.
Finally, if you haven't already found it, please take a look at the transition notes here: https://github.com/emberjs/data/blob/master/TRANSITION.md
If you read through the transition document shortly after the initial 1.0.0.beta.1 release I would recommend taking a look again as there have been a number of additions, particularly regarding serialization.
From the Ember 1.0.0 Transition Guide:
Underscored Keys, _id and _ids
In 0.13, the REST Adapter automatically camelized incoming keys for you. It also expected belongsTo relationships to be listed under name_id and hasMany relationships to be listed under name_ids.
If your application returns json with underscored attributes and _id or _ids for relation, you can extend ActiveModelSerializer and all will work out of the box.
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({});
Note: DS.ActiveModelSerializer is not to be confused with the ActiveModelSerializer gem that is part of Rails API project. A conventional Rails API project with produce underscored output and the DS.ActiveModelSerializer will perform the expected normalization behavior such as camelizing property keys in your JSON.

EmberData: Two models related with hasMany relationships

I have an application logic that requires two models to have reciprocal hasMany relationships. As an example, imagine a set of GitHub issues that can be tagged with several labels.
I am trying to use an adapter that extends the default RESTAdapter. All the application works fine but the double hasMany relationship throws an exception. Digging into the code, a method inverseBelongsToForHasMany throws an exception.
So, I guess that Ember.Data does not support the association of two models with hasMany relationships in both sides and every hasMany requires an associated belongsTo. My questions are:
Is this supported and the issue is just I am doing something wrong?
If it is not supported, is it a feature planned to appear?
Is this a association type to be avoided in this kind of applications? If so, which is the best approach or workaround?
Thanks in advance
We use a similar method of creating the association object. However, instead of overriding the methods in store, we just added the join objects to the api.
so in the models we create:
App.Hashtag = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.User = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.HashtagUser = DS.Model.extend({
user: DS.belongsTo('App.User'),
hashtag: DS.belongsTo('App.Hashtag')
});
Then for the transactions we simply alter and commit the join object.
App.UserController = Ember.ObjectController.extend({
followHashtag: function(tag) {
var hashtagUser;
hashtagUser = this.get('hashtagUsers').createRecord({
hashtag: tag
});
tag.get('hashtagUsers').pushObject(hashtagUser);
App.store.commit();
}
unfollowHashtag: function(tag) {
var itemToRemove;
itemToRemove = this.get('hashtagUsers').find(function(hashtagUser) {
if (hashtagUser.get('hashtag') === this) {
return true;
}
}, tag);
this.get('hashtagUser').removeObject(itemToRemove);
tag.get('hashtagUser').removeObject(itemToRemove);
itemToRemove.deleteRecord();
App.store.commit();
});
The API creates a HashtagUser object and the follow method just adds that user to both the associated pieces.
For removal, it pops the associated objects and destroys the association object.
Although it's not as elegant as it could be, our big motivation was that when Ember Data gets updated then we should be able to transition it to a simple stock Ember Data supported version more easily than if we've messed with the Store itself.
Many to Many relationships are not yet supported in ember-data. For the moment, one possible workaround is to manually manage the join table.
A = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('b');
}
});
Ab = DS.Model.extend({
a: DS.belongsTo('A'),
b: DS.belongsTo('b')
});
B = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('a');
}
});
This is just the starting point. You need then to customize your models and adapter in order to send/receive/persist records in a working manner
For example, in our app, we introduce an { includedJoin: true } option inside the hasMany relations, and declare the join table as a JoinModel
A = DS.Model.extend({
abs: DS.hasMany('Ab', {includeJoin: true}),
...
});
DS.JoinModel = DS.Model.extend();
Ab = DS.JoinModel.extend({
... belongsTo relationships ...
});
Then in the Adapter, we override the create/update/delete methods in order to ignore the joins table lifecycle in the store
createRecords: function (store, type, records) {
if (!DS.JoinModel.detect(type)) {
this._super(store, type, records);
}
}
Finally, in the serializer, we override the addHasMany function in order to send the join data to the server as embedded ids in the parent models.
addHasMany: function (hash, record, key, relationship) {
var
options = relationship.options,
children = [];
//we only add join models, use of `includeJoin`
if (options.includedJoin) {
record.get(relationship.key).forEach(function (child) {
children.pushObject(child.toJSON({
includeId: true
}));
});
hash[key] = children;
}
}
Server-side we are using Rails with ActiveModelSerializer, so the only little-tricky-customization is when when we update the parent models, we manually manage the joins relation, and create/delete entries in the join table.