]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/data/schema/ManyToOne.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / data / schema / ManyToOne.js
CommitLineData
6527f429
DM
1/**\r
2 * This type of association describes the case where one entity is referenced by zero or\r
3 * more other entities typically using a "foreign key" field.\r
4 * \r
5 * The way this is defined is for one entity to have a field that holds the unique id (also\r
6 * known as "Primary Key" or, more specifically, as the {@link Ext.data.Model#idProperty}\r
7 * field) of the related entity. These fields have a {@link Ext.data.field.Field#reference}\r
8 * in their definition. The value in the `reference` field of an entity instance holds the\r
9 * value of the id of the related entity instance. Since many entities can hold the same\r
10 * value in a `reference` field, this allows many entities to reference one entity.\r
11 * OrderItem has a foreign key to Order.\r
12 * \r
13 * OrderItem -> Order\r
14 * \r
15 * OrderItem is on the "left" and Order is on the "right". This is because the owner of\r
16 * the foreign key is always on the "left". Many OrderItems refer to one Order. The\r
17 * default name of this association would be "Order_OrderItems".\r
18 * \r
19 * var Order_OrderItems = {\r
20 * name: 'Order_OrderItems',\r
21 * owner: Order_OrderItems.right,\r
22 * left: {\r
23 * cls: OrderItem,\r
24 * type: 'OrderItem',\r
25 * association: Order_OrderItems,\r
26 * left: true,\r
27 * owner: false,\r
28 * autoLoad: true,\r
29 * isMany: true,\r
30 * inverse: Order_OrderItems.right,\r
31 * role: 'orderItems'\r
32 * },\r
33 * right: {\r
34 * cls: Order,\r
35 * type: 'Order',\r
36 * association: Order_OrderItems,\r
37 * left: false,\r
38 * owner: true,\r
39 * autoLoad: true,\r
40 * isMany: false,\r
41 * inverse: Order_OrderItems.left,\r
42 * role: 'order'\r
43 * }\r
44 * };\r
45 * \r
46 * OrderItem.associations.order = Order_OrderItems.left;\r
47 * Order.associations.orderItems = Order_OrderItems.right;\r
48 */\r
49Ext.define('Ext.data.schema.ManyToOne', {\r
50 extend: 'Ext.data.schema.Association',\r
51\r
52 isManyToOne: true,\r
53\r
54 isToOne: true,\r
55\r
56 kind: 'many-to-one',\r
57\r
58 Left: Ext.define(null, {\r
59 extend: 'Ext.data.schema.Role',\r
60\r
61 isMany: true,\r
62\r
63 onDrop: function(rightRecord, session) {\r
64 var me = this,\r
65 store = me.getAssociatedItem(rightRecord),\r
66 leftRecords, len, i, refs, id;\r
67\r
68 if (store) {\r
69 // Removing will cause the foreign key to be set to null.\r
70 leftRecords = store.removeAll();\r
71 if (leftRecords && me.inverse.owner) {\r
72 // If we're a child, we need to destroy all the "tickets"\r
73 for (i = 0, len = leftRecords.length; i < len; ++i) {\r
74 leftRecords[i].drop();\r
75 }\r
76 }\r
77\r
78 store.destroy();\r
79 rightRecord[me.getStoreName()] = null;\r
80 } else if (session) {\r
81 leftRecords = session.getRefs(rightRecord, me);\r
82 if (leftRecords) {\r
83 for (id in leftRecords) {\r
84 leftRecords[id].drop();\r
85 }\r
86 }\r
87 }\r
88 },\r
89\r
90 processUpdate: function(session, associationData) {\r
91 var me = this,\r
92 entityType = me.inverse.cls,\r
93 items = associationData.R,\r
94 id, rightRecord, store, leftRecords;\r
95\r
96 if (items) {\r
97 for (id in items) {\r
98 rightRecord = session.peekRecord(entityType, id);\r
99 if (rightRecord) {\r
100 leftRecords = session.getEntityList(me.cls, items[id]);\r
101 store = me.getAssociatedItem(rightRecord);\r
102 if (store) {\r
103 store.loadData(leftRecords);\r
104 store.complete = true;\r
105 } else {\r
106 // We don't have a store. Create it and add the records.\r
107 rightRecord[me.getterName](null, null, leftRecords);\r
108 }\r
109 } else {\r
110 session.onInvalidAssociationEntity(entityType, id);\r
111 }\r
112 }\r
113 }\r
114 },\r
115\r
116 findRecords: function(session, rightRecord, leftRecords, allowInfer) {\r
117 var ret = leftRecords,\r
118 refs = session.getRefs(rightRecord, this, true),\r
119 field = this.association.field,\r
120 fieldName = field.name,\r
121 leftRecord, id, i, len, seen;\r
122\r
123 if (!rightRecord.phantom) {\r
124 ret = [];\r
125 if (refs || allowInfer) {\r
126 if (leftRecords) {\r
127 seen = {};\r
128 // Loop over the records returned by the server and\r
129 // check they all still belong. If the session doesn't have any prior knowledge\r
130 // and we're allowed to infer the parent id (via nested loading), only do so if\r
131 // we explicitly have an id specified\r
132 for (i = 0, len = leftRecords.length; i < len; ++i) {\r
133 leftRecord = leftRecords[i];\r
134 id = leftRecord.id;\r
135 if (refs && refs[id]) {\r
136 ret.push(leftRecord);\r
137 } else if (allowInfer && leftRecord.data[fieldName] === undefined) {\r
138 ret.push(leftRecord);\r
139 leftRecord.data[fieldName] = rightRecord.id;\r
140 session.updateReference(leftRecord, field, rightRecord.id, undefined);\r
141 }\r
142 seen[id] = true;\r
143 }\r
144 }\r
145\r
146 // Loop over the expected set and include any missing records.\r
147 if (refs) {\r
148 for (id in refs) {\r
149 if (!seen || !seen[id]) {\r
150 ret.push(refs[id]);\r
151 }\r
152 }\r
153 }\r
154 }\r
155 }\r
156 return ret;\r
157 },\r
158\r
159 processLoad: function(store, rightRecord, leftRecords, session) {\r
160 var ret = leftRecords;\r
161\r
162 if (session) {\r
163 // Allow infer here, we only get called when loading an associated store\r
164 ret = this.findRecords(session, rightRecord, leftRecords, true);\r
165 }\r
166 this.onLoadMany(rightRecord, ret, session);\r
167 return ret;\r
168 },\r
169\r
170 adoptAssociated: function(rightRecord, session) {\r
171 var store = this.getAssociatedItem(rightRecord),\r
172 leftRecords, i, len;\r
173 if (store) {\r
174 store.setSession(session);\r
175 leftRecords = store.getData().items;\r
176 for (i = 0, len = leftRecords.length; i < len; ++i) {\r
177 session.adopt(leftRecords[i]);\r
178 }\r
179 }\r
180 },\r
181\r
182 createGetter: function() {\r
183 var me = this;\r
184 return function (options, scope, leftRecords) {\r
185 // 'this' refers to the Model instance inside this function\r
186 return me.getAssociatedStore(this, options, scope, leftRecords, me, true);\r
187 };\r
188 },\r
189\r
190 createSetter: null, // no setter for an isMany side\r
191\r
192 onAddToMany: function (store, leftRecords) {\r
193 this.syncFK(leftRecords, store.getAssociatedEntity(), false);\r
194 },\r
195\r
196 onLoadMany: function(rightRecord, leftRecords, session) {\r
197 var instanceName = this.inverse.getInstanceName(),\r
198 id = rightRecord.getId(),\r
199 field = this.association.field,\r
200 i, len, leftRecord, oldId, data, name;\r
201\r
202 if (field) {\r
203 for (i = 0, len = leftRecords.length; i < len; ++i) {\r
204 leftRecord = leftRecords[i];\r
205 leftRecord[instanceName] = rightRecord;\r
206 if (field) {\r
207 name = field.name;\r
208 data = leftRecord.data;\r
209 oldId = data[name];\r
210 if (oldId !== id) {\r
211 data[name] = id;\r
212 if (session) {\r
213 session.updateReference(leftRecord, field, id, oldId);\r
214 }\r
215 }\r
216 }\r
217 }\r
218 }\r
219 },\r
220\r
221 onRemoveFromMany: function (store, leftRecords) {\r
222 this.syncFK(leftRecords, store.getAssociatedEntity(), true);\r
223 },\r
224\r
225 read: function(rightRecord, node, fromReader, readOptions) {\r
226 var me = this,\r
227 // We use the inverse role here since we're setting ourselves\r
228 // on the other record\r
229 instanceName = me.inverse.getInstanceName(),\r
230 leftRecords = me.callParent([rightRecord, node, fromReader, readOptions]),\r
231 store, len, i;\r
232 \r
233 if (leftRecords) {\r
234 // Create the store and dump the data\r
235 store = rightRecord[me.getterName](null, null, leftRecords);\r
236 // Inline associations should *not* arrive on the "data" object:\r
237 delete rightRecord.data[me.role];\r
238\r
239 leftRecords = store.getData().items;\r
240\r
241 for (i = 0, len = leftRecords.length; i < len; ++i) {\r
242 leftRecords[i][instanceName] = rightRecord;\r
243 }\r
244 }\r
245 },\r
246\r
247 syncFK: function (leftRecords, rightRecord, clearing) {\r
248 // We are called to set things like the FK (ticketId) of an array of Comment\r
249 // entities. The best way to do that is call the setter on the Comment to set\r
250 // the Ticket. Since we are setting the Ticket, the name of that setter is on\r
251 // our inverse role.\r
252\r
253 var foreignKeyName = this.association.getFieldName(),\r
254 inverse = this.inverse,\r
255 setter = inverse.setterName, // setTicket\r
256 instanceName = inverse.getInstanceName(),\r
257 i = leftRecords.length,\r
258 id = rightRecord.getId(),\r
259 different, leftRecord, val;\r
260\r
261 while (i-- > 0) {\r
262 leftRecord = leftRecords[i];\r
263 different = !leftRecord.isEqual(id, leftRecord.get(foreignKeyName));\r
264\r
265 val = clearing ? null : rightRecord;\r
266 if (different !== clearing) {\r
267 // clearing === true\r
268 // different === true :: leave alone (not associated anymore)\r
269 // ** different === false :: null the value (no longer associated)\r
270 //\r
271 // clearing === false\r
272 // ** different === true :: set the value (now associated)\r
273 // different === false :: leave alone (already associated)\r
274 //\r
275 leftRecord.changingKey = true;\r
276 leftRecord[setter](val);\r
277 leftRecord.changingKey = false;\r
278 } else {\r
279 // Ensure we set the instance, we may only have the key\r
280 leftRecord[instanceName] = val;\r
281 }\r
282 }\r
283 }\r
284 }),\r
285\r
286 Right: Ext.define(null, {\r
287 extend: 'Ext.data.schema.Role',\r
288\r
289 left: false,\r
290 side: 'right',\r
291\r
292 onDrop: function(leftRecord, session) {\r
293 // By virtue of being dropped, this record will be removed\r
294 // from any stores it belonged to. The only case we have\r
295 // to worry about is if we have a session but were not yet\r
296 // part of any stores, so we need to clear the foreign key.\r
297 var field = this.association.field;\r
298 if (field) {\r
299 leftRecord.set(field.name, null);\r
300 }\r
301 leftRecord[this.getInstanceName()] = null;\r
302 },\r
303\r
304 createGetter: function() {\r
305 // As the target of the FK (say "ticket" for the Comment entity) this\r
306 // getter is responsible for getting the entity referenced by the FK value.\r
307 var me = this;\r
308\r
309 return function (options, scope) {\r
310 // 'this' refers to the Comment instance inside this function\r
311 return me.doGetFK(this, options, scope);\r
312 };\r
313 },\r
314 \r
315 createSetter: function() {\r
316 var me = this;\r
317\r
318 return function (rightRecord, options, scope) {\r
319 // 'this' refers to the Comment instance inside this function\r
320 return me.doSetFK(this, rightRecord, options, scope);\r
321 };\r
322 },\r
323\r
324 checkMembership: function(session, leftRecord) {\r
325 var field = this.association.field,\r
326 store;\r
327\r
328 store = this.getSessionStore(session, leftRecord.get(field.name));\r
329 // Check we're not in the middle of an add to the store.\r
330 if (store && !store.contains(leftRecord)) {\r
331 store.add(leftRecord);\r
332 }\r
333 },\r
334\r
335 onValueChange: function(leftRecord, session, newValue, oldValue) {\r
336 // If we have a session, we may be able to find the new store this belongs to\r
337 // If not, the best we can do is to remove the record from the associated store/s.\r
338 var me = this,\r
339 instanceName = me.getInstanceName(),\r
340 cls = me.cls,\r
341 hasNewValue,\r
342 joined, store, i, len, associated, rightRecord;\r
343\r
344 if (!leftRecord.changingKey) {\r
345 hasNewValue = newValue || newValue === 0;\r
346 if (!hasNewValue) {\r
347 leftRecord[instanceName] = null;\r
348 }\r
349 if (session) {\r
350 // Find the store that holds this record and remove it if possible.\r
351 store = me.getSessionStore(session, oldValue);\r
352 if (store) {\r
353 store.remove(leftRecord);\r
354 }\r
355 // If we have a new value, try and find it and push it into the new store.\r
356 if (hasNewValue) {\r
357 store = me.getSessionStore(session, newValue);\r
358 if (store && !store.isLoading()) {\r
359 store.add(leftRecord);\r
360 }\r
361 if (cls) {\r
362 rightRecord = session.peekRecord(cls, newValue);\r
363 }\r
364 // Setting to undefined is important so that we can load the record later.\r
365 leftRecord[instanceName] = rightRecord || undefined;\r
366 }\r
367 } else {\r
368 joined = leftRecord.joined;\r
369 if (joined) {\r
370 for (i = 0, len = joined.length; i < len; ++i) {\r
371 store = joined[i];\r
372 if (store.isStore) {\r
373 associated = store.getAssociatedEntity();\r
374 if (associated && associated.self === me.cls && associated.getId() === oldValue) {\r
375 store.remove(leftRecord);\r
376 }\r
377 }\r
378 }\r
379 }\r
380 }\r
381 }\r
382\r
383 if (me.owner && newValue === null) {\r
384 me.association.schema.queueKeyCheck(leftRecord, me);\r
385 }\r
386 },\r
387\r
388 checkKeyForDrop: function(leftRecord) {\r
389 var field = this.association.field;\r
390 if (leftRecord.get(field.name) === null) {\r
391 leftRecord.drop();\r
392 }\r
393 },\r
394\r
395 getSessionStore: function(session, value) {\r
396 // May not have the cls loaded yet\r
397 var cls = this.cls,\r
398 rec;\r
399\r
400 if (cls) {\r
401 rec = session.peekRecord(cls, value);\r
402\r
403 if (rec) {\r
404 return this.inverse.getAssociatedItem(rec);\r
405 }\r
406 }\r
407 },\r
408 \r
409 read: function(leftRecord, node, fromReader, readOptions) {\r
410 var rightRecords = this.callParent([leftRecord, node, fromReader, readOptions]),\r
411 rightRecord;\r
412\r
413 if (rightRecords) {\r
414 rightRecord = rightRecords[0];\r
415 if (rightRecord) {\r
416 leftRecord[this.getInstanceName()] = rightRecord;\r
417 delete leftRecord.data[this.role];\r
418 }\r
419 }\r
420 }\r
421 })\r
422});\r