THE BLOG

News, tips and tricks from 2Gears

Using Model Associations in Sencha Touch 2 and Ext JS 4

Posted · 44 Comments
Model associations feature 150

The data package in Sencha Touch and ExtJS is awesome. Models let you easily and robustly configure your data structures and easily use them in all sorts of components. One of the model features that have a lot of potential but are used and understood relatively poorly are model associations. One of the most interesting uses of associations is that they allow you to use parent data when using the model in components. In practice this can be quite hard to accomplish however when all stores load their own data. In this blogpost I will show how to use parent (belongsTo) relations to automatically fetch and use parent data.

Model associations

First some basics about model associations. Associations define relationships between models. In Sencha Touch and ExtJS there are 3 types of associations:

  • hasOne – The model instance has one and only one sibling (of that type)
  • hasMany – The model instance has multiple children
  • belongsTo – The model instance has one and only one parent (of that type)

Sencha Touch

In Sencha Touch, models that implement the above illustration would be implemented as follows:

Ext.define('MyApp.model.Owner', {
	extend: 'Ext.data.Model',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'firstName' },
			{ name: 'lastName' }
		],
		hasMany: [{ model: 'MyApp.model.Car' }]
	}
});

Ext.define("MyApp.model.Car", {
	extend: 'Ext.data.Model',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'brandName' },
			{ name: 'type' },
			{ name: 'ownerId' }
		],
		belongsTo: [{ model: 'MyApp.model.Owner', associationKey: 'ownerId' }],
		hasOne: [{ model: 'MyApp.model.Engine', associationKey: 'engineId' }],
		hasMany: [{ model: 'MyApp.model.Tyre' }]
	}
});

Ext.define("MyApp.model.Engine", {
	extend: 'Ext.data.Model',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'cilinders' }
		],
		hasOne: [{ model: 'MyApp.model.Car' }]
	}
});

Ext.define("MyApp.model.Tyre", {
	extend: 'Ext.data.Model',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'brandName' },
			{ name: 'position' },
			{ name: 'carId' }
		],
		belongsTo: [{ model: 'MyApp.model.Car', associationKey: 'carId' }]
	}
});

What we want

Now that we setup our associations, let’s put them to use.

For an imaginary car tyre dealer we are creating a mobile app. The client wants to include a list of all the tyres currently attached to it’s clients cars. The list should include the Tyre brand and position on the car, the car brand of the car it’s attached to and the client name.

We create 3 stores, a ‘Owners’ store containing the clients, a ‘Cars’ store that contains all cars and a ‘Tyres’ store that contains all tyres. The three stores operate on their respective models ‘Owner’, ‘Car’ and ‘Tyre’.

Now this is where it gets tricky. The ‘Tyres’ list operates on a store that contains ‘Tyre’ records. The ‘car brand’ and ‘customer name’ are part of other models however. Because we setup the ‘belongsTo’ relations on the Tyre and Car model we would think we can use those associations to gather the car brand name and client name. Unfortunately, that is not the case.

The Problem

The reason for the list not being able to display associated data has to do with the way stores load their data from a backend server.

Simply put, the stores load data from the backend throught their configured proxy (which have not been drawn) and instantiate records (model instances) from that data. The load process is in most cases asynchronous. This means that the Tyre and Car records will be instantiated without knowing about their related (instantiated) counterparts. So when we fetch a Tyre record from the store, it has no reference to a parent Car record we would like to use in the list component. The only way to accomplish that is to load the entire data structure of owners, including car and tyre children in 1 go using a hyrarchical data structure. In practice this way of loading data is cumbersome and mostly unsupported by typical standardized backend API’s .

The solution

Of course, in Sencha Touch and ExtJS there is always one or more ways to do it. After brainstorming a bit (Aaron Smith, thanks for your time) we figured out a solution. Our models need to be provided with a way to initiate the relations with other models when they are needed in components. Besides the definitions of the associations themselves on the model we need to instruct the model where the related objects can be found.

Most components get their data from models through the ‘getData’ method on the Model. That method already consumes an argument telling it to incorporate associated data in the results. The only problem is that it can’t find the related records. Therefore we chose to inject some custom code in the getData method that searches the associations. The easiest way for us was to let all our models inherit from a BaseModel class that includes those additions. By providing a couple of additions to the association we can completely transparently let the models take care of everything.

BaseModel

