Skip to content
This repository

Fix issues with unregistering nested records. #863

Open
wants to merge 6 commits into from

3 participants

Greg Fairbanks Evin Grano Tyler Keating
Greg Fairbanks
Collaborator

After upgrading to 1.9, we started having issues with the datastore and nested records. I was able to trace this back to commits 0b5b2db and 62d599a. I've made some changes here that appear to improve the situation , but I haven't completely fixed the problem and haven't been having much luck finding the problem.

I made a gist (https://gist.github.com/4004203) with a small app that exhibits the problem.

I'd like to get some insight into what might be causing the remaining issues.

Greg Fairbanks fairbanksg Fix issues with unregistering nested records.
There were a couple of issues with unregistering nested records:

1) SC.ChildArray did not unregister nested records at all, so toMany
relations with nested records did not work properly.
2) When a child was unregistered from its parent, that did not propagate
to children of the child, leading to problems when nesting was several
levels deep.
3) There were two additional caches that were not being cleared when the
nested record was unregistered.
5821de2
Evin Grano
Owner
fairbanksg added some commits
Greg Fairbanks fairbanksg Fix calculations for (un)registering ChildArray records.
The previous commit incorrectly calculated which records to unregister
and register. All items after the passed in index need to be unregistered,
then all the new items should be registered, followed by the existing items
that were previously unregistered.
8b108d1
Greg Fairbanks fairbanksg Fix drag and drop handling for nested records.
If the item that is dragged is a nested record, special handling is needed
because the data hash will be removed when the record is unregistered. We
need to read the data hash so it doesn't get lost.
9c3c524
Tyler Keating

Hi Greg, I sort of see what is happening. Your example code (thanks it helps a lot!) is creating App.BottomLevel and App.MidLevel as SC.Records and then adding them to the nested record relationships. I haven't figured it out entirely, but since nested records are really just convenience wrappers around Objects, I think you should be creating them as such.

var midlevel = { bProperty: 'BBB' };
var bottomlevel = { cProperty: 'CCC' };
midlevel.bottomLevel = bottomlevel;

Instead of:

var midlevel = App.store.createRecord(App.MidLevel, { bProperty: 'BBB' });
var bottomlevel = App.store.createRecord(App.BottomLevel, { cProperty: 'CCC' });
midlevel.set('bottomLevel', bottomlevel);

Doing it that way solves one problem that I found, which is that attempting to get the nested record property ends up creating a new Record with a default internal id, because the previously assigned manually created record doesn't have an id. You can see this using the gist code, because there are 4 MidLevel records in the store on launch instead of two. Two of which don't have ids (manually created) and two that do (automatically created and used).

But it'll take more time to figure out why removing a midlevel object doesn't remove it from the store, which is why adding a new midlevel object doesn't seem to work correctly (it generates an internal id, finds that a record with a matching id is in the store and uses it).

I'll keep looking at it when I can. I'm also unsure of what the proper CRUD for nested records really should be. Actually, now that I think of it, the way you are creating them should work...

Tyler Keating

Oops.. I wrote the above thinking I was replying on an issue and forgot about your proposed changes. I will look at how that solves the problem.

Tyler Keating

As you've probably seen, it looks like a refactor of nested records which should alleviate all these weird behaviours is in the works. While that is in progress, I thought I would pull in your commits in this branch (except maybe the CollectionView patch), but when I ran the full suite of datastore unit tests, there were a number that failed. I didn't look at them, but are you able to figure out what is happening? I can't bring this in unless all the tests (save 1 that I know is failing currently)

About the CollectionView patch, it's so special case and I'm really worried about it getting forgotten inside the view. I wonder if we can't manage without it or separate it out more so that those who need it can find it.

Thanks.

fairbanksg added some commits
Greg Fairbanks fairbanksg Further changes to unregister child records when a record is unloaded. 85fe1fd
Greg Fairbanks fairbanksg Propagate status changes to child records.
Since nested records are now being properly unregistered, the status of
nested records was getting stale, leading to exceptions because the status
was still set to SC.Record.EMPTY.
c5b4462
Greg Fairbanks fairbanksg Modified unregisterChildFromParent so it can safely be called on any
storeKey, even if it is not a child record.
020a5f8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 6 unique commits by 1 author.

Oct 30, 2012
Greg Fairbanks fairbanksg Fix issues with unregistering nested records.
There were a couple of issues with unregistering nested records:

