escalant3
escalant3

Reputation: 68

EmberData: Two models related with hasMany relationships

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 labels.

I am trying to use an adapter that extends the default RESTAdapter. All the application works fine but the double hasMany relationship throws an exception. Digging into the code, a method inverseBelongsToForHasMany throws an exception.

So, I guess that Ember.Data does not support the association of two models with hasMany relationships in both sides and every hasMany requires an associated belongsTo. My questions are:

  1. Is this supported and the issue is just I am doing something wrong?
  2. If it is not supported, is it a feature planned to appear?
  3. Is this a association type to be avoided in this kind of applications? If so, which is the best approach or workaround?

Thanks in advance

Upvotes: 4

Views: 3735

Answers (2)

Andre Malan
Andre Malan

Reputation: 2043

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.

Upvotes: 8

sly7_7
sly7_7

Reputation: 12011

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.

Upvotes: 8

Related Questions