First we create our BaseModel that includes a couple of methods:

  • linkAssociations – Iterates over all parent (belongsTo) and straight (hasOne) associations. If the association includes our special foreignStore config we use the StoreManager to find the related record. The link to the related record is stored in a way in which Sencha Touch automatically picks it up.
  • linkChildAssociations – Used to fetch the direct children (hasMany) of the record and again stored in a way the default model handlers pick it up. When using this recursive, things can quickly spin out of control, be careful with this one!
  • getFlattenedData – Turns a hierarchical structure, fetched with getData, into a flat hash. The keys contain the relations in dot format. This way, the relations can be used for instance in a form panel.
Ext.define("MyApp.model.BaseModel", {
	extend: 'Ext.data.Model',

	linkedAssociations: false,

	/* uses information from the associations to fetch a parent from an associated store */
	getParent: function(assocName) {
		var assoc = this.associations.get(assocName);
		if (!assoc) {
			return null;
		}
		var store = Ext.StoreMgr.get(assoc.config.foreignStore);
		if (!store) {
			return null;
		}

		return store.findRecord(assoc.config.primaryKey, this.get(assoc.config.foreignKey)) || undefined;
	},

	getChildren: function(assocName) {
		var assoc = this.associations.get(assocName),
			id = this.get(assoc.config.primaryKey);

		if (!assoc) {
			return null;
		}
		var store = Ext.StoreMgr.get(assoc.config.foreignStore);
		if (!store) {
			return null;
		}

		store.suspendEvents(); /* make sure the store does not fire all sorts of events, triggering stuff we dont want */
		store.clearFilter();
		store.filterBy(function(record) {
			return record.get(assoc.config.foreignKey) === id;
		});

		var range = store.getRange(); // return array of records
		store.clearFilter();
		store.resumeEvents();

		return range;
	},

	/* warning, recursive down in combination with up can be dangerous when there are loops in associations */
	getData: function(includeAssociated,down) { 
        if (includeAssociated && !this.linkedAssociations) {
			this.linkedAssociations = true;
			this.linkChildAssociations(includeAssociated);
			this.linkAssociations(includeAssociated);
        }

		var data = this.callParent(arguments);
		return data;
    },

	getFlattenedData: function(includeAssociated) {
		var data = this.getData(includeAssociated, false); // don't ever recurse down when getting flattened data!

		/* This function flattens the datastructure of am object such that it can be used in a form
		 * {foo:1,bar:{blah: {boo: 3}}} becomes {foo: 1, bar.blah.boo: 3}
		 * This is the only way to use associated data in a form
		 * thanks to http://stackoverflow.com/users/2214/matthew-crumley
		 */
		var count=1;
		var prop;
		var flatten = function(obj, includePrototype, into, prefix) {
			if (count++ > 20) {console.log('TOO DEEP RECURSION'); return;} // prevent infinite recursion
		    into = into || {};
		    prefix = prefix || "";

		    for (var k in obj) {
		        if (includePrototype || obj.hasOwnProperty(k)) {
		            var prop = obj[k];
					if (prop instanceof Array) { continue; } // Don't recurse into hasMany relations
		            if (prop && typeof prop === "object" &&
		                !(prop instanceof Date || prop instanceof RegExp)) {
		                flatten(prop, includePrototype, into, prefix + k + ".");
		            }
		            else {
		                into[prefix + k] = prop;
		            }
		        }
		    }

		    return into;
		};

		return flatten(data, false);
	},

	/* this function ONLY recurses upwards (belongsTo), otherwise the data structure could become infinite */
	linkAssociations: function(includeAssociated, count) {
		var associations = this.associations.items,
			associationCount = associations.length,
			associationName,
			association,
			associatedRecord,
			i,
			type,
			foreignStore;

		count = count || 0;

		if (count > 10) {
			console.log('Too deep recursion in linkAssociations');
			return;
		}

		for (i = 0; i < associationCount; i++) {
			association = associations[i];
			associationName = association.getName();
			type = association.getType();
			foreignStore = association.config.foreignStore;

			if (!foreignStore) {
				continue;
			}

			if (type.toLowerCase() == 'belongsto' || type.toLowerCase() == 'hasone') {
				associatedRecord = this.getParent(associationName);
				if (associatedRecord) {
					this[association.getInstanceName()] = associatedRecord;
					associatedRecord.linkAssociations(includeAssociated, (count+1));
				}
			}
		}
	},

	linkChildAssociations: function(includeAssociated, count) {
		var associations = this.associations.items,
			associationCount = associations.length,
			associationName,
			association,
			associatedRecord,
			i,
			type,
			foreignStore;

		count = count || 0;

		if (count > 10) {
			console.log('Too deep recursion in linkAssociations');
			return;
		}

		for (i = 0; i < associationCount; i++) {
			association = associations[i];
			associationName = association.getName();
			type = association.getType();
			foreignStore = association.config.foreignStore;

			if (!foreignStore) {
				continue;
			}

			if (type.toLowerCase() == 'hasmany') {
				var children = this.getChildren(associationName);
				association.setStoreName('hasMany_'+associationName+'_'+Ext.id());
				var store = Ext.create('Ext.data.Store',{
					model: association.config.associatedModel
				});
				store.add(children);
				this[association.getStoreName()] = store;
			}
		}
	}
});

