Hybrid View

Saving objects that are linked hasMany relation with a single Store

We are using a main object (A) that has a hasMany relation with a list of B objects.
For the A object we have a store with specific url api for reading and for updating data.

I have to mention that we have this URL configured only in the Store object A.
When we use the .load() method on the A store we receive a json stream that will contain also a list of B objects and this objects are very well fetched in B list form A object.

After we modify A object and some of the related B objects, and after we call .sync() method on AStore object we discover that the related B objects are not present in the generated json stream, so are not sent to the server together with A object.

Do you have any idea how can I synchronize with a single call the main A objects(along with the list of B objects) with the server?

We verified in the documentation and in the source code and it seems that the reader is able to pre-load with data the objects linked with hasMany form the parent store, but the writer is not taking into account the hasMany relations, is putting in the final json stream only the fields directly attached to the model.

In conclusion, to achieve the synchronization of the main object and also of the associated objects we need to provide a custom implementation of the default Ext.data.writer.Writer

It seems that this method needs to take into account the has Many associationsgetRecordData: function(record)

If you already encountered this issue please do not hesitate to share your information.
Thanks.

This is something I had to do as well myself and I took the same approach of overriding the getRecordData function to take into account the associations. I also added some code to take care of deleted records as well. I'm adding a forDeletion property only for deleted records so that I can keep it out of the fields on the models.

Code:

Ext.data.writer.Json.override({
/*
* This function overrides the default implementation of json writer. Any hasMany relationships will be submitted
* as nested objects. When preparing the data, only children which have been newly created, modified or marked for
* deletion will be added. To do this, a depth first bottom -> up recursive technique was used.
*/
getRecordData: function(record) {
//Setup variables
var me = this, i, association, childStore, data = record.data;
//Iterate over all the hasMany associations
for (i = 0; i < record.associations.length; i++) {
association = record.associations.get(i);
data[association.name] = null;
childStore = record[association.storeName];
//Iterate over all the children in the current association
childStore.each(function(childRecord) {
if (!data[association.name]){
data[association.name] = [];
}
//Recursively get the record data for children (depth first)
var childData = this.getRecordData.call(this, childRecord);
/*
* If the child was marked dirty or phantom it must be added. If there was data returned that was neither
* dirty or phantom, this means that the depth first recursion has detected that it has a child which is
* either dirty or phantom. For this child to be put into the prepared data, it's parents must be in place whether
* they were modified or not.
*/
if (childRecord.dirty | childRecord.phantom | (childData != null)){
data[association.name].push(childData);
record.setDirty();
}
}, me);
/*
* Iterate over all the removed records and add them to the preparedData. Set a flag on them to show that
* they are to be deleted
*/
Ext.each(childStore.removed, function(removedChildRecord) {
//Set a flag here to identify removed records
removedChildRecord.set('forDeletion', true);
var removedChildData = this.getRecordData.call(this, removedChildRecord);
data[association.name].push(removedChildData);
record.setDirty();
}, me);
}
//Only return data if it was dirty, new or marked for deletion.
if (record.dirty | record.phantom | record.get('forDeletion')){
return data;
}
}
});

I'm not sure if this code will work under every condition but it's been fine for me so far. If you have any suggestions for improvements or a better way of doing it I'd like to hear your thoughts.

Thank you very much chrisface, we already put in place a custom Writer that is taking into accounts also the fields of hasMany relations. I will take a look at your solution and after we sill finalise ours we will publish here the code.

Missing something: how to mark parent record dirty when changes made to child store?

Many thanks to chrisface for the code posting.
As I understand it, the objective here is to read and write an object graph using only a single round trip each.
I think I am still missing one piece...

Let's pretend I am dealing with User and Post. User hasMany Post
I am showing the User information in one place, and I am displaying Post records in their own separate grid.
Now I add a new Post to the Post grid, and then call sync() on the User store.
Nothing happens. Why? Because the User record was never marked dirty even though a new Post object was added like so:
user.posts().add( {} ); // <-- this should mark the user instance dirty, but doesn't
I can fake it by adding a 'datachanged' listener or by manually:
user.setDirty();
I am thinking this linkage should be an option to set up in the hasManyAssociations createStore()
method, to enable this kind of behavior, thus reducing the number of round-trips to the server.
What am I missing?

