ExtJS 4.1 - 在Model.Save()响应中返回关联数据

12

在执行Model.save()后,结果集合中记录所包含的关联数据未能正确返回更新后的值,尽管这些更新的数据在服务器响应中已经存在,我对此很感兴趣...

示例模型和存储定义:

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
});

应用服务器以单个模型及其关联数据响应代理的READ请求,一切都正常运行!

服务器响应READ请求

{
"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
}

到目前为止,一切都按计划进行...

store = Ext.create("App.store.test.Simpson");
homer = store.getById(1);
kids  = homer.getKids().getRange();
console.log("The Simpson Kids", kids);  // [>constructor, >constructor, >constructor]

不期望的行为始于保存和更新请求

这是我针对更新请求的测试响应...

/** 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!!
    }
})
我注意到,如果我检查服务器返回的记录,生成的关联存储(即getKidsStore)中包含的记录是原始记录,即它们的名称中没有“SAVED”。但是,返回记录的kids属性确实包含正确的数据。
如果我正确理解了问题,那么问题在于Ext.data.reader.Reader没有正确地使用.save()响应中包含的关联数据更新相关的存储。如果是这样的话,在我的看法里,这非常不直观,因为我希望与处理store.load()请求并填充生成的关联存储开始的阅读器相同的行为。
有人能指点我实现我想要的行为的方向吗?
声明:同样的问题在这里提出:ExtJs 4 - Load nested data on record save,但没有回复。我感觉我的问题更加详尽。
编辑:我在Sencha论坛上发布了这个问题:http://www.sencha.com/forum/showthread.php?270336-Associated-Data-in-Model.save()-Response 编辑(8/23/13):我用一个完整的例子重写了这篇文章,并做了额外的发现...
4个回答

6
我找到了问题所在,或者说混淆出现在 Ext.data.OperationgetRecords() 方法中。根据文档,此方法返回“初始配置的记录将被返回,尽管代理可能会在操作初始化后的某个时刻修改这些记录的数据。”
在我看来,这相当令人困惑,因为返回的记录确实已经更新,但生成的关联存储和因此关联的数据并没有更新!这就导致了我的困惑,看起来记录包含了应用服务器上更新的数据,但事实并非如此。
为了帮助我这个简单的头脑从响应中获取完全更新的数据,我向 Ext.data.Operation 类添加了一个方法......我刚刚编写了这个方法,只测试了我需要的功能,所以使用时需自行承担风险!
请记住,我不调用 store.sync(),而是实例化一个模型并调用模型的 save() 方法,因此我的 resultSet 通常只包含单个记录...
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!";
        }

    }
});

现在我能够实现我想要的功能了...

// 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
          */
    }
});

编辑12/10/13 我还添加了一个名为updateTo的方法到Ext.data.Model中,它可以处理将记录更新到提供的记录,并且也可以处理关联。我与上面的getSavedRecord方法一起使用它。请注意,这不会处理任何belongsTo关联,因为我在我的应用程序中没有使用它们,但是这个功能很容易添加。

/**
 * 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;
}

所以基本上我会像这样做(理论上应该在订单控制器中执行):
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;
            }
        }
    });
}

我希望这篇文章能够帮助到一些像我一样感到困惑的人!

4

我完全同意你的观点。这是一个非常奇怪的行为。应该在记录中更新关联存储。以下是我解决此问题的方法(基本上只需要通过读取器运行响应!):

 success: function(record, operation) {
     var newRecord= me.getMyModel().getProxy().reader.read(operation.response).records[0];
 }

好的解决方案!出于某种原因,我发现当手动调用代理的读操作时,存储器并不总是更新...因此,我在模型类上添加了一个名为“updateTo”的方法,它处理将实例更新到提供的记录(从服务器响应中检索),包括关联存储。同样的想法,只是不同的方法 :) - John Hall
我在我的答案中添加了updateTo方法,以使其更加清晰/有帮助。 - John Hall

1
在ExtJS 6.2中,问题仍然存在(或再次出现)。 我的解决方案:
/**
 * 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]);
    }
});

很高兴知道在后续版本中它仍然能够正常工作,但我很困惑为什么Sencha团队不认为这是奇怪的行为... - John Hall

-1
如果您的ID字段有值,那么ExtJS将始终调用update。如果您没有向ID字段写入任何值或将其设置为null,则应该调用create。我猜您正在尝试使用现有记录调用save,因此它将始终调用update。这是期望的行为。

1
这个问题与创建/更新请求无关...仅与返回相关数据和ExtJS相应地更新存储记录有关,但这根本不起作用...正如所示。记录必须从服务器完全重新加载(强制执行无用的往返)或手动使用服务器响应中提供的数据进行更新,这个过程在我的答案中已经详细描述了。 - John Hall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接