I have an application logic that requires two models to have reciprocal hasMany relationships. As an example, imagine a set of GitHub issues that can be tagged with several labe
Many to Many relationships are not yet supported in ember-data. For the moment, one possible workaround is to manually manage the join table.
A = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('b');
}
});
Ab = DS.Model.extend({
a: DS.belongsTo('A'),
b: DS.belongsTo('b')
});
B = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('a');
}
});
This is just the starting point. You need then to customize your models and adapter in order to send/receive/persist records in a working manner
For example, in our app, we introduce an { includedJoin: true }
option inside the hasMany relations, and declare the join table as a JoinModel
A = DS.Model.extend({
abs: DS.hasMany('Ab', {includeJoin: true}),
...
});
DS.JoinModel = DS.Model.extend();
Ab = DS.JoinModel.extend({
... belongsTo relationships ...
});
Then in the Adapter, we override the create/update/delete methods in order to ignore the joins table lifecycle in the store
createRecords: function (store, type, records) {
if (!DS.JoinModel.detect(type)) {
this._super(store, type, records);
}
}
Finally, in the serializer, we override the addHasMany
function in order to send the join data to the server as embedded ids in the parent models.
addHasMany: function (hash, record, key, relationship) {
var
options = relationship.options,
children = [];
//we only add join models, use of `includeJoin`
if (options.includedJoin) {
record.get(relationship.key).forEach(function (child) {
children.pushObject(child.toJSON({
includeId: true
}));
});
hash[key] = children;
}
}
Server-side we are using Rails with ActiveModelSerializer, so the only little-tricky-customization is when when we update the parent models, we manually manage the joins relation, and create/delete entries in the join table.
We use a similar method of creating the association object. However, instead of overriding the methods in store, we just added the join objects to the api.
so in the models we create:
App.Hashtag = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.User = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.HashtagUser = DS.Model.extend({
user: DS.belongsTo('App.User'),
hashtag: DS.belongsTo('App.Hashtag')
});
Then for the transactions we simply alter and commit the join object.
App.UserController = Ember.ObjectController.extend({
followHashtag: function(tag) {
var hashtagUser;
hashtagUser = this.get('hashtagUsers').createRecord({
hashtag: tag
});
tag.get('hashtagUsers').pushObject(hashtagUser);
App.store.commit();
}
unfollowHashtag: function(tag) {
var itemToRemove;
itemToRemove = this.get('hashtagUsers').find(function(hashtagUser) {
if (hashtagUser.get('hashtag') === this) {
return true;
}
}, tag);
this.get('hashtagUser').removeObject(itemToRemove);
tag.get('hashtagUser').removeObject(itemToRemove);
itemToRemove.deleteRecord();
App.store.commit();
});
The API creates a HashtagUser object and the follow method just adds that user to both the associated pieces.
For removal, it pops the associated objects and destroys the association object.
Although it's not as elegant as it could be, our big motivation was that when Ember Data gets updated then we should be able to transition it to a simple stock Ember Data supported version more easily than if we've messed with the Store itself.