Skip to content

Commit

Permalink
Reify relationships lazily
Browse files Browse the repository at this point in the history
The goal here is to do as little work as we can with relationship payloads until
the relationship is actually accessed.  In an extreme case, where many
relationship payloads are pushed, but no relationships are accessed, all we want
to do is stash the payload.
  • Loading branch information
hjdivad committed Mar 20, 2017
1 parent f0453ea commit a38f408
Show file tree
Hide file tree
Showing 16 changed files with 1,683 additions and 38 deletions.
6 changes: 5 additions & 1 deletion addon/-private/system/model/internal-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ export default class InternalModel {
destroy() {
assert("Cannot destroy an internalModel while its record is materialized", !this.record || this.record.get('isDestroyed') || this.record.get('isDestroying'));

this.store._removeFromIdMap(this);
this.store._internalModelDestroyed(this);
this._isDestroyed = true;
}

Expand All @@ -531,6 +531,8 @@ export default class InternalModel {

setupData(data) {
heimdall.increment(setupData);
this.store._internalModelDidReceiveRelationshipData(this.modelName, this.id, data.relationships);

let changedKeys;

if (this.hasRecord) {
Expand Down Expand Up @@ -1013,6 +1015,8 @@ export default class InternalModel {
*/
adapterDidCommit(data) {
if (data) {
this.store._internalModelDidReceiveRelationshipData(this.modelName, this.id, data.relationships);

data = data.attributes;
}

Expand Down
20 changes: 16 additions & 4 deletions addon/-private/system/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1315,12 +1315,24 @@ Model.reopenClass({
*/
inverseFor(name, store) {
let inverseMap = get(this, 'inverseMap');
if (inverseMap[name]) {
if (inverseMap[name] !== undefined) {
return inverseMap[name];
} else {
let inverse = this._findInverseFor(name, store);
inverseMap[name] = inverse;
return inverse;
let relationship = get(this, 'relationshipsByName').get(name);
if (!relationship) {
inverseMap[name] = null;
return null;
}

let options = relationship.options;
if (options && options.inverse === null) {
// populate the cache with a miss entry so we can skip getting and going
// through `relationshipsByName`
inverseMap[name] = null;
return null;
}

return inverseMap[name] = this._findInverseFor(name, store);
}
},

Expand Down
228 changes: 228 additions & 0 deletions addon/-private/system/relationships/relationship-payloads-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import Ember from 'ember';
import RelationshipPayloads from './relationship-payloads';

const get = Ember.get;

/**
Manages relationship payloads for a given store, for uninitialized
relationships. Acts as a single source of truth (of payloads) for both sides
of an uninitialized relationship so they can agree on the most up-to-date
payload received without needing too much eager processing when those payloads
are pushed into the store.
This minimizes the work spent on relationships that are never initialized.
Once relationships are initialized, their state is managed in a relationship
state object (eg BelongsToRelationship or ManyRelationship).
@example
let relationshipPayloadsManager = new RelationshipPayloadsManager(store);
const User = DS.Model.extend({
hobbies: DS.hasMany('hobby')
});
const Hobby = DS.Model.extend({
user: DS.belongsTo('user')
});
let userPayload = {
data: {
id: 1,
type: 'user',
relationships: {
hobbies: {
data: [{
id: 2,
type: 'hobby'
}]
}
}
},
};
relationshipPayloadsManager.push('user', 1, userPayload.data.relationships);
relationshipPayloadsManager.get('hobby', 2, 'user') === {
{
data: {
id: 1,
type: 'user'
}
}
}
@private
@class RelationshipPayloadsManager
*/
export default class RelationshipPayloadsManager {
constructor(store) {
this._store = store;
// cache of `RelationshipPayload`s
this._cache = Object.create(null);
}

/**
Find the payload for the given relationship of the given model.
Returns the payload for the given relationship, whether raw or computed from
the payload of the inverse relationship.
@example
relationshipPayloadsManager.get('hobby', 2, 'user') === {
{
data: {
id: 1,
type: 'user'
}
}
}
@method
*/
get(modelName, id, relationshipName) {
let modelClass = this._store._modelFor(modelName);
let relationshipsByName = get(modelClass, 'relationshipsByName');
let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, false);
return relationshipPayloads && relationshipPayloads.get(modelName, id, relationshipName);
}

/**
Push a model's relationships payload into this cache.
@example
let userPayload = {
data: {
id: 1,
type: 'user',
relationships: {
hobbies: {
data: [{
id: 2,
type: 'hobby'
}]
}
}
},
};
relationshipPayloadsManager.push('user', 1, userPayload.data.relationships);
@method
*/
push(modelName, id, relationshipsData) {
if (!relationshipsData) { return; }

let modelClass = this._store._modelFor(modelName);
let relationshipsByName = get(modelClass, 'relationshipsByName');
Object.keys(relationshipsData).forEach(key => {
let relationshipPayloads = this._getRelationshipPayloads(modelName, key, modelClass, relationshipsByName, true);
if (relationshipPayloads) {
relationshipPayloads.push(modelName, id, key, relationshipsData[key]);
}
});
}

/**
Unload a model's relationships payload.
@method
*/
unload(modelName, id) {
let modelClass = this._store._modelFor(modelName);
let relationshipsByName = get(modelClass, 'relationshipsByName');
relationshipsByName.forEach((_, relationshipName) => {
let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, false);
if (relationshipPayloads) {
relationshipPayloads.unload(modelName, id, relationshipName);
}
});
}

/**
Find the RelationshipPayloads object for the given relationship. The same
RelationshipPayloads object is returned for either side of a relationship.
@example
const User = DS.Model.extend({
hobbies: DS.hasMany('hobby')
});
const Hobby = DS.Model.extend({
user: DS.belongsTo('user')
});
relationshipPayloads.get('user', 'hobbies') === relationshipPayloads.get('hobby', 'user');
The signature has a somewhat large arity to avoid extra work, such as
a) string maipulation & allocation with `modelName` and
`relationshipName`
b) repeatedly getting `relationshipsByName` via `Ember.get`
@private
@method
*/
_getRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName, init) {
if (!relationshipsByName.has(relationshipName)) { return; }

let key = `${modelName}:${relationshipName}`;
if (!this._cache[key] && init) {
return this._initializeRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName);
}

return this._cache[key];
}

/**
Create the `RelationshipsPayload` for the relationship `modelName`, `relationshipName`, and its inverse.
@private
@method
*/
_initializeRelationshipPayloads(modelName, relationshipName, modelClass, relationshipsByName) {
let relationshipMeta = relationshipsByName.get(relationshipName);
let inverseMeta = modelClass.inverseFor(relationshipName, this._store);

let inverseModelName;
let inverseRelationshipName;
let inverseRelationshipMeta;

// figure out the inverse relationship; we need two things
// a) the inverse model name
//- b) the name of the inverse relationship
if (inverseMeta) {
inverseRelationshipName = inverseMeta.name
inverseModelName = relationshipMeta.type;
inverseRelationshipMeta = get(inverseMeta.type, 'relationshipsByName').get(inverseRelationshipName);
} else {
// relationship has no inverse
inverseModelName = inverseRelationshipName = '';
inverseRelationshipMeta = null;
}

let lhsKey = `${modelName}:${relationshipName}`;
let rhsKey = `${inverseModelName}:${inverseRelationshipName}`;

// populate the cache for both sides of the relationship, as they both use
// the same `RelationshipPayloads`.
//
// This works out better than creating a single common key, because to
// compute that key we would need to do work to look up the inverse
//
return this._cache[lhsKey] =
this._cache[rhsKey] =
new RelationshipPayloads(
this._store,
modelName,
relationshipName,
relationshipMeta,
inverseModelName,
inverseRelationshipName,
inverseRelationshipMeta
);
}
}
Loading

0 comments on commit a38f408

Please sign in to comment.