I am curious as to why the record contained in the result set of a Model.save()
response does not properly return updated associated data, despite the updated data being contained in the server response...
Example Model & Store Definition:
Ext.define("App.model.test.Parent",{
extend: 'Ext.data.Model',
requires: ['App.model.test.Child'],
fields: [
{name: 'id', type: 'int' },
{name: 'name', type: 'string'},
{name: 'kids', type: 'auto', defaultValue: []}
],
idProperty: 'id',
hasMany: [{
foreignKey: 'parent_id',
model: 'App.model.test.Child',
associationKey: 'kids',
name: 'getKids'
}],
proxy: {
type: 'ajax',
api : {
create: '/service/test/create/format/json',
read : '/service/test/read/format/json',
update : '/service/test/update/format/json'
},
reader: {
idProperty : 'id',
type : 'json',
root : 'data',
successProperty : 'success',
messageProperty : 'message'
},
writer: {
type : 'json',
writeAllFields : true
}
}
});
Ext.define("App.model.test.Child",{
extend: 'Ext.data.Model',
fields: [
{name: 'id', type: 'int' },
{name: 'name', type: 'string'},
{name: 'parent_id', type: 'int'}
]
});
Ext.define("App.store.test.Simpson",{
storeId: 'TheSimpsons',
extend: 'Ext.data.Store',
model : 'App.model.test.Parent',
autoLoad: true,
autoSync: false
});
The application server response to the proxy's READ
request with a single model, and its associated data. This is all working hunky dory!
Server Response to READ Request
{
"data":{
"id":1,
"name":"Homer Simpson",
"children":{
"1":{
"id":1,
"name":"Bart Simpson"
},
"2":{
"id":2,
"name":"Lisa Simpson"
},
"3":{
"id":3,
"name":"Maggie Simpson"
}
}
},
"success":true,
"message":null
}
Thus far, everything is working according to plan...
store = Ext.create("App.store.test.Simpson");
homer = store.getById(1);
kids = homer.getKids().getRange();
console.log("The Simpson Kids", kids); // [>constructor, >constructor, >constructor]
THE UNDESIRED BEHAVIOR BEGINS WITH SAVE AND UPDATE REQUESTS
Here is my test response for the UPDATE Request...
/** Server UPDATE Response */
{
"data":{
"id":1,
"name":"SAVED Homer Simpson",
"kids":[{
"id":1,
"name":"SAVED Bart Simpson",
"parent_id":1
},{
"id":2,
"name":"SAVED Lisa Simpson",
"parent_id":1
},{
"id":3,
"name":"SAVED Maggie Simpson",
"parent_id":1
}]
},
"success":true,
"message":null
}
/** Will call proxy UPDATE, response is above */
homer.save({
success: function(rec, op){
var savedRec = op.getRecords().pop(),
kidNames = '';
console.log(savedRec.get('name')); // SAVED Homer Simpson = CORRECT!
Ext.each(savedRec.getKids().getRange(), function(kid){
kidNames += kid.get('name') + ", ";
});
console.log(kids);
//Outputs: Bart Simpson, Lisa Simpson, Maggie Simpson = WRONG!!
}
})
I notice that if I inspect the record returned by the server, the generated Association Store (i.e., getKidsStore
) the contained records are the original records, i.e., they do not have "SAVED" in their name. The kids
property of the returned record, however, DOES indeed contain the correct data.
If I understand the issue correctly, it is that the Ext.data.reader.Reader
does not correctly update the associated store with the associated data contained in the .save()
response. If so, in my opinion, this is very unintuitive as I would expect the same behavior as the reader that handles the store.load()
request and populates the generated association stores to begin with.
Can anyone point me in the right direction in achieving the behavior I'm after?
Disclaimer: Same question was asked here: ExtJs 4 - Load nested data on record save but with no response. I feel my question to be a bit more thorough..
EDIT: I have posted this question over on the Sencha forums: http://www.sencha.com/forum/showthread.php?270336-Associated-Data-in-Model.save()-Response
EDIT (8/23/13): I re-wrote this post with a COMPLETE example, as well as additional findings...
In ExtJS 6.2 the problem still (or again exists). My solution:
/**
* In Ext.data.reader.Reader::extractRecord the call readAssociated reads out the hasMany associations and processes them.
* This works perfectly for Model.load() since internally a Model is used as record variable in extractRecord.
* For Model.save() record extractRecord contains just the Object with the received data from the PUT request,
* therefore readAssociated is never called and no associations are initialized or updated.
* The following override calls readAssociated if necessary in the save callback.
*/
Ext.override(Ext.data.Model, {
save: function(options) {
options = Ext.apply({}, options);
var me = this,
includes = me.schema.hasAssociations(me),
scope = options.scope || me,
callback,
readAssoc = function(record) {
//basicly this is the same code as in readAssociated to loop through the associations
var roles = record.associations,
key, role;
for (key in roles) {
if (roles.hasOwnProperty(key)) {
role = roles[key];
// The class for the other role may not have loaded yet
if (role.cls) {
//update the assoc store too
record[role.getterName]().loadRawData(role.reader.getRoot(record.data));
delete record.data[role.role];
}
}
}
};
//if we have includes, then we can read the associations
if(includes) {
//if there is already an success handler, we have to call both
if(options.success) {
callback = options.success;
options.success = function(rec, operation) {
readAssoc(rec);
Ext.callback(callback, scope, [rec, operation]);
};
}
else {
options.success = readAssoc;
}
}
this.callParent([options]);
}
});
Completely agree with you. Really odd behavior. It should update the association store on the record. This is how i got around this issue (basically just run the response through the reader!):
success: function(record, operation) {
var newRecord= me.getMyModel().getProxy().reader.read(operation.response).records[0];
}
I've found the issue, or rather, confusion lies in the getRecords()
method of the Ext.data.Operation
. This method returns "the operation's initially configured records will be returned, although the proxy may modify these records' data at some point after the operation is initialized." as per the documentation.
This is rather confusing IMO, as the returned record is indeed updated, however the generated association store, and therefore associated data, is not! This is what lead to my confusion, it appeared as though the record contained the updated data from the application server, but this was not the case.
In order to aid my simple mind in obtaining the FULLY updated data from the response, I have added a method to the Ext.data.Operation
class... I just wrote this method and haven't tested it more than ensuring the functionality I was looking for, so use at your own risk!
Please keep in mind that I do not call store.sync(), rather I instantiate a model and call the model.save() method, so my resultSet typically only ever contains a single record...
Ext.override(Ext.data.Operation,{
getSavedRecord: function(){
var me = this, // operation
resultSet = me.getResultSet();
if(resultSet.records){
return resultSet.records[0];
}else{
throw "[Ext.data.Operation] EXCEPTION: resultSet contains no records!";
}
}
});
Now I am able to achieve the functionality I was after...
// Get the unsaved data
store = Ext.create('App.store.test.Simpson');
homer = store.getById(1);
unsavedChildren = '';
Ext.each(homer.getKids().getRange(), function(kid){
unsavedChildren += kid.get('name') + ",";
});
console.log(unsavedChildren); // Bart Simpson, Lisa Simpson, Maggie Simpson
// Invokes the UPDATE Method on the proxy
// See original post for server response
home.save({
success: function(rec, op){
var savedRecord = op.getSavedRecord(), // the magic! /sarcasm
savedKids = '';
Ext.each(savedRecord.getKids().getRange(), function(kid){
savedKids += kid.get('name') + ',';
});
console.log("Saved Children", savedKids);
/** Output is now Correct!!
SAVED Bart Simpson, SAVED Lisa Simpson, SAVED Maggie Simpson
*/
}
});
Edit 12/10/13
I also added a method to Ext.data.Model
which I called updateTo
which handles updating a record to the provided record, which also handles associations. I use this in conjunction with the above getSavedRecord
method. Please note this does not handle any belongsTo
associations as I don't use them in my application, but that functionality would be easy to add.
/**
* Provides a means to update to the provided model, including any associated data
* @param {Ext.data.Model} model The model instance to update to. Must have the same modelName as the current model
* @return {Ext.data.Model} The updated model
*/
updateTo: function(model){
var me = this,
that = model,
associations = me.associations.getRange();
if(me.modelName !== that.modelName)
throw TypeError("updateTo requires a model of the same type as the current instance ("+ me.modelName +"). " + that.modelName + " provided.");
// First just update the model fields and values
me.set(that.getData());
// Now update associations
Ext.each(associations, function(assoc){
switch(assoc.type){
/**
* hasOne associations exist on the current model (me) as an instance of the associated model.
* This instance, and therefore the association, can be updated by retrieving the instance and
* invoking the "set" method, feeding it the updated data from the provided model.
*/
case "hasOne":
var instanceName = assoc.instanceName,
currentInstance = me[instanceName],
updatedInstance = that[instanceName];
// Update the current model's hasOne instance with data from the provided model
currentInstance.set(updatedInstance.getData());
break;
/**
* hasMany associations operate from a store, so we need to retrieve the updated association
* data from the provided model (that) and feed it into the current model's (me) assocStore
*/
case "hasMany":
var assocStore = me[assoc.storeName],
getter = assoc.name,
newData = that[getter]().getRange();
// Update the current model's hasMany association store with data from the provided model's hasMany store
assocStore.loadData(newData);
break;
// If for some reason a bogus association type comes through, throw a type error
// At this time I have no belongsTo associations in my application, so this TypeError
// may one day appear if I decide to implement them.
default:
throw TypeError("updateTo does not know how to handle association type: " + assoc.type);
break;
}
});
// Commit these changes
me.commit();
return me;
}
So basically I do something like this (this would theoretically be in the Order controller)
doSaveOrder: function(order){
var me = this, // order controller
orderStore = me.getOrderStore(); // magic method
// Save request
order.save({
scope: me,
success: function(responseRecord, operation){
// note: responseRecord does not have updated associations, as per post
var serverRecord = operation.getSavedRecord(),
storeRecord = orderStore.getById(order.getId());
switch(operation.action){
case 'create':
// Add the new record to the client store
orderStore.add(serverRecord);
break;
case 'update':
// Update existing record, AND associations, included in server response
storeRecord.updateTo(serverRecord);
break;
}
}
});
}
I hope this helps someone who was confused as I was!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With