1) SC.ChildArray did not unregister nested records at all, so toMany
relations with nested records did not work properly.
2) When a child was unregistered from its parent, that did not propagate
to children of the child, leading to problems when nesting was several
levels deep.
3) There were two additional caches that were not being cleared when the
nested record was unregistered.
5821de2
Nov 19, 2012
Greg Fairbanks fairbanksg Fix calculations for (un)registering ChildArray records.
The previous commit incorrectly calculated which records to unregister
and register. All items after the passed in index need to be unregistered,
then all the new items should be registered, followed by the existing items
that were previously unregistered.
8b108d1
Greg Fairbanks fairbanksg Fix drag and drop handling for nested records.
If the item that is dragged is a nested record, special handling is needed
because the data hash will be removed when the record is unregistered. We
need to read the data hash so it doesn't get lost.
9c3c524
Dec 20, 2012
Greg Fairbanks fairbanksg Further changes to unregister child records when a record is unloaded. 85fe1fd
Jan 07, 2013
Greg Fairbanks fairbanksg Propagate status changes to child records.
Since nested records are now being properly unregistered, the status of
nested records was getting stale, leading to exceptions because the status
was still set to SC.Record.EMPTY.
c5b4462
Jan 09, 2013
Greg Fairbanks fairbanksg Modified unregisterChildFromParent so it can safely be called on any
storeKey, even if it is not a child record.
020a5f8
This page is out of date. Refresh to see the latest.
2  frameworks/datastore/models/record.js
@@ -965,7 +965,7 @@ SC.Record = SC.Object.extend(
965 965 // that we don't keep making new storeKeys for the same child record each
966 966 // time that it is reloaded.
967 967 id = hash[recordType.prototype.primaryKey];
968   - if (!id) this.generateIdForChild(cr);
  968 + if (!id) { id = this.generateIdForChild(cr); }
969 969 if (!id) { id = psk + '.' + path; }
970 970
971 971 // If there is an id, there may also be a storeKey. If so, update the
44 frameworks/datastore/system/child_array.js
@@ -167,9 +167,22 @@ SC.ChildArray = SC.Object.extend(SC.Enumerable, SC.Array,
167 167 record = this.get('record'), newRecs,
168 168
169 169 pname = this.get('propertyName'),
170   - cr, recordType;
171   -
  170 + cr, recordType, i;
  171 +
172 172 newRecs = this._processRecordsToHashes(recs);
  173 +
  174 + for (i = idx; i < children.length; ++i) {
  175 + this.unregisterNestedRecord(i);
  176 + }
  177 +
  178 + for (i = 0; i < len; ++i) {
  179 + record.registerNestedRecord(newRecs[i], pname, pname + '.' + (idx + i));
  180 + }
  181 +
  182 + for (i = 0; i < children.length - idx - amt; ++i) {
  183 + record.registerNestedRecord(children[idx + amt + i], pname, pname + '.' + (idx + len + i));
  184 + }
  185 +
173 186 // notify that the record did change...
174 187 if (newRecs !== this._prevChildren){
175 188 this._performRecordPropertyChange(null, false);
@@ -202,6 +215,33 @@ SC.ChildArray = SC.Object.extend(SC.Enumerable, SC.Array,
202 215 },
203 216
204 217 /**
  218 + Unregisters a child record from its parent record.
  219 +
  220 + Since accessing a child (nested) record creates a new data hash for the
  221 + child and caches the child record and its relationship to the parent record,
  222 + it's important to clear those caches when the child record is overwritten
  223 + or removed. This function tells the store to remove the child record from
  224 + the store's various child record caches.
  225 +
  226 + You should not need to call this function directly. Simply setting the
  227 + child record property on the parent to a different value will cause the
  228 + previous child record to be unregistered.
  229 +
  230 + @param {Number} idx The index of the child record.
  231 + */
  232 + unregisterNestedRecord: function(idx) {
  233 + var childArray, childRecord, csk, store,
  234 + record = this.get('record'),
  235 + pname = this.get('propertyName');
  236 +
  237 + store = record.get('store');
  238 + childArray = record.getPath(pname);
  239 + childRecord = childArray.objectAt(idx);
  240 + csk = childRecord.get('storeKey');
  241 + store.unregisterChildFromParent(csk);
  242 + },
  243 +
  244 + /**
205 245 Calls normalize on each object in the array
206 246 */
207 247 normalize: function(){
50 frameworks/datastore/system/store.js
@@ -467,9 +467,15 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
467 467 @returns {SC.Store} receiver
468 468 */
469 469 writeStatus: function(storeKey, newStatus) {
  470 + var that = this,
  471 + ret;
470 472 // use writeDataHash for now to handle optimistic lock. maximize code
471 473 // reuse.
472   - return this.writeDataHash(storeKey, null, newStatus);
  474 + ret = this.writeDataHash(storeKey, null, newStatus);
  475 + this._propagateToChildren(storeKey, function(storeKey) {
  476 + that.writeStatus(storeKey, newStatus);
  477 + });
  478 + return ret;
473 479 },
474 480
475 481 /**
@@ -673,6 +679,10 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
673 679 }
674 680 }
675 681
  682 + this.records = {};
  683 + this.childRecords = {};
  684 + this.parentRecords = {};
  685 +
676 686 this.set('hasChanges', NO);
677 687 },
678 688
@@ -1264,6 +1274,8 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
1264 1274 that.unloadRecord(null, null, storeKey, newStatus);
1265 1275 });
1266 1276
  1277 + this.unregisterChildFromParent(storeKey);
  1278 +
1267 1279 return this ;
1268 1280 },
1269 1281
@@ -1446,20 +1458,27 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
1446 1458 /**
1447 1459 Unregister the Child Record from its Parent. This will cause the Child
1448 1460 Record to be removed from the store.
  1461 +
  1462 + @param {Number} childStoreKey storeKey to unregister
1449 1463 */
1450 1464 unregisterChildFromParent: function(childStoreKey) {
1451   - var crs, oldPk;
  1465 + var crs = this.childRecords,
  1466 + prs = this.parentRecords,
  1467 + recs = this.records,
  1468 + that = this,
  1469 + oldPk;
1452 1470
1453 1471 // Check the child to see if it has a parent
1454   - crs = this.childRecords;
1455   -
1456   - // Remove the parent's connection to the child. This doesn't remove the
1457   - // parent store key from the cache of parent store keys if the parent
1458   - // no longer has any other registered children, because the amount of effort
1459   - // to determine that would not be worth the miniscule memory savings.
1460   - oldPk = crs[childStoreKey];
1461   - if (oldPk) {
1462   - delete this.parentRecords[oldPk][childStoreKey];
  1472 + if (crs) {
  1473 + // Remove the parent's connection to the child. This doesn't remove the
  1474 + // parent store key from the cache of parent store keys if the parent
  1475 + // no longer has any other registered children, because the amount of effort
  1476 + // to determine that would not be worth the miniscule memory savings.
  1477 + oldPk = crs[childStoreKey];
  1478 + if (oldPk && prs) {
  1479 + delete prs[oldPk][childStoreKey];
  1480 + }
  1481 + delete crs[childStoreKey];
1463 1482 }
1464 1483
1465 1484 // Remove the child.
@@ -1467,8 +1486,13 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
1467 1486 // 2. from the cache of record objects
1468 1487 // 3. from the cache of child record store keys
1469 1488 this.removeDataHash(childStoreKey);
1470   - delete this.records[childStoreKey];
1471   - delete crs[childStoreKey];
  1489 + if (recs) {
  1490 + delete recs[childStoreKey];
  1491 + }
  1492 +
  1493 + this._propagateToChildren(childStoreKey, function(storeKey) {
  1494 + that.unregisterChildFromParent(storeKey);
  1495 + });
1472 1496 },
1473 1497
1474 1498 /**
22 frameworks/datastore/tests/models/nested_records/nested_record.js
@@ -287,6 +287,7 @@ test("Basic Write As a Hash when Child Record has no primary key", function() {
287 287
288 288 // Test Child Record creation
289 289 var oldCR = testParent3.get('info');
  290 + var oldKey = oldCR.get('id');
290 291 testParent3.set('info', {
291 292 type: 'ChildRecordTest',
292 293 name: 'New Child Name',
@@ -302,8 +303,7 @@ test("Basic Write As a Hash when Child Record has no primary key", function() {
302 303 var storeRef = store.find(NestedRecord.ChildRecordTest, key);
303 304 ok(storeRef, 'after a set() with an object, checking that the store has the instance of the child record with proper primary key');
304 305 equals(cr, storeRef, "after a set with an object, checking the parent reference is the same as the direct store reference");
305   - var oldKey = oldCR.get('id');
306   - ok((oldKey === key), 'check to see that the old child record has the same key as the new child record');
  306 + equals(oldKey, key, 'check to see that the old child record has the same key as the new child record');
307 307
308 308 // Check for changes on the child bubble to the parent.
309 309 cr.set('name', 'Child Name Change');
@@ -398,11 +398,14 @@ test("Basic Write As a Child Record when Child Record has no primary key", funct
398 398
399 399 test("Writing over a child record should remove caches in the store.", function() {
400 400 // Test Child Record creation
401   - var cr, key, store = testParent.get('store'), cacheLength;
  401 + var cr, key, store = testParent.get('store'), cacheLength, idx, storeKeys = [], ids = [], sks;
402 402
403 403 // Get the child record once before setting it in order to test that this child
404 404 // doesn't become abandoned in the store.
405 405 cr = testParent.get('info');
  406 + storeKeys.push(cr.get('storeKey'));
  407 + ids.push(cr.get('id'));
  408 + ids = ids.uniq();
406 409
407 410 // Once we get the child record, certain caches are created in the store.
408 411 // Verify the cache lengths to prove that there are no leaked objects.
@@ -425,6 +428,9 @@ test("Writing over a child record should remove caches in the store.", function(
425 428 // Overwrite the child record with a new child record with the same guid.
426 429 testParent.set('info', {type: 'ChildRecordTest', name: 'New Child Name', value: 'Red Goo', guid: '5001'});
427 430 cr = testParent.get('info');
  431 + storeKeys.push(cr.get('storeKey'));
  432 + ids.push(cr.get('id'));
  433 + ids = ids.uniq();
428 434
429 435 // Verify the cache lengths to prove that there are no leaked objects.
430 436 cacheLength = 0;
@@ -446,6 +452,9 @@ test("Writing over a child record should remove caches in the store.", function(
446 452 // Overwrite the child record with a new child record with the same guid.
447 453 testParent.set('info', store.createRecord(NestedRecord.ChildRecordTest, {type: 'ChildRecordTest', name: 'New Child Name', value: 'Orange Goo', guid: '6001'}));
448 454 cr = testParent.get('info');
  455 + storeKeys.push(cr.get('storeKey'));
  456 + ids.push(cr.get('id'));
  457 + ids = ids.uniq();
449 458
450 459 // Verify the cache lengths to prove that there are no leaked objects.
451 460 cacheLength = 0;
@@ -608,11 +617,11 @@ test("Reloading the parent record uses same child record.", function() {
608 617
609 618 cacheLength = 0;
610 619 for (key in store.childRecords) { cacheLength += 1; }
611   - equals(cacheLength, 1, 'there should only be one child record registered in the store');
  620 + equals(cacheLength, 0, 'there should be zero child records registered in the store');
612 621
613 622 cacheLength = 0;
614 623 for (key in store.records) { cacheLength += 1; }
615   - equals(cacheLength, 4, 'there should be four records cached in the store');
  624 + equals(cacheLength, 2, 'there should be two records cached in the store');
616 625
617 626 cacheLength = 0;
618 627 for (key in store.dataHashes) { if (store.dataHashes[key] !== null) cacheLength += 1; }
@@ -620,7 +629,7 @@ test("Reloading the parent record uses same child record.", function() {
620 629
621 630 // Reload the record
622 631 SC.RunLoop.begin();
623   - store.loadRecord(NestedRecord.ParentRecordTest, {
  632 + parentStoreKey = store.loadRecord(NestedRecord.ParentRecordTest, {
624 633 name: 'Parent Name 3',
625 634 info: {
626 635 type: 'ChildRecordTest',
@@ -631,6 +640,7 @@ test("Reloading the parent record uses same child record.", function() {
631 640 parentId);
632 641 SC.RunLoop.end();
633 642
  643 + testParent3 = store.materializeRecord(parentStoreKey);
634 644 child = testParent3.get('info');
635 645 equals(testParent3.get('status'), SC.Record.READY_CLEAN, 'parent status should be READY_CLEAN');
636 646 equals(child.get('status'), SC.Record.READY_CLEAN, 'child status should be READY_CLEAN');
2  frameworks/datastore/tests/models/nested_records/nested_record_complex.js
@@ -246,6 +246,7 @@ function() {
246 246
247 247 // Test Child Record creation
248 248 oldP = testParent.get('person');
  249 + oldKey = oldP.get('id');
249 250 testParent.set('person', {
250 251 type: 'Person',
251 252 name: 'Al Gore',
@@ -266,7 +267,6 @@ function() {
266 267 storeRef = store.find(NestedRecord.Person, key);
267 268 ok(storeRef, 'after a set() with an object, checking that the store has the instance of the child record with proper primary key');
268 269 equals(p, storeRef, "after a set with an object, checking the parent reference is the same as the direct store reference");
269   - oldKey = oldP.get('id');
270 270 ok((oldKey === key), 'check to see that the old child record has the same key as the new child record');
271 271
272 272 // Check for changes on the child bubble to the parent.
15 frameworks/desktop/views/collection.js
@@ -2994,7 +2994,20 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate, SC.CollectionConte
2994 2994 objects = [];
2995 2995 shift = 0;
2996 2996 data.indexes.forEach(function(i) {
2997   - objects.push(content.objectAt(i-shift));
  2997 + var o = content.objectAt(i-shift),
  2998 + store, sk;
  2999 + if (SC.get(o, 'isNestedRecord')) {
  3000 + // special case here. removing a nested record from content will
  3001 + // unregister the record from the parent, which removes the data hash.
  3002 + // trying to reinsert that record will fail since the data hash is gone.
  3003 + // to avoid this, read the data hash before removing and keep that around
  3004 + // to reinsert.
  3005 + store = o.get('store');
  3006 + sk = o.get('storeKey');
  3007 + objects.push(store.readDataHash(sk));
  3008 + } else {
  3009 + objects.push(o);
  3010 + }
2998 3011 content.removeAt(i-shift);
2999 3012 shift++;
3000 3013 if (i < idx) idx--;

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.