Cleaner way to define polymorphic associations


#1

Polymorphic relations are currently the pain point for me (I hope for other people as well) in complex scenarios. Here is how we create complex polymorphic model relations right now:

Child models:

App.Foo = DS.Model.extend({
  fooBarable: DS.belongsTo('foo-bar', { polymorphic: true })
});

App.Bar = DS.Model.extend({
  fooBarable: DS.belongsTo('foo-bar', { polymorphic: true })
  bazBarable: DS.belongsTo('bar-baz', { polymorphic: true })
});

App.Baz = DS.Model.extend({
  bazable: DS.belongsTo('bar-baz', { polymorphic: true })
});

Mixins that will make multiple associations possible:

App.Fooable = Ember.Mixin.create({
  foos: DS.hasMany('foo')
});

App.Barable = Ember.Mixin.create({
  bars: DS.hasMany('bar')
});

App.Bazable = Ember.Mixin.create({
  bazs: DS.hasMany('baz')
});

Objects which we will extend to create parent objects (and which contain different combinations of child objects):

App.FooBar = DS.Model.extend(
  Fooable, Barable, {});

App.BarBaz = DS.Model.extend(
  Barable, Bazable, {});

Parent objects

App.Father = FooBar.extend({
});

App.Mother = BarBaz.extend({
});

With 3 child objects and 2 parent objects we needed to create 3 mixins and 2 objects (additional 5 objects) from which parent objects will inherit. And this is a fairly simple scenario; once things get more complex (and they always do in larger projects) this will be a nightmare to maintain.

I want to propose a change into the public API which will help developers to define polymorphic models in a cleaner way (Ruby on Rails is a huge inspiration here — http://guides.rubyonrails.org/association_basics.html#polymorphic-associations):

Child objects:

App.Foo = DS.Model.extend({
  fooable: DS.belongsTo('fooable', { polymorphic: true })
});

App.Bar = DS.Model.extend({
  barable: DS.belongsTo('barable', { polymorphic: true })
});

App.Baz = DS.Model.extend({
  bazable: DS.belongsTo('bazable', { polymorphic: true })
});

Parent objects

App.Father = DS.Model.extend({
  foos: DS.hasMany('foo', { as: 'fooable' }),
  bars: DS.hasMany('bar', { as: 'barable' })
});

App.Mother = DS.Model.extend({
  bars: DS.hasMany('bar', { as: 'barable' }),
  bazs: DS.hasMany('baz', { as: 'bazable' })
});

So instead of creating 5 more additional objects and writing code that is hardly readable and a nightmare to maintain we could have relations defined on the models in a cleaner way.

Unfortunately I am not familiar with Ember-data internals and I am not sure about feasibility of this proposal. I will start digging into Ember-data slowly in attempt to learn more and to craft pull request one day. Meanwhile feedback (and help) is welcomed!


#2

I agree with you that currently the polymorphic association is not very convenient.

I also use polymorphic association recently. It seems in 1.0.0.beta.9 use mixin instead of class as xxx-able is not working. But I remember in the beta 8 it works. So I could not do this in beta 9:

App.Articleable = Em.Mixin.create();

App.Notable = Em.Mixin.create();

// Post as multiple xxx-able class
App.Post = DS.Model.extend(App.Articleable, App.Notable, {
  author: DS.belongsTo('author', async: true),
  notes:  DS.hasMany('notes', async: true)
});

App.Author = DS.Model.extend({
  articles: DS.belongsTo('articleable', {async: true})
});

App.Note = DS.Model.extend({
  notable: DS.belongsTo('notable', {async: true})
});

After viewing this awesome presentation Ember Data Polymorphic Associations I realize the solution is creating 2 standalone classes:

App.Articleable = DS.Model.extend();

App.Notable = DS.Model.extend();

// Post doesn't need to inherit from Articleable or Notable
App.Post = DS.Model.extend();

But in this case I have to define multiple blank class which do nothing. This is not good either.


#3

Mixins work but you have to concentrate them in a separate class from which you will inherit (take a look at the code I’ve posted).

Now regarding approach from presentation on Polymorphic Associations — as you’ve pointed out: we still need to create common class. Another problem is that relations are not readable from defined models (which is a mess to work with). Also, will traversing both ways work? I.e. will post.atricles return articles and will article.articleable return post? I think it won’t without additional bending and hacking.

https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/model.js#L471 — the way we are forced to create polymorphic relations (using inheritance of the “common” object) right now makes sense from the source code point of view. It is clean on the backend but make code of the application unreadable.

So far, the cleanest way to define polymorphic relations is to “flatten” them:

App.Post = DS.Model.extend({
  article : DS.hasMany('article')
});

App.NotPost = DS.Model.extend({
  article: DS.hasMany('article')
});

App.Article = DS.Model.extend({
  post: DS.belongsTo('post'),
  notPost: DS.belongsTo('notPost')
});

Finally I have to define custom adapter and store to work with polymorphic relations. In complex scenarios it is much cleaner than defining X mixins and another Y models to inherit from.