Some mixins for managing model relationships.
belongsTo('this-model-name', 'to-model-name')
hasMany('this-model-name', 'has-model-name')
hasManyThrough('this-model-name', 'has-model-name', 'through-model-name')
The mixins are returned by factory functions that take standard arguments to create methods and properties based on the model names provided.
Using the mixins you can easily create models with a standard API for managing relationships.
Create a model for a post
that has comment
and tag
relationships. The tag
relationship is managed through a post-tag
model.
import DS from 'ember-data';
import { hasMany, hasManyThrough } from 'relationship/mixins';
// Post
export default DS.Model.extend(
hasMany('post', 'comment'),
hasMany('post', 'post-tag'),
hasManyThrough('post', 'tag', 'post-tag'),
{
title: DS.attr('string'),
body: DS.attr('string'),
}
);
A comment
belongs to a post
.
import DS from 'ember-data';
import { belongsTo } from 'relationship/mixins';
// Comment
export default DS.Model.extend(
belongsTo('comment', 'post'),
{
body: DS.attr('string'),
}
);
The post-tag
model links a post
to a tag
.
import DS from 'ember-data';
import { belongsTo } from 'relationship/mixins';
// PostTag
export default DS.Model.extend(
belongsTo('post-tag', 'post'),
belongsTo('post-tag', 'tag'),
);
The tag
can have many post-tag
relationships. It's not usually necessary to make a post
directly available from the tag
but this could easily be done with hasManyThrough('tag', 'post', 'post-tag')
.
import DS from 'ember-data';
import { hasMany } from 'relationship/mixins';
// Tag
export default DS.Model.extend(
hasMany('tag', 'post-tag'),
{
name: DS.attr('string'),
}
);
Using belongsTo
. Not super useful.
// fetch a comment and post from the store
const comment = this.store.find('comment', 1);
const post = this.store.find('post'), 1;
// standard
comment.get('post');
// add or remove
comment.addPost(post);
comment.removePost(post);
comment.removePost();
// status
comment.get('isDirty');
comment.get('hasRemovedPost');
comment.get('_removedPost');
// save
comment.save();
// --- better example
// add a post
comment.addPost(post);
// access the post
comment.get('post'); // post
// remove the post
comment.removePost(post);
comment.get('post'); // null
// or remove whichever post
comment.addPost(post);
comment.removePost();
comment.get('post'); // null
// status
comment.get('isDirty'); // true
comment.get('hasRemovedPost'); // true
comment.get('_removedPost'); // post
// save
comment.save();
// ... after save completes
comment.get('isDirty'); // false
comment.get('hasRemovedPost'); // false
comment.get('_removedPost'); // false
Using hasMany
or hasManyThrough
relationships is nearly identical. Simplifies working with relationships. Especially useful for sanely managing through
relationships.
// fetch a post from the store
const post = this.store.find('post', 1);
// fetching a comment, postTag and tag
const comment = this.store.find('comment', 1);
const postTag = this.store.find('post-tag', 1);
const tag = this.store.find('tag', 1);
// standard lists
post.get('comments');
post.get('postTags');
post.get('tags');
// add, create, remove comment
post.addComment(comment);
post.createComment({body: 'hello.'});
post.removeComment(comment);
post.deleteComment(comment);
post.destroyComment(comment);
// add, create, remove postTag
post.addPostTag(postTag);
post.createPostTag({tag: tag});
post.removePostTag(postTag);
post.deletePostTag(postTag);
post.destroyPostTag(postTag);
// add, create, remove tag
post.addTag(tag);
post.createTag({name: 'hello'});
post.removeTag(tag);
post.deleteTag(tag);
post.destroyTag(tag);
// post status
post.get('hasDirtyAttributes');
// comment status
post.get('hasDirtyComments');
post.get('hasRemovedComments');
// postTag status
post.get('hasDirtyPostTags');
post.get('hasRemovedPostTags');
// tag status
post.get('hasDirtyTags');
post.get('hasRemovedTags');
// special lists
post.get('_removedComments');
post.get('_removedPostTags');
post.get('_removedTags');
// saving
post.saveComments();
post.savePostTags();
post.saveTags();
post.save();
addItem(item)
removeItem(item)
removeItem()
save()
item
hasRemovedItem
_removedItem
addItem(item)
createItem(props)
removeItem(item)
deleteItem(item)
destroyItem(item)
saveItems()
saveItems(false)
note: hasManyThrough only.save()
save(false)
Maybe?
removeItems(items)
deleteItems(items)
destroyItems(items)
clearItems()
items
hasDirtyItems
hasRemovedItems
_removedItems
Adds a relationship to the item from the parent. When defining a relationship, the addItem
method will be renamed to match the model relationship being defined. For instance, using the mixin hasMany('post', 'comment')
on the post
model will make addComment(comment)
available.
Presume the comment
model is using the belongsTo('comment', 'post')
mixin.
// fetch records from the store
const post = this.store.find('post', 1);
const comment = this.store.find('comment', 1);
// has no post
comment.get('post'); // null
// add a post
comment.addPost(post);
// status
comment.get('post'); // post
comment.get('isDirty'); // true
// save
comment.save();
// ... after save completes
comment.get('isDirty'); // false
Presume the post
model is using the hasMany('post', 'comment')
mixin.
// fetch records from the store
const post = this.store.find('post', 1);
const comment = this.store.find('comment', 1);
// has no comments
post.get('comments'); // []
// add a comment to the post
post.addComment(comment);
// status
post.get('hasDirtyComments'); // true
post.get('comments'); // [comment]
post.get('comments.firstObject.isDirty'); // true
// save
post.saveComments();
// ... after save completes
post.get('hasDirtyComments'); // false
post.get('comments.firstObject.isDirty'); // false
Presume the post
model is using the hasManyThrough('post', 'tag', 'post-tag')
mixin along with the hasMany('post', 'post-tag')
mixin.
// fetch records from the store
const post = this.store.find('post', 1);
const tag = this.store.find('tag', 1);
// has no postTags or tags
post.get('postTags'); // []
post.get('tags'); // []
// add a tag to the post
post.addTag(tag);
// tag isn't dirty because it hasn't changed
post.get('hasDirtyTags'); // false
post.get('tags'); // [tag]
post.get('tags.firstObject.isDirty'); // false
// a postTag record was auto-created
post.get('hasDirtyPostTags'); // true
post.get('postTags'); // [postTag]
post.get('postTags.firstObject.isDirty'); // true
post.get('postTags.firstObject.isNew'); // true
// save
post.saveTags();
// ... after save completes
post.get('hasDirtyPostTags'); // false
post.get('postTags.firstObject.isDirty'); // false
post.get('postTags.firstObject.isNew'); // false
Creates a new records and relates it to the parent model. When defining a relationship, the createItem
method will be renamed to match the model relationship being defined. For instance, using the mixin hasMany('post', 'comment')
on the post
model will make createComment(props)
available.
Presume the post
model is using the hasMany('post', 'comment')
mixin.
// fetch records from the store
const post = this.store.find('post', 1);
// has no comments
post.get('comments'); // []
// create a comment
post.createComment({body: 'hello'});
// status
post.get('hasDirtyComments'); // true
post.get('comments'); // [comment]
post.get('comments.firstObject.isDirty'); // true
post.get('comments.firstObject.isNew'); // true
// comment is associated with post
const comment = post.get('comments.firstObject');
comment.get('post.id'); // 1
// save
post.saveComments();
// ... after save completes
post.get('hasDirtyComments'); // false
post.get('comments.firstObject.isDirty'); // false
post.get('comments.firstObject.isNew'); // false
Presume the post
model is using the hasManyThrough('post', 'tag', 'post-tag')
mixin along with the hasMany('post', 'post-tag')
mixin.
// fetch records from the store
const post = this.store.find('post', 1);
// has no tags
post.get('tags'); // []
// create a tag
post.createTag({name: 'hello'});
// status of tag
post.get('hasDirtyTags'); // true
post.get('tags'); // [tag]
post.get('tags.firstObject.isDirty'); // true
post.get('tags.firstObject.isNew'); // true
// status of postTag
post.get('hasDirtyPostTags'); // true
post.get('postTags'); // [postTag]
post.get('postTags.firstObject.isDirty'); // true
post.get('postTags.firstObject.isNew'); // true
// save
post.saveTags();
// ... after save completes
post.get('hasDirtyTags'); // fasle
post.get('hasDirtyPostTags'); // false
Removes an item relationship from the parent.
Presume the comment
model is using the belongsTo('comment', 'post')
mixin.
// fetch records from the store
const post = this.store.find('post', 1);
const comment = this.store.find('comment', 1);
// has a post
comment.get('post'); // post
// remove a post
comment.removePost(post);
// status
comment.get('post'); // null
comment.get('_removedPost'); // post
comment.get('isDirty'); // true
comment.get('hasRemovedPost'); // true
// save
comment.save();
// ... after save completes
comment.get('_removedPost'); // null
comment.get('isDirty'); // false
comment.get('hasRemovedPost'); // false
// fetch records from the store
const post = this.store.find('post', 1);
// has a comment
post.get('comments'); // [comment]
const comment = post.get('comments.firstObject');
// remove the comment
post.removeComment(comment);
// status
post.get('comments'); // []
post.get('hasDirtyComments'); // true
post.get('hasRemovedComments'); // true
post.get('_removedComments'); // [comment]
// comment still exists, without a post
comment.get('post'); // null
comment.get('isDirty'); // true
comment.get('isDeleted'); // false
// save
post.saveComments();
// ... after save completes
post.get('hasDirtyComments'); // false
post.get('hasRemovedComments'); // false
post.get('_removedComments'); // []
comment.get('isDirty'); // false
// fetch records from the store
const post = this.store.find('post', 1);
// has a tag
post.get('tags'); // [tag]
const tag = post.get('tags.firstObject');
const postTag = post.get('postTags.firstObject');
// remove the tag
post.removeTag(tag);
// tag status
post.get('tags'); // []
post.get('hasDirtyTags'); // false
post.get('hasRemovedTags'); // true
post.get('_removedTags'); // [tag]
// tag is not touched
tag.get('isDirty'); // false
tag.get('isDeleted'); // false
// postTag status
post.get('postTags'); // []
post.get('hasDirtyPostTags'); // true
post.get('hasRemovedPostTags'); // true
post.get('_removedPostTags'); // [postTag]
// postTag is changed but not deleted
postTag.get('isDirty'); // true
postTag.get('isDeleted'); // false
postTag.get('post'); // null
// save
post.saveTags();
// ... after save completes
post.get('hasRemovedTags'); // false
post.get('hasRemovedPostTags'); // false
post.get('_removedTags'); // []
post.get('_removedPostTags'); // []
postTag.get('isDirty'); // false
Only for belongsTo
. Removes an item relationship from the parent. Doesn't require an item to be specified since there is ony one item in a belongsTo
relationship.
// fetch records from the store
const comment = this.store.find('comment', 1);
// has a post
comment.get('post'); // post
// remove a post
comment.removePost();
// status
comment.get('post'); // null
comment.get('_removedPost'); // post
comment.get('isDirty'); // true
comment.get('hasRemovedPost'); // true
// save
comment.save();
// ... after save completes
comment.get('_removedPost'); // null
comment.get('isDirty'); // false
comment.get('hasRemovedPost'); // false
Removes an item relationship from the parent and deletes the record.
// fetch records from the store
const post = this.store.find('post', 1);
// has a comment
post.get('comments'); // [comment]
const comment = post.get('comments.firstObject');
// delete the comment
post.deleteComment(comment);
// status
post.get('comments'); // []
post.get('hasDirtyComments'); // true
post.get('hasRemovedComments'); // true
post.get('_removedComments'); // [comment]
// comment is deleted
comment.get('post'); // null
comment.get('isDirty'); // true
comment.get('isDeleted'); // true
// save
post.saveComments();
// ... after save completes
post.get('hasDirtyComments'); // false
post.get('hasRemovedComments'); // false
post.get('_removedComments'); // []
comment.get('isDirty'); // false
comment.get('isDeleted'); // true
// fetch records from the store
const post = this.store.find('post', 1);
// has a tag
post.get('tags'); // [tag]
const tag = post.get('tags.firstObject');
// delete the tag
post.deleteTag(tag);
// tag status
post.get('tags'); // []
post.get('hasDirtyTags'); // true
post.get('hasRemovedTags'); // true
post.get('_removedTags'); // [tag]
// tag is deleted
tag.get('isDirty'); // true
tag.get('isDeleted'); // true
// postTag status
post.get('postTags'); // []
post.get('hasDirtyPostTags'); // true
post.get('hasRemovedPostTags'); // true
// postTag is deleted too
const postTag = post.get('_removedPostTags.firstObject');
postTag.get('isDirty'); // true
postTag.get('isDeleted'); // true
// save
post.saveTags();
// ... after save completes
post.get('hasDirtyTags'); // false
post.get('hasRemovedTags'); // false
post.get('hasDirtyPostTags'); // false
post.get('hasRemovedPostTags'); // false
tag.get('isDirty'); // false
tag.get('isDeleted'); // true
postTag.get('isDirty'); // false
postTag.get('isDeleted'); // true
Removes an item relationship from the parent, deletes the record and saves the deletion.
// fetch records from the store
const post = this.store.find('post', 1);
// has a comment
post.get('comments'); // [comment]
const comment = post.get('comments.firstObject');
// destroy the comment
post.destroyComment(comment);
// ... after save completes
post.get('comments'); // []
post.get('hasDirtyComments'); // false
post.get('hasRemovedComments'); // false
post.get('_removedComments'); // []
// comment is destroyed
comment.get('post'); // null
comment.get('isDirty'); // false
comment.get('isDeleted'); // true
// fetch records from the store
const post = this.store.find('post', 1);
// has a tag
post.get('tags'); // [tag]
const postTag = post.get('postTags.firstObject');
const tag = post.get('tags.firstObject');
// destroy the tag
post.destroyTag(tag);
// ... after save completes
// tag status
post.get('tags'); // []
post.get('hasDirtyTags'); // false
post.get('hasRemovedTags'); // false
post.get('_removedTags'); // []
// tag is destroyed
tag.get('isDirty'); // false
tag.get('isDeleted'); // true
// postTag status
post.get('postTags'); // []
post.get('hasDirtyPostTags'); // false
post.get('hasRemovedPostTags'); // false
// postTag is destroyed too
postTag.get('isDirty'); // false
postTag.get('isDeleted'); // true
Saves any items related to the parent that are dirty, including those that have been removed or deleted.
// fetch records from the store
const post = this.store.find('post', 1);
// has a comment
post.get('comments'); // [comment]
// make some changes
post.addComment(comment);
post.createComment({body: 'hello'});
post.deleteComment(post.get('comments.firstObject'));
// status
post.get('comments'); // [comment, comment]
post.get('hasDirtyComments'); // true
post.get('hasRemovedComments'); // true
// save
post.saveComments();
// ... after save completes
post.get('hasDirtyComments'); // false
post.get('hasRemovedComments'); // false
// fetch records from the store
const post = this.store.find('post', 1);
// has a tag
post.get('tags'); // [tag]
// make some changes
post.addTag(tag);
post.createTag({name: 'hello'});
post.deleteTag(post.get('tags.firstObject'));
// tag status
post.get('tags'); // [tags, tags]
post.get('_removedTags'); // [tag]
post.get('hasDirtyTags'); // true
post.get('hasRemovedTags'); // true
// postTag status
post.get('postTags'); // [postTag, postTag]
post.get('hasDirtyPostTags'); // true
post.get('hasRemovedPostTags'); // true
// save
post.saveTags();
// ... after save completes
post.get('hasDirtyTags'); // false
post.get('hasRemovedTags'); // false
post.get('hasDirtyPostTags'); // false
post.get('hasRemovedPostTags'); // false
Only for hasManyThrough
. Same as saveItems()
except it does not save the "through" records.
// fetch records from the store
const post = this.store.find('post', 1);
// has a tag
post.get('tags'); // [tag]
// make some changes
post.addTag(tag);
post.createTag({name: 'hello'});
post.deleteTag(post.get('tags.firstObject'));
// tag status
post.get('tags'); // [tags, tags]
post.get('_removedTags'); // [tag]
post.get('hasDirtyTags'); // true
post.get('hasRemovedTags'); // true
// postTag status
post.get('postTags'); // [postTag, postTag]
post.get('hasDirtyPostTags'); // true
post.get('hasRemovedPostTags'); // true
// save
post.saveTags(false);
// ... after save completes
// tags are saved
post.get('hasDirtyTags'); // false
post.get('hasRemovedTags'); // false
// post tags are not saved
post.get('hasDirtyPostTags'); // true
post.get('hasRemovedPostTags'); // true
The relationship mixins override the save()
on the model itself in order to automatically save changes to any of its relationships. The order the mixins are added to the model will have an effect on the order the saveItems()
calls are made.
Presume we define our model like this:
import DS from 'ember-data';
import { hasMany, hasManyThrough } from 'relationship/mixins';
// Post
export default DS.Model.extend(
hasMany('post', 'comment'),
hasMany('post', 'post-tag'),
hasManyThrough('post', 'tag', 'post-tag'),
{
title: DS.attr('string'),
body: DS.attr('string'),
}
);
Our save method will work a little bit like this:
// psuedo-code
// in reality this is some form of callback hell (super hell?)
// the code below wouldn't actually execute as written
save() {
// save tags was defined last
// so it is the first override called
// save() saves the tags first
this.saveTags();
// then saveTags() calls savePostTags()
this.savePostTags();
// then saveTags() calls _super()
this._super.apply(this, arguments);
// _super() is the postTags override
// postTags needs the post to be saved first
this._super.apply(this, arguments);
// _super() is the comments override
// comments needs the post to be saved first too
this._super.apply(this, arguments);
// _super() calls the real save()
this.save();
// then comments can finish its override
this.saveComments();
// then postTags can finish its override
this.savePostTags(); // does nothing
}
Rewritten as a nested function.
// psuedo-code
// it's more like this but not entirely
function save () {
const self = this;
// from hasManyThrough('post', 'tag', 'post-tag')
function save() {
// from hasMany('post', 'post-tag')
function save() {
// hasMany('post', 'comment')
function save() {
// real save
function save() {
// ... whatever the model does
}
// super first
save();
// then save comments
self.saveComments();
}
// super first
save();
// then save post tags
self.savePostTags();
}
// save tags
self.saveTags(); // internally calls self.savePostTags();
// then super
save();
}
save();
}
Doesn't actually override the save()
method because it's not necessary.
// fetch records from the store
const comment = this.store.find('comment', 1);
// make a change
comment.removePost();
// save it
comment.save();
// fetch records from the store
const post = this.store.find('post', 1);
// make a change
post.createComment({body: 'hello'});
// save it
post.save();
// calls post.saveComments()
// then post.save();
// returns RSVP hash?
// fetch records from the store
const post = this.store.find('post', 1);
// make a change
post.createTag({name: 'hello'});
// save it
post.save();
// calls post.saveTags()
// then post.savePostTags();
// then post.save();
// returns RSVP hash?
Same as save()
except the relationship overrides are not called.
Doesn't actually override the save()
method because it's not necessary.
// fetch records from the store
const comment = this.store.find('comment', 1);
// make a change
comment.removePost();
// save it
comment.save(false); // undefined behavior
// fetch records from the store
const post = this.store.find('post', 1);
// make a change
post.createComment({body: 'hello'});
// save it
post.save(false);
// ... after save completes
post.get('hasDirtyComments'); // true
// fetch records from the store
const post = this.store.find('post', 1);
// make a change
post.createTag({name: 'hello'});
// save it
post.save(false);
// ... after save completes
post.get('hasDirtyTags'); // true