You're right cstrong, this wont send back your Posts. I do something a little bit different with my relationships which I should have mentioned for my override to work properly.

Instead of having a hasMany of "Post" models on the "User" I have a "UserPosts" model which I use instead. This model maps to the linker table that I use for saving which Posts belong to what User.

It looks a bit like this:
Id , UserId, PostId

What I then do is that I make my UserPosts model extend my Post model and I still have all the information I need. I also override the idProperty value to use the id for the linker table.

This means that when I'm assigning Posts to a User, what I'm actually doing is creating new UserPost models. They have a Post Id inside of them but no UserPost Id so they get picked up as phantom records.

I use my UserPosts model for handling the relationships and I use the Post model when doing CRUD on Posts. It does mean that there are some cases where they can't be used interchangeably but I either do a load using the Ids I have to get the correct kind of model or do a quick transformation between the objects which I have static methods for in the models.

It's probably not how Sencha intended the persistence of models to work but it was the only way I could get nested saving to work for me.

I ended up making more changes to the code as well because it caused some unforeseen bugs but there may be more that I've not seen yet

This is something I had to do as well myself and I took the same approach of overriding the getRecordData function to take into account the associations. I also added some code to take care of deleted records as well. I'm adding a forDeletion property only for deleted records so that I can keep it out of the fields on the models.

Code:

Ext.data.writer.Json.override({
/*
* This function overrides the default implementation of json writer. Any hasMany relationships will be submitted
* as nested objects. When preparing the data, only children which have been newly created, modified or marked for
* deletion will be added. To do this, a depth first bottom -> up recursive technique was used.
*/
getRecordData: function(record) {
//Setup variables
var me = this, i, association, childStore, data = record.data;
//Iterate over all the hasMany associations
for (i = 0; i < record.associations.length; i++) {
association = record.associations.get(i);
data[association.name] = null;
childStore = record[association.storeName];
//Iterate over all the children in the current association
childStore.each(function(childRecord) {
if (!data[association.name]){
data[association.name] = [];
}
//Recursively get the record data for children (depth first)
var childData = this.getRecordData.call(this, childRecord);
/*
* If the child was marked dirty or phantom it must be added. If there was data returned that was neither
* dirty or phantom, this means that the depth first recursion has detected that it has a child which is
* either dirty or phantom. For this child to be put into the prepared data, it's parents must be in place whether
* they were modified or not.
*/
if (childRecord.dirty | childRecord.phantom | (childData != null)){
data[association.name].push(childData);
record.setDirty();
}
}, me);
/*
* Iterate over all the removed records and add them to the preparedData. Set a flag on them to show that
* they are to be deleted
*/
Ext.each(childStore.removed, function(removedChildRecord) {
//Set a flag here to identify removed records
removedChildRecord.set('forDeletion', true);
var removedChildData = this.getRecordData.call(this, removedChildRecord);
data[association.name].push(removedChildData);
record.setDirty();
}, me);
}
//Only return data if it was dirty, new or marked for deletion.
if (record.dirty | record.phantom | record.get('forDeletion')){
return data;
}
}
});

I'm not sure if this code will work under every condition but it's been fine for me so far. If you have any suggestions for improvements or a better way of doing it I'd like to hear your thoughts.

Any ideas on how to configure json reader to handle deepjson response from model.save() or store.sync()?

I have deepjson writer functional and working. Server receives all data, creates, updates, deletes as necessary ... And responds with the same data structure as it received, except that phantom records now have IDs, creation dates are there, and maybe some other parameters are present that are set on the server when record is first created.

So my question is ... how do you apply data returned from deepjson writer to the existing data and mark all those records !dirty and !phantom? Is json reader even capable of doing that? Will store.sync() take care of everything if readers are set up properly? If yes, how to configure reader?

Any comments from Sencha support on this issue. Handling change tracking using associated data, ie form has parent store and associated data store and store.save is for parent. So it only tracks parent fields dirty and not associated. Others have manually coded to handle some of this, but really I would think by now using 4.2 this would be built in?