Permalink
Browse files

Delay saving children until parents are ready

Relational databases store the foreign key on the
child. If the parent of a child is new, that
record must be created first before the child can
be saved, as the child needs to know the parent's
primary key.

The DS.RESTAdapter now observes new parents of
dependent children, delaying their committal until
the parent has been created.
  • Loading branch information...
1 parent 73ad70c commit e5f89defd0f3b7c6fce5884b9557723035b23316 Alex Kwiatkowski & Ian Lesperance committed with elliterate Oct 26, 2012
View
100 packages/ember-data/lib/adapters/rest_adapter.js
@@ -77,23 +77,25 @@ DS.RESTAdapter = DS.Adapter.extend({
},
createRecord: function(store, type, record) {
- var root = this.rootForType(type);
-
- var data = {};
- data[root] = this.serialize(record, { includeId: true });
-
- this.ajax(this.buildURL(root), "POST", {
- data: data,
- context: this,
- success: function(json) {
- Ember.run(this, function(){
- this.didCreateRecord(store, type, record, json);
- });
- },
- error: function(xhr) {
- this.didError(store, type, record, xhr);
- }
- });
+ waitForParents(record, function() {
+ var root = this.rootForType(type);
+
+ var data = {};
+ data[root] = this.serialize(record, { includeId: true });
+
+ this.ajax(this.buildURL(root), "POST", {
+ data: data,
+ context: this,
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didCreateRecord(store, type, record, json);
+ });
+ },
+ error: function(xhr) {
+ this.didError(store, type, record, xhr);
+ }
+ });
+ }, this);
},
dirtyRecordsForRecordChange: function(dirtySet, record) {
@@ -153,24 +155,26 @@ DS.RESTAdapter = DS.Adapter.extend({
},
updateRecord: function(store, type, record) {
- var id = get(record, 'id');
- var root = this.rootForType(type);
-
- var data = {};
- data[root] = this.serialize(record);
-
- this.ajax(this.buildURL(root, id), "PUT", {
- data: data,
- context: this,
- success: function(json) {
- Ember.run(this, function(){
- this.didSaveRecord(store, type, record, json);
- });
- },
- error: function(xhr) {
- this.didError(store, type, record, xhr);
- }
- });
+ waitForParents(record, function() {
+ var id = get(record, 'id');
+ var root = this.rootForType(type);
+
+ var data = {};
+ data[root] = this.serialize(record);
+
+ this.ajax(this.buildURL(root, id), "PUT", {
+ data: data,
+ context: this,
+ success: function(json) {
+ Ember.run(this, function(){
+ this.didSaveRecord(store, type, record, json);
+ });
+ },
+ error: function(xhr) {
+ this.didError(store, type, record, xhr);
+ }
+ });
+ }, this);
},
updateRecords: function(store, type, records) {
@@ -366,3 +370,29 @@ DS.RESTAdapter = DS.Adapter.extend({
}
});
+function waitForParents(record, callback, context) {
+ var observers = new Ember.Set();
+
+ record.eachRelationship(function(name, meta) {
+ var relationship = record.cacheFor(name);
+
+ if (meta.kind === 'belongsTo' && relationship && get(relationship, 'isNew')) {
+ var observer = function() {
+ relationship.removeObserver('id', context, observer);
+ observers.remove(name);
+ finish();
+ };
+
+ relationship.addObserver('id', context, observer);
+ observers.add(name);
+ }
+ });
+
+ finish();
+
+ function finish() {
+ if (observers.length === 0) {
+ callback.call(context);
+ }
+ }
+}
View
165 packages/ember-data/tests/integration/relationships/rest_adapter_test.js
@@ -0,0 +1,165 @@
+var get = Ember.get, set = Ember.set;
+
+var Person, Comment, store, requests;
+
+Person = DS.Model.extend();
+Comment = DS.Model.extend();
+
+Person.reopen({
+ name: DS.attr('string'),
+ comments: DS.hasMany(Comment)
+});
+Person.toString = function() { return "Person"; };
+
+Comment.reopen({
+ body: DS.attr('string'),
+ person: DS.belongsTo(Person)
+});
+Comment.toString = function() { return "Comment"; };
+
+module('Relationships with the REST adapter', {
+ setup: function() {
+ var Adapter, adapter;
+
+ requests = [];
+
+ Adapter = DS.RESTAdapter.extend();
+ Adapter.configure('plurals', {
+ person: 'people'
+ });
+
+ adapter = Adapter.create({
+ ajax: function(url, method, options) {
+ var success = options.success,
+ error = options.error;
+
+ options.url = url;
+ options.method = method;
+
+ if (success) {
+ options.success = function() {
+ success.apply(options.context, arguments);
+ };
+ }
+
+ if (error) {
+ options.error = function() {
+ error.apply(options.context, arguments);
+ };
+ }
+
+ requests.push(options);
+ }
+ });
+
+ store = DS.Store.create({
+ isDefaultStore: true,
+ adapter: adapter
+ });
+ }
+});
+
+function expectState(record, state, value) {
+ if (value === undefined) { value = true; }
+
+ var flag = "is" + state.charAt(0).toUpperCase() + state.substr(1);
+ equal(get(record, flag), value, "the " + record.constructor + " is " + (value === false ? "not " : "") + state);
+}
+
+function expectStates(records, state, value) {
+ records.forEach(function(record) {
+ expectState(record, state, value);
+ });
+}
+
+test("creating a parent and child in the same commit", function() {
+ var person, comment;
+
+ comment = store.createRecord(Comment);
+
+ person = store.createRecord(Person, { name: "John Doe" });
+ person.get('comments').pushObject(comment);
+
+ store.commit();
+
+ expectStates([person, comment], 'saving', true);
+
+ equal(requests.length, 1, "Only one request is attempted");
+ equal(requests[0].url, "/people", "The person is created first");
+
+ requests[0].success({
+ person: { id: 1, name: "John Doe", comments: [] },
+ comments: []
+ });
+
+ stop();
+ setTimeout(function() {
+ start();
+
+ expectState(person, 'saving', false);
+ expectState(comment, 'saving', true);
+ expectStates([person, comment], 'error', false);
+
+ equal(requests.length, 2, "A second request is attempted");
+ equal(requests[1].url, "/comments", "The comment is created second");
+ equal(requests[1].data.comment.person_id, 1, "The submitted comment attributes include the person_id");
+
+ requests[1].success({
+ comment: { id: 2, person_id: 1 }
+ });
+ });
+
+ stop();
+ setTimeout(function() {
+ start();
+
+ expectStates([person, comment], 'saving', false);
+ expectStates([person, comment], 'error', false);
+ });
+});
+
+test("creating a parent and updating a child in the same commit", function() {
+ var person, comment;
+
+ store.load(Comment, { id: 2 });
+ comment = store.find(Comment, 2);
+ comment.set('body', 'Lorem ipsum dolor sit amet.');
+
+ person = store.createRecord(Person, { name: "John Doe" });
+ person.get('comments').pushObject(comment);
+
+ store.commit();
+
+ expectStates([person, comment], 'saving', true);
+
+ equal(requests.length, 1, "Only one request is attempted");
+ equal(requests[0].url, "/people", "The person is created first");
+
+ requests[0].success({
+ person: { id: 1, name: "John Doe", comments: [] },
+ comments: []
+ });
+
+ stop();
+ setTimeout(function() {
+ start();
+
+ expectState(person, 'saving', false);
+ expectState(comment, 'saving', true);
+ expectStates([person, comment], 'error', false);
+
+ equal(requests.length, 2, "A second request is attempted");
+ equal(requests[1].url, "/comments/2", "The comment is updated second");
+ equal(requests[1].data.comment.person_id, 1, "The submitted comment attributes include the person_id");
+
+ requests[1].success();
+ });
+
+ stop();
+ setTimeout(function() {
+ start();
+
+ expectStates([person, comment], 'saving', false);
+ expectStates([person, comment], 'error', false);
+ });
+});
View
98 packages/ember-data/tests/unit/rest_adapter_test.js
@@ -1030,3 +1030,101 @@ test("updating a record with a 500 error marks the record as error", function()
expectState('error');
});
+
+test("delaying create when the parent is new", function() {
+ group = store.createRecord(Group, { name: "Engineers" });
+
+ person = store.createRecord(Person, { name: "John" });
+ person.set('group', group);
+
+ ok(!group.hasObserverFor('id'), "The group's ID is not yet being observed");
+
+ adapter.createRecord(store, Person, person);
+
+ ok(!ajaxHash, "The AJAX call has not yet been made");
+ ok(group.hasObserverFor('id'), "The group's ID is being observed");
+
+ var observer = group.observersForKey('id')[0],
+ target = observer[0],
+ callback = observer[1];
+
+ callback.call(target);
+
+ ok(ajaxHash, "The AJAX call has been made");
+ ok(!group.hasObserverFor('id'), "The group's ID is no longer being observed");
+});
+
+test("immediately creating when the parent is already loaded", function() {
+ store.load(Group, { id: 1, name: "Engineers" });
+ group = store.find(Group, 1);
+
+ person = store.createRecord(Person, { name: "John" });
+ person.set('group', group);
+
+ ok(!group.hasObserverFor('id'), "The group's ID is not being observed");
+
+ adapter.createRecord(store, Person, person);
+
+ ok(ajaxHash, "The AJAX call has been made");
+ ok(!group.hasObserverFor('id'), "The group's ID is not being observed");
+});
+
+test("immediately creating when there is no parent", function() {
+ person = store.createRecord(Person, { name: "John" });
+
+ adapter.createRecord(store, Person, person);
+
+ ok(ajaxHash, "The AJAX call has been made");
+});
+
+test("delaying update when the parent is new", function() {
+ group = store.createRecord(Group, { name: "Engineers" });
+
+ store.load(Person, { id: 1, name: "John" });
+ person = store.find(Person, 1);
+ person.set('name', 'Jonathan');
+ person.set('group', group);
+
+ ok(!group.hasObserverFor('id'), "The group's ID is not yet being observed");
+
+ adapter.updateRecord(store, Person, person);
+
+ ok(!ajaxHash, "The AJAX call has not yet been made");
+ ok(group.hasObserverFor('id'), "The group's ID is being observed");
+
+ var observer = group.observersForKey('id')[0],
+ target = observer[0],
+ callback = observer[1];
+
+ callback.call(target);
+
+ ok(ajaxHash, "The AJAX call has been made");
+ ok(!group.hasObserverFor('id'), "The group's ID is no longer being observed");
+});
+
+test("immediately updating when the parent is already loaded", function() {
+ store.load(Group, { id: 1, name: "Engineers" });
+ group = store.find(Group, 1);
+
+ store.load(Person, { id: 1, name: "John" });
+ person = store.find(Person, 1);
+ person.set('name', 'Jonathan');
+ person.set('group', group);
+
+ ok(!group.hasObserverFor('id'), "The group's ID is not being observed");
+
+ adapter.updateRecord(store, Person, person);
+
+ ok(ajaxHash, "The AJAX call has been made");
+ ok(!group.hasObserverFor('id'), "The group's ID is not being observed");
+});
+
+test("immediately updating when there is no parent", function() {
+ store.load(Person, { id: 1, name: "John" });
+ person = store.find(Person, 1);
+ person.set('name', 'Jonathan');
+
+ adapter.updateRecord(store, Person, person);
+
+ ok(ajaxHash, "The AJAX call has been made");
+});

0 comments on commit e5f89de

Please sign in to comment.