Models

We need a couple of minor changes to our model associations specified above:

  • primaryKey – the field in the parent that identifies it.
  • foreignKey – the key that identifies the parent in the child. In a belongsTo or hasOne relation, this is part of the model itself, in a hasMany relation this is a field of the child objects that refer to my Id.
  • foreignStore – the store name that contains the related records

The adjusted model definitions become the following:

Ext.define("MyApp.model.Owner", {
	extend: 'MyApp.model.BaseModel',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'firstName' },
			{ name: 'lastName' }
		],
		hasMany: [{
			model: 'MyApp.model.Car',
			name: 'Car',
			primaryKey: 'Id',
			foreignKey: 'ownerId',
			foreignStore: 'Cars'
		}]
	}
});

Ext.define("MyApp.model.Car", {
	extend: 'MyApp.model.BaseModel',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'brandName' },
			{ name: 'type' },
			{ name: 'ownerId' },
			{ name: 'engineId' }
		],
		belongsTo: [{
			model: 'MyApp.model.Owner',
			name: 'Owner',
			primaryKey: 'Id',
			foreignKey: 'ownerId',
			foreignStore: 'Owners'
		}],
		hasMany:  [{
			model: 'MyApp.model.Tyre',
			name: 'Tyre',
			primaryKey: 'Id',
			foreignKey: 'carId',
			foreignStore: 'Tyres'
		}],
		hasOne:	 [{
			model: 'MyApp.model.Engine',
			name: 'Engine',
			primaryKey: 'Id',
			foreignKey: 'engineId',
			foreignStore: 'Engines'
		}]
	}
});

Ext.define("MyApp.model.Engine", {
	extend: 'MyApp.model.BaseModel',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'cilinders' },
			{ name: 'carId' }
		]
	},
	hasOne:	 [{
		model: 'MyApp.model.Car',
		name: 'Car',
		primaryKey: 'Id',
		foreignKey: 'carId',
		foreignStore: 'Cars'
	}]
});

Ext.define("MyApp.model.Tyre", {
	extend: 'MyApp.model.BaseModel',
	config: {
		idProperty: 'Id',
		fields: [
			{ name: 'Id' },
			{ name: 'brandName' },
			{ name: 'position' },
			{ name: 'carId' }
		],
		belongsTo: [{
			model: 'MyApp.model.Car',
			name: 'Car',
			primaryKey: 'Id',
			foreignKey: 'carId',
			foreignStore: 'Cars'
		}]
	}
});

List

Now that we structured our data let’s see how difficult it is to load related data in a List component. This assumes that the various stores have been configured using their respective model and set to autoLoad.

Ext.define("MyApp.view.TyreList", {
	extend: "Ext.List",
	config: {
		store: 'Tyres',
		emptyText: 'No tyres',
		itemTpl: [
			'<div class="myapp-list-item">',
				'<p>Client name: {Car.Owner.lastName}, {Car.Owner.firstName}</p>',
				'<p>Tyre brand: {brandName}</p>',
				'<p>Tyre position: {position}</p>',
				'<p>Car brand: {Car.brandName}</p>',
			'</div>'
		]
	}
});

That’s it! The List operates on the Tyres store but is able to use the defined associations to fetch and display parent data. Of course, the developer has to make sure that the parent relations are always there, otherwise the List’s XTemplate will croak. This has to do with the fact that at the moment of writing Sencha Touch triggers an error instead of a warning on these cases. Like everything there is also a way around this:

