Saturday, June 30, 2012

Sencha Touch 2: Models and proxies: associations - traversing and saving

It's difficult to locate reasonable information on using a proxy with a model that has associations - it's even more difficult to actually have it work, as (in a JSON context), the standard JSON writer of Sencha Touch 2 does not traverse associations. The extended JSON writer in this post will traverse model associations and also allows one to inject handlers for any required custom formatting - else isomorphism is assumed.

Semantically, I don't personally like the relationship between a model and a proxy, it is unnatural to my mindset, but I do admit it useful on occasion. So here I present a modified example, including an extension of the standard Ext.data.writer.Json. The code presented includes hard coded url's, names and so on, which the real code base does not - as I said, and example.

First, the model, including a proxy section, with the extended writer. The User model 'hasMany' membership details - yes, not a compelling example, but it serves for illustrative purposes:

1:  Ext.define('app.model.User', {  
2:    extend: 'Ext.data.Model',  
3:    config: {  
4:        fields: [  
5:           {name: 'id',   type: 'int'},  
6:           {name: 'firstName', type: 'string'},  
7:           {name: 'surname', type: 'string'},  
8:           {name: 'age', type: 'int'}  
9:        ],  
10:        hasMany: {  
11:           model: 'app.model.MembershipDetails',  
12:           name: 'details'  
13:        },  
14:        proxy: {  
15:           type: 'ajax',  
16:           url: '/createUser',  
17:           method: 'POST',  
18:           writer: {  
19:                 type: 'associationsWriter',  
20:                 root: 'UserCreationRequest',  
21:                 encodeRequest: true  
22:           }  
23:        }  
24:     }  
25:  });  

Line 19 defines the type of the writer, which is shown below:


1:  Ext.define('app.lib.data.AssociationsWriter', {  
2:     extend: 'Ext.data.writer.Json',  
3:     alias: 'writer.associationsWriter',  
4:     config: {  
5:        // Array of objects of type:   
6:       // { name: xxx processor: function(name, data, record) { xxx },   
7:        // retain: true/false/undef}  
8:        customFieldHandlers: [],  
9:        customAssociationHandlers: []  
10:     },  
11:     constructor: function() {  
12:        this.callParent(arguments);  
13:        this.depth = -1;  
14:     },  
15:     getRecordData: function(record) {  
16:        this.depth++;  
17:        var data = this.callParent(arguments);  
18:        (this.depth == 0 ?   
19:          this.processCustomFields(data, record) : this).  
20:            processAssociations(data, record);  
21:        this.depth--;    
22:        return data;  
23:     },  
24:     processCustomFields: function(data, record) {  
25:        this.getCustomFieldHandlers().forEach(function(e) {  
26:           e.processor(e.name, data, record);  
27:           if (!e.retain) delete data[name];  
28:        }, this);  
29:        return this;  
30:     },  
31:     processAssociations: function(data, record) {  
32:        record.getAssociations().each(function(ass) {  
33:           if (ass.getType() !== 'belongsto') {  
34:              var customHandler =   
35:                this.getCustomAssociationHandlers().first(  
36:                 function(e) { return e.name === ass.getName(); }  
37:                );  
38:              var handler = customHandler ?   
39:                customHandler.processor : this.standardAssociation;  
40:              var store = ass.getStore().apply(record, null);  
41:              store.each(function (rec) {  
42:                         handler.apply(this, [ ass.getName(), data, rec ] );  
43:           }, this);  
44:           }  
45:      }, this);  
46:     },  
47:     standardAssociation: function(name, data, rec) {  
48:        if (!data[name]) data[name] = [];  
49:      data[name].push(this.getRecordData.call(this, rec));  
50:     }  
51:  });  

The key override from the base type is getRecordData(Object).The code is fairly simple, the association handling function being processAssociations. As can be seen, a standard ST2 config section in this type allows you to associate custom field and association handlers if the normal behaviour does not suit requirements.


A little clumsily, line 33 handles the case where an association is encountered that should not be traversed. An associated object (membership details) includes a 'back association' with containing object, in this instance a model type of user. 


Also noteworthy is line 40 which gets the dynamically generated store that will hold the associations objects. As can be inferred, association.getStore() returns a function, which we call using the apply function.


MembershipDetails is included below - note the FK field reference to user, 'user_id' - the default name expected by Sencha if not explicitly specified:




1:  Ext.define('app.model.MembershipDetails', {  
2:    extend: 'Ext.data.Model',  
3:    config: {  
4:        fields: [  
5:           {name: 'id',   type: 'int'},  
6:           {name: 'user_id', type: 'int'},  
7:           {name: 'joinDate', type: 'date'},  
8:           {name: 'promoCode', type: 'string'}  
9:        ],  
10:        associations: { type: 'belongsTo',   
11:                        model: 'app.model.User' }  
12:     }  
13:  });  



Finally, a different version of the model with a custom field handler and custom association handler:


1:  Ext.define('app.model.User', {  
2:    extend: 'Ext.data.Model',  
3:    config: {  
4:        fields: [  
5:           {name: 'id',   type: 'int'},  
6:           {name: 'firstName', type: 'string'},  
7:           {name: 'surname', type: 'string'},  
8:           {name: 'age', type: 'int'}  
9:        ],  
10:        hasMany: {  
11:           model: 'app.model.MembershipDetails',  
12:           name: 'details'  
13:        },  
14:        proxy: {  
15:           type: 'ajax',  
16:           url: '/createUser',  
17:           method: 'POST',  
18:           writer: {  
19:                 type: 'associationsWriter',  
20:                 root: 'UserCreationRequest',  
21:                 encodeRequest: true,  
22:                 // Custom handler only required when there is not an   
23:                 // isomorphic relationship between the model and target  
24:                 customFieldHandlers: [  
25:                    { name: 'surname', processor: function(name, data, record) {  
26:                                      data['lastName'] = record.get(name);  
27:                                    }  
28:                    }  
29:                 ],  
30:                 customAssociationHandlers: [  
31:                    // Name here matches the name of the association defined above  
32:                    { name: 'details', processor: function(name, data, record) {  
33:                          data[key] = {  
34:                                'when' : record.get('joinDate'),  
35:                                'voucher' : record.get('promoCode')     
36:                             };  
37:                          }  
38:                    }  
39:                 ],  
40:              }  
41:      }  
42:     }  
43:  });  

Line 25 has a field handler that morphs the surname property, making it instead lastName in the generated JSON request. Line 32, takes the details association and changes the names of the joinDate and promoCode - of course, more sophisticated actions are possible. The default behaviour in the extended writer is to not further process or include in the generated request any fields or associations that are handled by custom handlers - but this can be overridden by including a 'retain: true' association  in the custom handler definition.

No comments: