Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / data / BelongsToAssociation.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @author Ed Spencer
17  * @class Ext.data.BelongsToAssociation
18  * @extends Ext.data.Association
19  *
20  * <p>Represents a many to one association with another model. The owner model is expected to have
21  * a foreign key which references the primary key of the associated model:</p>
22  *
23 <pre><code>
24 Ext.define('Category', {
25     extend: 'Ext.data.Model',
26     fields: [
27         {name: 'id',   type: 'int'},
28         {name: 'name', type: 'string'}
29     ]
30 });
31
32 Ext.define('Product', {
33     extend: 'Ext.data.Model',
34     fields: [
35         {name: 'id',          type: 'int'},
36         {name: 'category_id', type: 'int'},
37         {name: 'name',        type: 'string'}
38     ],
39     // we can use the belongsTo shortcut on the model to create a belongsTo association
40     belongsTo: {type: 'belongsTo', model: 'Category'}
41 });
42 </code></pre>
43  * <p>In the example above we have created models for Products and Categories, and linked them together
44  * by saying that each Product belongs to a Category. This automatically links each Product to a Category
45  * based on the Product's category_id, and provides new functions on the Product model:</p>
46  *
47  * <p><u>Generated getter function</u></p>
48  *
49  * <p>The first function that is added to the owner model is a getter function:</p>
50  *
51 <pre><code>
52 var product = new Product({
53     id: 100,
54     category_id: 20,
55     name: 'Sneakers'
56 });
57
58 product.getCategory(function(category, operation) {
59     //do something with the category object
60     alert(category.get('id')); //alerts 20
61 }, this);
62 </code></pre>
63 *
64  * <p>The getCategory function was created on the Product model when we defined the association. This uses the
65  * Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided
66  * callback when it has loaded.</p>
67  *
68  * <p>The new getCategory function will also accept an object containing success, failure and callback properties
69  * - callback will always be called, success will only be called if the associated model was loaded successfully
70  * and failure will only be called if the associatied model could not be loaded:</p>
71  *
72 <pre><code>
73 product.getCategory({
74     callback: function(category, operation) {}, //a function that will always be called
75     success : function(category, operation) {}, //a function that will only be called if the load succeeded
76     failure : function(category, operation) {}, //a function that will only be called if the load did not succeed
77     scope   : this //optionally pass in a scope object to execute the callbacks in
78 });
79 </code></pre>
80  *
81  * <p>In each case above the callbacks are called with two arguments - the associated model instance and the
82  * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
83  * useful when the instance could not be loaded.</p>
84  *
85  * <p><u>Generated setter function</u></p>
86  *
87  * <p>The second generated function sets the associated model instance - if only a single argument is passed to
88  * the setter then the following two calls are identical:</p>
89  *
90 <pre><code>
91 //this call
92 product.setCategory(10);
93
94 //is equivalent to this call:
95 product.set('category_id', 10);
96 </code></pre>
97  * <p>If we pass in a second argument, the model will be automatically saved and the second argument passed to
98  * the owner model's {@link Ext.data.Model#save save} method:</p>
99 <pre><code>
100 product.setCategory(10, function(product, operation) {
101     //the product has been saved
102     alert(product.get('category_id')); //now alerts 10
103 });
104
105 //alternative syntax:
106 product.setCategory(10, {
107     callback: function(product, operation), //a function that will always be called
108     success : function(product, operation), //a function that will only be called if the load succeeded
109     failure : function(product, operation), //a function that will only be called if the load did not succeed
110     scope   : this //optionally pass in a scope object to execute the callbacks in
111 })
112 </code></pre>
113 *
114  * <p><u>Customisation</u></p>
115  *
116  * <p>Associations reflect on the models they are linking to automatically set up properties such as the
117  * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:</p>
118  *
119 <pre><code>
120 Ext.define('Product', {
121     fields: [...],
122
123     associations: [
124         {type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}
125     ]
126 });
127  </code></pre>
128  *
129  * <p>Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
130  * with our own settings. Usually this will not be needed.</p>
131  */
132 Ext.define('Ext.data.BelongsToAssociation', {
133     extend: 'Ext.data.Association',
134
135     alias: 'association.belongsto',
136
137     /**
138      * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
139      * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
140      * model called Product would set up a product_id foreign key.
141      * <pre><code>
142 Ext.define('Order', {
143     extend: 'Ext.data.Model',
144     fields: ['id', 'date'],
145     hasMany: 'Product'
146 });
147
148 Ext.define('Product', {
149     extend: 'Ext.data.Model',
150     fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
151     belongsTo: 'Group'
152 });
153 var product = new Product({
154     id: 1,
155     name: 'Product 1',
156     order_id: 22
157 }, 1);
158 product.getOrder(); // Will make a call to the server asking for order_id 22
159
160      * </code></pre>
161      */
162
163     /**
164      * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
165      * Defaults to 'get' + the name of the foreign model, e.g. getCategory
166      */
167
168     /**
169      * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
170      * Defaults to 'set' + the name of the foreign model, e.g. setCategory
171      */
172     
173     /**
174      * @cfg {String} type The type configuration can be used when creating associations using a configuration object.
175      * Use 'belongsTo' to create a HasManyAssocation
176      * <pre><code>
177 associations: [{
178     type: 'belongsTo',
179     model: 'User'
180 }]
181      * </code></pre>
182      */
183
184     constructor: function(config) {
185         this.callParent(arguments);
186
187         var me             = this,
188             ownerProto     = me.ownerModel.prototype,
189             associatedName = me.associatedName,
190             getterName     = me.getterName || 'get' + associatedName,
191             setterName     = me.setterName || 'set' + associatedName;
192
193         Ext.applyIf(me, {
194             name        : associatedName,
195             foreignKey  : associatedName.toLowerCase() + "_id",
196             instanceName: associatedName + 'BelongsToInstance',
197             associationKey: associatedName.toLowerCase()
198         });
199
200         ownerProto[getterName] = me.createGetter();
201         ownerProto[setterName] = me.createSetter();
202     },
203
204     /**
205      * @private
206      * Returns a setter function to be placed on the owner model's prototype
207      * @return {Function} The setter function
208      */
209     createSetter: function() {
210         var me              = this,
211             ownerModel      = me.ownerModel,
212             associatedModel = me.associatedModel,
213             foreignKey      = me.foreignKey,
214             primaryKey      = me.primaryKey;
215
216         //'this' refers to the Model instance inside this function
217         return function(value, options, scope) {
218             this.set(foreignKey, value);
219
220             if (typeof options == 'function') {
221                 options = {
222                     callback: options,
223                     scope: scope || this
224                 };
225             }
226
227             if (Ext.isObject(options)) {
228                 return this.save(options);
229             }
230         };
231     },
232
233     /**
234      * @private
235      * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
236      * the first time it is loaded so that subsequent calls to the getter always receive the same reference.
237      * @return {Function} The getter function
238      */
239     createGetter: function() {
240         var me              = this,
241             ownerModel      = me.ownerModel,
242             associatedName  = me.associatedName,
243             associatedModel = me.associatedModel,
244             foreignKey      = me.foreignKey,
245             primaryKey      = me.primaryKey,
246             instanceName    = me.instanceName;
247
248         //'this' refers to the Model instance inside this function
249         return function(options, scope) {
250             options = options || {};
251
252             var model = this,
253                 foreignKeyId = model.get(foreignKey),
254                 instance,
255                 args;
256
257             if (model[instanceName] === undefined) {
258                 instance = Ext.ModelManager.create({}, associatedName);
259                 instance.set(primaryKey, foreignKeyId);
260
261                 if (typeof options == 'function') {
262                     options = {
263                         callback: options,
264                         scope: scope || model
265                     };
266                 }
267
268                 associatedModel.load(foreignKeyId, options);
269                 model[instanceName] = associatedModel;
270                 return associatedModel;
271             } else {
272                 instance = model[instanceName];
273                 args = [instance];
274                 scope = scope || model;
275                 
276                 //TODO: We're duplicating the callback invokation code that the instance.load() call above
277                 //makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer
278                 //instead of the association layer.
279                 Ext.callback(options, scope, args);
280                 Ext.callback(options.success, scope, args);
281                 Ext.callback(options.failure, scope, args);
282                 Ext.callback(options.callback, scope, args);
283
284                 return instance;
285             }
286         };
287     },
288
289     /**
290      * Read associated data
291      * @private
292      * @param {Ext.data.Model} record The record we're writing to
293      * @param {Ext.data.reader.Reader} reader The reader for the associated model
294      * @param {Object} associationData The raw associated data
295      */
296     read: function(record, reader, associationData){
297         record[this.instanceName] = reader.read([associationData]).records[0];
298     }
299 });
300