Ext.override(Ext.XTemplate,{
	applyOut: function(values, out) {
		var me = this,
			compiler;

		if (!me.fn) {
			compiler = new Ext.XTemplateCompiler({
				useFormat: me.disableFormats !== true
			});

			me.fn = compiler.compile(me.html);
		}

		try {
			me.fn.call(me, out, values, {}, 1, 1);
		} catch (e) {
			//
			Ext.Logger.log(e.message);
			//
		}
		return out;
	}
});

Conclusion

When understanding how associations work in Sencha Touch and ExtJS it’s pretty easy to make them work for you. The result is an enormous amount of freedom in the display and usage of associated data without having to resort to all kinds of nasty hacks. The above solution is one example of such a use with the associations neatly abstracted away in the data layer. Using the BaseModel’s getFlattenedData also allows one to use associated data in forms. This I will leave to the reader as a practical exercise. Good luck and let me know how it works for you.

In the next part of this series I will demonstrate how the next version of the base model can be used in a more real-life setup using SalesForce data.

44 Responses to "Using Model Associations in Sencha Touch 2 and Ext JS 4"
  1. George says:

    Please I have added the BaseModel class to my js files but when I run my project I get

    Uncaught Error: The following classes are not declared even if their files have been loaded: ‘Ext.data.BaseModel’. Please check the source code of their corresponding files for possible typos: ‘touch/src/data/BaseModel.js

    What could be the possible cause?

    • Rob Boerman says:

      Hi George,
      First of all, I would never recommend putting files in the Ext namespace. The unwritten rule is ‘never edit the framework’. The BaseModel in the post is in the MyApp namespace. I would start with that and then run Sencha Cmd to check if everything loads ok (‘sencha app refresh’ or ‘sencha app build testing’)

  2. Greg Silverman says:

    Hey Rob,
    Kept meaning to let tell you that the hasOne relations between Car and Engine create a circular reference. I think you want Engine to use a belongsTo association to the Car model.

    • admin says:

      Hey Greg, I actually did not test a hasOne relation on both sides. Where does it create a circular reference? I think the BaristaStuff example is better anyway :)

  3. Masud says:

    This looks very interesting. Can the same logic be used for a many-to-many relationship? Will you need to create a linking table to support many-to-many association? Any guidance would be highly appreciated.

    Thanks.

  4. Jithin says:

    I have two stores say ‘Polls’ & ‘Choices’ and their respective models ‘Poll’ & ‘Choices’. The models are associated in such a manner that ‘ Choice belongsTo Poll ‘ and ‘Poll hasMany Choice ‘ , the problem is I cannot access ‘Poll.Choice.choice ‘ and ‘m getting a warning like this

    [INFO][Ext.XTemplate#apply] Error: Choice is not defined

    any thoughts ??

  5. Julian Ashworth says:

    How many round-trips to the server does it take to load your list?

    thanks

  6. payk says:

    Hi Rob.
    Let’s say you have a JSON which you would want to be in your store.
    this JSON has an unknown amount of depth (meaning: its nested, children have attributes and may have children)

    Is that possible?
    Can i somehow use the same model which i am defining inside of the defintition?
    Or is there a clean way of doing that?

  7. Anand kumar says:

    Hi !

    New to sencha touch, as per your tutorial implemented the relation. But unable to fetch association data. Please check below code snippets.

    In Recipe Model:
    hasMany: [{
    model: 'Neoceres.model.RecipeIngredient',
    name: 'RecipeIngredient',
    primaryKey: 'Id',
    foreignKey: 'recipe_id',
    foreignStore: 'RecipeIngredient'
    }],

    In Recipe Ingrediants :
    belongsTo: [{
    model: 'Neoceres.model.Recipe',
    name: 'Recipe',
    primaryKey: 'ID',
    foreignKey: 'recipe_id',
    foreignStore: 'Recipe'
    }],

    Fetching the data as per below code

    var rIStore = Ext.getStore(‘RecipeIngredient’);
    var recipeIngredients = rIStore.getRange();
    //console.log(recipeIngredients);
    Ext.each(recipeIngredients,function(recipeIngredient){
    console.log(recipeIngredient.data.recipe_id);
    });
    That is giving only number of recipe.

    Help is very much appreciated.

    Thanks in Advance.

Leave a Reply

Your email address will not be published. Required fields are marked *

thirteen − twelve =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>