Skip to content

Commit

Permalink
Delay saving children until parents are ready
Browse files Browse the repository at this point in the history
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
Alex Kwiatkowski & Ian Lesperance authored and elliterate committed Mar 25, 2013
1 parent 73ad70c commit e5f89de
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 35 deletions.
100 changes: 65 additions & 35 deletions packages/ember-data/lib/adapters/rest_adapter.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
}
@@ -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);
});
});
98 changes: 98 additions & 0 deletions packages/ember-data/tests/unit/rest_adapter_test.js
Expand Up @@ -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.