Associations in Ext JS 5

   Sencha
Associations in Ext JS 5

Associations have undergone quite a few changes in Ext JS 5. Sencha has really put work into making them more functional and easier to use. Like all things though, they have their ups and downs. So, let’s dig into the new associations starting with…

The Good

Music Associations? Yeah, it’s very good. It’s very good for the digestion.

The New Ext.data.Field.reference Approach
One of the nice things Sencha has done is to eliminate the need to specify both sides of the association “equation” by giving us a new way to declare them. We do this by using the reference property of the field. So, if you have a Customer model with an Address association, you can simply have the following:

Ext.define('Customer', {
    extend : 'Ext.data.Model',
    fields : [
        'name'
    ]
});

Ext.define('Address', {
    extend : 'Ext.data.Model',
    fields : [
        { name : 'address'},
        { name : 'customerId', reference : 'Customer'}
    ]
});

Ext.define('Customers', {
    extend   : 'Ext.data.Store',
    model    : 'Customer',
    autoLoad : true,
    proxy : {
        type : 'ajax',
        url  : 'customers.json'
    }
});

// customers.json
[{
    id   : 1,
    name : "Modus Create",
    addresses : [{
           id      : 1,
           address : 'Reston, VA'
    },{
        id      : 2,
        address : 'Providence, RI'
    }]
}]

Fiddle: https://fiddle.sencha.com/#fiddle/914

The Extras
This will create an association and inverse association for both Customer and Address and should get our data tied together easily. If you inspect the Customers store, you’ll see that each record now has an addresses() getter that will return a store of this customer’s addresses. Also, each address record in that store will have a reference to the customer record and a “customerId” on its data property.

The Bad

How’s your digestion now?

Also the New Ext.data.Field.reference Approach

As I mentioned, the new method for defining these relationships is all driven from the reference property of a field in the association model. As of today, you can’t use the Ext.data.Field.reference form of the relationship from the parent (customer) model. This is unfortunate as most people will naturally approach these definitions from the right/parent/customer side. I know what you’re thinking. Tim is great. If you have 10 different models that all need address associations, then yes, you are going to need 10 address models. The number of model classes you have is going to increase… possibly by quite a bit. You could, of course, create a base address model (minus the reference field) and then extend all of the others from this base class.

Legacy HasMany
What if you don’t want forty new models or what if you already have a code base that is using hasMany associations? The answer is that you cannot use the new reference association feature but you can still create association relationships from the parent by using the hasMany config property on your parent model, just like you have been. Building on our example above, we can add a Vendors model with an Address hasMany.

Ext.define('Vendor', {
    extend: 'Ext.data.Model',
    requires: ['Address'],
    fields: [
        'name'
    ],    
    hasMany: [{
        name: 'addresses',
        // associationKey: 'foo',
        model: 'Address'
    }]
});

Fiddle: https://fiddle.sencha.com/#fiddle/914

Gotchas
If you run the fiddle above and check out the vendorsStore, you’ll see that we still get a reference to the appropriate parent model on each of the address models. One gotcha with this approach is that you still need to include the association model classname in your requires array. If you leave that out, your association data won’t load and you won’t even get a console error. So, don’t forget to put that in.

It’s also worth noting that “associationKey” is still available as a config option for hasMany associations. Unfortunately, the documentation for the hasMany association config is completely gone. You can always refer to the Ext JS 4 docs until/if this gets added to the 5.0 docs.

Finally, under the covers, Ext JS 5 will actually run your hasMany associations through Ext.data.schema.Schema.addLegacyHasMany(). So, if you are curious, you can set a breakpoint at the end of that function and inspect the association it creates. You could then use that information to convert your hasManys over to field references if you wanted to.

The Ugly

Who the hell is that? One bastard association goes in, another one comes out.

Overrides
As hard as framework authors try, there is just no way they can predict or even accommodate everyone’s needs. We’ve had some requirements on our own projects that have needed some overrides. Remember, the danger with overrides is that, as the framework changes, your overrides may break. Be sure to pay close attention to your unit tests for these when you upgrade. You are doing unit testing right? OK, let’s look at these overrides in order.

1) Missing Association Data on Some Records

What if the server doesn’t send the nested association data on every record? Some servers won’t send empty objects in an attempt to keep transmission sizes down. For example, if you look at the fiddle again, you’ll notice in vendors.json that “Granite Parts” doesn’t have an address. What would normally happen is that the record with the missing data would not get an association store. We can change this with an override to Ext.data.schema.ManyToOne so that every record gets an association store even if its just an empty one.

/**
 * Association ManyToOne extension
 */
Ext.define('MyApp.data.schema.ManyToOne', {
  override: 'Ext.data.schema.ManyToOne',

  constructor: function(config) {
     this.Left.override({
        /**
         * The original function had a check to make sure that the read root for this
         * association existed in the data object. In our case, the server may not send
         * the association data if its empty. In that case, we want an empty store.
         */
        read: function(rightRecord, node, fromReader, readOptions) {
           var me = this,
               // We use the inverse role here since we're setting ourselves
               // on the other record
               key = me.inverse.role,

               // result = me.callParent([ rightRecord, node, fromReader, readOptions ]),
               result = me.callSuper([ rightRecord, node, fromReader, readOptions ]),
               store, leftRecords, len, i;
                
           // Did the root exist in the data?
           if (result.getReadRoot()) {

               // Create the store and dump the data
               store = rightRecord[me.getterName](null, null, result.getRecords());

               // Inline associations should *not* arrive on the "data" object:
               // delete rightRecord.data[me.role];
               delete rightRecord.data[me.associationKey ? me.associationKey : me.role];

               leftRecords = store.getData().items;

               for (i = 0, len = leftRecords.length; i < len; ++i) {
                   leftRecords[i][key] = rightRecord;
               }
                
           // Create an empty store if the root doesn't exist
           } else {
               store = rightRecord[me.getterName](null, null, []);
           }
        }
      });

      this.callParent(arguments);
   }
});

Fiddle: https://fiddle.sencha.com/#fiddle/914

2) Cleanup of “associationKey” Data
Association data for hasMany associations that uses an “associationKey” remains on the data object. Ext JS does a fine job of cleaning the data object of association data properties. By that I mean you won’t see an “address” property on any of the record data objects. It only falls down when you are using an “associationKey” on a legacy hasMany. So, you’ll notice up above, that we’ve replaced the delete rightRecord.data[me.role]; statement with one that supports association keys. If you are using dot notation in your associationKey then you’ll need to modify the code above to traverse the data object and delete the appropriate property.

3) Support for “storeConfig” on hasMany
hasMany config objects used to support a storeConfig property which was especially handy when you needed to supply your association store with sorters, etc. Let’s add support back in with an override to Ext.data.schema.Schema. Technically, the logic should go in the addLegacyHasMany method but we are going to put our custom code into the addReference method because it’s less invasive.

/**
 * Schema extension
 */
Ext.define('Schema', {
    override: 'Ext.data.schema.Schema',

    addReference: function (entityType, referenceField, descr, unique) {

        // add the storeConfig from the legacy hasMany association to the inverse
        if (descr.legacy && descr.inverse && descr.storeConfig && !descr.inverse.storeConfig) {
            descr.inverse.storeConfig = descr.storeConfig;
        }

        this.callParent(arguments);
    }
});

Fiddle: https://fiddle.sencha.com/#fiddle/914

Fin

With those overrides in place, we can now handle missing data from the server, clean up after association keys and sort our association stores. Now if we could only convince Sencha to support the Ext.data.Field.reference approach from the right/parent side…


Like What You See?

Got any questions?