Skip to content

Commit

Permalink
Coalesce find requests, add support for preloading data
Browse files Browse the repository at this point in the history
This PR brings following improvements/changes:

1. Find calls from the same runloop will be coalesced into findMany
requests if the adapter has a findMany method
2. Adds a groupRecordsForFindMany hook on the adapter that allows you
to decide how to coalesce the record requests. This will enable fixing
of bugs such as #651, by segmenting on the number of records
3. Allows users to preset attributes and relationships on a model when
doing a find call. For relationships you can either pass in a record or
an id
4. Gives rudimentary support for nested urls, by passing in the record
object to the buildUrl method
5. Removes the owner being passed to findMany becuase it can be looked
up on the original record
6. Adds couple special cased SSOT features that were needed for
buildUrl/removal of owner property to be viable

This PR lays the groundwork for:
1. Adding support for reloading hasManys
2. Making nice sugar for nestedUrls in the RestAdater
  • Loading branch information
igorT committed Jul 24, 2014
1 parent 1eed136 commit d463ead
Show file tree
Hide file tree
Showing 14 changed files with 749 additions and 137 deletions.
51 changes: 40 additions & 11 deletions packages/ember-data/lib/adapters/rest_adapter.js
Expand Up @@ -205,10 +205,11 @@ export default Adapter.extend({
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {String} id
@param {DS.Model} record
@return {Promise} promise
*/
find: function(store, type, id) {
return this.ajax(this.buildURL(type.typeKey, id), 'GET');
find: function(store, type, id, record) {
return this.ajax(this.buildURL(type.typeKey, id, record), 'GET');
},

/**
Expand Down Expand Up @@ -288,10 +289,11 @@ export default Adapter.extend({
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {Array} ids
@param {Array} records
@return {Promise} promise
*/
findMany: function(store, type, ids) {
return this.ajax(this.buildURL(type.typeKey), 'GET', { data: { ids: ids } });
findMany: function(store, type, ids, records) {
return this.ajax(this.buildURL(type.typeKey, ids, records), 'GET', { data: { ids: ids } });
},

/**
Expand Down Expand Up @@ -391,7 +393,7 @@ export default Adapter.extend({

serializer.serializeIntoHash(data, type, record, { includeId: true });

return this.ajax(this.buildURL(type.typeKey), "POST", { data: data });
return this.ajax(this.buildURL(type.typeKey, null, record), "POST", { data: data });
},

/**
Expand All @@ -418,7 +420,7 @@ export default Adapter.extend({

var id = get(record, 'id');

return this.ajax(this.buildURL(type.typeKey, id), "PUT", { data: data });
return this.ajax(this.buildURL(type.typeKey, id, record), "PUT", { data: data });
},

/**
Expand All @@ -435,7 +437,7 @@ export default Adapter.extend({
deleteRecord: function(store, type, record) {
var id = get(record, 'id');

return this.ajax(this.buildURL(type.typeKey, id), "DELETE");
return this.ajax(this.buildURL(type.typeKey, id, record), "DELETE");
},

/**
Expand All @@ -451,12 +453,13 @@ export default Adapter.extend({
@method buildURL
@param {String} type
@param {String} id
@param {DS.Model} record
@return {String} url
*/
buildURL: function(type, id) {
var url = [];
var host = get(this, 'host');
var prefix = this.urlPrefix();
buildURL: function(type, id, record) {
var url = [],
host = get(this, 'host'),
prefix = this.urlPrefix();

if (type) { url.push(this.pathForType(type)); }
if (id) { url.push(id); }
Expand Down Expand Up @@ -504,6 +507,32 @@ export default Adapter.extend({
return url.join('/');
},

_stripIDFromURL: function(store, record) {
var type = store.modelFor(record);
var url = this.buildURL(type.typeKey, record.get('id'), record);

var expandedURL = url.split('/');
if (expandedURL[expandedURL.length -1 ] === record.get('id')){
expandedURL[expandedURL.length - 1] = "";
}
return expandedURL.join('/');
},

groupRecordsForFindMany: function (store, records) {
var groups = Ember.MapWithDefault.create({defaultValue: function(){return [];}});
var adapter = this;
forEach.call(records, function(record){
var baseUrl = adapter._stripIDFromURL(store, record);
groups.get(baseUrl).push(record);
});
var groupsArray = [];
groups.forEach(function(key, group){
groupsArray.push(group);
});

return groupsArray;
},

/**
Determines the pathname for a given type.
Expand Down
21 changes: 20 additions & 1 deletion packages/ember-data/lib/system/adapter.js
Expand Up @@ -445,14 +445,33 @@ var Adapter = Ember.Object.extend({
@param {DS.Store} store
@param {subclass of DS.Model} type the DS.Model class of the records
@param {Array} ids
@param {Array} records
@return {Promise} promise
*/
findMany: function(store, type, ids) {
findMany: function(store, type, ids, records) {
var promises = map.call(ids, function(id) {
return this.find(store, type, id);
}, this);

return Ember.RSVP.all(promises);
},

/**
Organize records into groups, each of which is to be passed to separate
calls to `findMany`.
For example, if your api has nested URLs that depend on the parent, you will
want to group records by their parent.
The default implementation returns the records as a single group.
@method groupRecordsForFindMany
@param {Array} records
@returns {Array} an array of arrays of records, each of which is to be
loaded separately by `findMany`.
*/
groupRecordsForFindMany: function (store, records) {
return [records];
}
});

Expand Down
47 changes: 47 additions & 0 deletions packages/ember-data/lib/system/model/model.js
Expand Up @@ -9,6 +9,7 @@ var get = Ember.get;
var set = Ember.set;
var merge = Ember.merge;
var Promise = Ember.RSVP.Promise;
var forEach = Ember.EnumerableUtils.forEach;

var JSONSerializer;
var retrieveFromCurrentState = Ember.computed('currentState', function(key, value) {
Expand Down Expand Up @@ -629,6 +630,52 @@ var Model = Ember.Object.extend(Ember.Evented, {
get(this, 'store').dataWasUpdated(this.constructor, this);
},

_preloadData: function(preload) {
var record = this;
//TODO(Igor) consider the polymorphic case
forEach(Ember.keys(preload), function(key) {
var preloadValue = get(preload, key);
var relationshipMeta = record.constructor.metaForProperty(key);
if (relationshipMeta.isRelationship) {
record._preloadRelationship(key, preloadValue);
} else {
get(record, '_data')[key] = preloadValue;
}
});
},

_preloadRelationship: function(key, preloadValue) {
var relationshipMeta = this.constructor.metaForProperty(key);
var type = relationshipMeta.type;
if (relationshipMeta.kind === 'hasMany'){
this._preloadHasMany(key, preloadValue, type);
} else {
this._preloadBelongsTo(key, preloadValue, type);
}
},

_preloadHasMany: function(key, preloadValue, type) {
Ember.assert("You need to pass in an array to set a hasMany property on a record", Ember.isArray(preloadValue));
var record = this;

forEach(preloadValue, function(recordToPush) {
recordToPush = record._convertStringOrNumberIntoRecord(recordToPush, type);
get(record, key).pushObject(recordToPush);
});
},

_preloadBelongsTo: function(key, preloadValue, type){
var recordToPush = this._convertStringOrNumberIntoRecord(preloadValue, type);
set(this, key, recordToPush);
},

_convertStringOrNumberIntoRecord: function(value, type) {
if (Ember.typeOf(value) === 'string' || Ember.typeOf(value) === 'number'){
return this.store.recordForId(type, value);
}
return value;
},

/**
Returns an object, whose keys are changed properties, and value is
an [oldProp, newProp] array.
Expand Down
6 changes: 6 additions & 0 deletions packages/ember-data/lib/system/model/states.js
Expand Up @@ -254,6 +254,9 @@ var DirtyState = {
// EVENTS
didSetProperty: didSetProperty,

//TODO(Igor) think this through
loadingData: Ember.K,

propertyWasReset: function(record, name) {
var stillDirty = false;

Expand Down Expand Up @@ -536,6 +539,9 @@ var RootState = {
// FLAGS
isLoaded: true,

//TODO(Igor) think this through
loadingData: Ember.K,

// SUBSTATES

// If there are no local changes to a record, it remains
Expand Down
4 changes: 2 additions & 2 deletions packages/ember-data/lib/system/record_arrays/many_array.js
Expand Up @@ -121,8 +121,8 @@ export default RecordArray.extend({
var store = get(this, 'store');
var owner = get(this, 'owner');

var unloadedRecords = records.filterBy('isEmpty', true);
store.fetchMany(unloadedRecords, owner);
var unloadedRecords = records.filterProperty('isEmpty', true);
store.scheduleFetchMany(unloadedRecords, owner);
},

// Overrides Ember.Array's replace method to implement
Expand Down
10 changes: 8 additions & 2 deletions packages/ember-data/lib/system/relationships/belongs_to.js
Expand Up @@ -35,7 +35,13 @@ function asyncBelongsTo(type, options, meta) {
var belongsTo = data[key];

if (!isNone(belongsTo)) {
promise = store.fetchRecord(belongsTo) || Promise.cast(belongsTo, promiseLabel);
var inverse = this.constructor.inverseFor(key);
//but for now only in the oneToOne case
if (inverse && inverse.kind === 'belongsTo'){
set(belongsTo, inverse.name, this);
}
//TODO(Igor) after OR doesn't seem that will be called
promise = store.findById(belongsTo.constructor, belongsTo.get('id')) || Promise.cast(belongsTo, promiseLabel);
return PromiseObject.create({
promise: promise
});
Expand Down Expand Up @@ -139,7 +145,7 @@ function belongsTo(type, options) {

if (isNone(belongsTo)) { return null; }

store.fetchRecord(belongsTo);
store.findById(belongsTo.constructor, belongsTo.get('id'));

return belongsTo;
}).meta(meta);
Expand Down
15 changes: 14 additions & 1 deletion packages/ember-data/lib/system/relationships/has_many.js
Expand Up @@ -12,6 +12,7 @@ import {
var get = Ember.get;
var set = Ember.set;
var setProperties = Ember.setProperties;
var map = Ember.EnumerableUtils.map;

function asyncHasMany(type, options, meta) {
return Ember.computed('data', function(key) {
Expand All @@ -28,7 +29,19 @@ function asyncHasMany(type, options, meta) {
if (link) {
rel = store.findHasMany(this, link, relationshipFromMeta(store, meta), resolver);
} else {
rel = store.findMany(this, data[key], typeForRelationshipMeta(store, meta), resolver);
//This is a temporary workaround for setting owner on the relationship
//until single source of truth lands. It only works for OneToMany atm
var records = data[key];
var inverse = this.constructor.inverseFor(key);
var owner = this;
if (inverse && records) {
if (inverse.kind === 'belongsTo'){
map(records, function(record){
set(record, inverse.name, owner);
});
}
}
rel = store.findMany(owner, data[key], typeForRelationshipMeta(store, meta), resolver);
}
// cache the promise so we can use it
// when we come back and don't need to rebuild
Expand Down

0 comments on commit d463ead

Please sign in to comment.