Hybrid View

  1. #1
    Ext JS Premium Member
    Join Date
    Jun 2011
    Posts
    14
    Vote Rating
    4
    emilianm is on a distinguished road

      2  

    Question Unanswered: Saving objects that are linked hasMany relation with a single Store

    Unanswered: 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?

    Thank you very much for any idea regarding this.

  2. #2
    Ext JS Premium Member
    Join Date
    Jun 2011
    Posts
    14
    Vote Rating
    4
    emilianm is on a distinguished road

      0  

    Default


    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 associations
    getRecordData: function(record)

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

  3. #3
    Sencha User
    Join Date
    Dec 2010
    Location
    Aberdeen, Scotland
    Posts
    18
    Vote Rating
    1
    chrisface is on a distinguished road

      1  

    Default


    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.

  4. #4
    Ext JS Premium Member
    Join Date
    Jun 2011
    Posts
    14
    Vote Rating
    4
    emilianm is on a distinguished road

      0  

    Default


    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.

    I think this should be integrated in the next ExtJs release.

  5. #5
    Ext JS Premium Member
    Join Date
    Feb 2009
    Location
    New York, NY
    Posts
    5
    Vote Rating
    0
    cstrong@arielpartners.com is on a distinguished road

      0  

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

    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?

  6. #6
    Sencha User
    Join Date
    Dec 2010
    Location
    Aberdeen, Scotland
    Posts
    18
    Vote Rating
    1
    chrisface is on a distinguished road

      0  

    Default


    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

    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 = {};
            if(record.proxy.writer.writeAllFields){
                data = record.data;
            }
            else {
                var changes, name,  field, fields = record.fields, nameProperty = this.nameProperty, key;
                changes = record.getChanges();
                for (key in changes) {
                    if (changes.hasOwnProperty(key)) {
                        field = fields.get(key);
                        name = field[nameProperty] || field.name;
                        data[name] = changes[key];
                    }
                }
                if (!record.phantom) {
                    // always include the id for non phantoms
                    data[record.idProperty] = record.getId();
                }
            }
    
    
            //Iterate over all the hasMany associations
            for (i = 0; i < record.associations.length; i++) {
                association = record.associations.get(i);
                data[association.name] = [];
                childStore = record[association.storeName];
    
    
                //Iterate over all the children in the current association
                childStore.each(function(childRecord) {
    
    
                    //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;
            }
        }
    });
    
    
    Ext.data.Store.override({
        remove: function(records, /* private */ isMove) {
            if (!Ext.isArray(records)) {
                records = [records];
            }
    
    
            /*
             * Pass the isMove parameter if we know we're going to be re-inserting this record
             */
            isMove = isMove === true;
            var me = this,
                    sync = false,
                    i = 0,
                    length = records.length,
                    isPhantom,
                    index,
                    record;
    
    
            for (; i < length; i++) {
                record = records[i];
                index = me.data.indexOf(record);
    
    
                if (me.snapshot) {
                    me.snapshot.remove(record);
                }
    
    
                if (index > -1) {
                    isPhantom = record.phantom === true;
                    if (!isMove && !isPhantom) {
                        // don't push phantom records onto removed
                        record.set('forDeletion', true);
                        me.removed.push(record);
                    }
    
    
                    record.unjoin(me);
                    me.data.remove(record);
                    sync = sync || !isPhantom;
    
    
                    me.fireEvent('remove', me, record, index);
                }
            }
    
    
            me.fireEvent('datachanged', me);
            if (!isMove && me.autoSync && sync) {
                me.sync();
            }
        }
    });
    Ext.data.TreeStore.override({
        onNodeRemove: function(parent, node) {
            var removed = this.removed;
    
    
            if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) {
                node.set('forDeletion', true);
                removed.push(node);
            }
        }
    });

  7. #7
    Sencha Premium Member
    Join Date
    Nov 2012
    Location
    Bangalore
    Posts
    79
    Vote Rating
    1
    rupamkhaitan is on a distinguished road

      0  

    Default


    hi chrisface,

    Thanks for sharing your code. I have a doubt

    Will it take care of multiple relationship?
    Combination of hasOne & hasMany?

  8. #8
    Sencha Premium Member
    Join Date
    Nov 2012
    Location
    Bangalore
    Posts
    79
    Vote Rating
    1
    rupamkhaitan is on a distinguished road

      0  

    Default


    my last post was for chrisface who explained the below approach

    Quote Originally Posted by chrisface View Post
    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.

  9. #9
    Sencha User
    Join Date
    Apr 2010
    Posts
    75
    Answers
    3
    Vote Rating
    5
    HriBB is on a distinguished road

      0  

    Default


    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?

    I guess I'll have to go deep into source code ... :|

  10. #10
    Sencha User
    Join Date
    Jul 2012
    Posts
    25
    Answers
    1
    Vote Rating
    1
    delebash is on a distinguished road

      0  

    Default


    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?