New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for polymorphic associations. #750

Merged
merged 1 commit into from Apr 2, 2013

Conversation

Projects
None yet
@cyril-sf
Contributor

cyril-sf commented Feb 22, 2013

App.User = DS.Model.extend({
 messages: DS.hasMany(App.Message, {polymorphic: true})
});

App.Message = DS.Model.extend({
  created_at: DS.attr('date'),
  user: DS.belongsTo(App.User)
});

App.Post = App.Message.extend({
  title: DS.attr('string')
});

App.Comment = App.Message.extend({
  body: DS.attr('string'),
  message: DS.belongsTo(App.Message, {polymorphic: true})
});

You need to configure the serializer to map to the correct type:

DS.RESTAdapter.configure('App.Post' {
  alias: 'post'
});
DS.RESTAdapter.configure('App.Comment' {
  alias: 'comment'
});

The expected payload for a polymorphic association with the REST adapter/serializer should contain the type:

{
    user: {
        id: 3,
        // For a polymorphic hasMany
        messages: [
            {id: 1, type:"post"},
            {id:1, type: "comment"}
        ]
    },

    comment: {
        id: 1,
        // For a polymorphic belongsTo
        message_id: 1,
        message_type: "post"
    }
}

Initializing an alias will automatically allow you to sideload this
type. The support for sideloadAs is only for backward compatibility.

Two hooks exist for the serialization and materialization of an embedded
polymorphic association:

  • addType that adds the record's type to the serialized data
  • extractEmbeddedType that retrieves the record's type from the
    payload.

Both hooks rely in the JSONSerializer on keyForEmbeddedType, which returns the string used to
represent the record's type in the payload. The default value is 'type'

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Feb 23, 2013

Contributor

The code for loadHasMany isn't great but I have no idea how it is used.

Ideally, I'd like to remove the need to declare the alias, but this can be done in a separate commit.

There's no documentation, I'm willing to do it in an extra step.

cc @wycats @tomdale

Contributor

cyril-sf commented Feb 23, 2013

The code for loadHasMany isn't great but I have no idea how it is used.

Ideally, I'd like to remove the need to declare the alias, but this can be done in a separate commit.

There's no documentation, I'm willing to do it in an extra step.

cc @wycats @tomdale

@kdemanawa

This comment has been minimized.

Show comment
Hide comment
@kdemanawa

kdemanawa commented Feb 23, 2013

+1

@tchak

View changes

Show outdated Hide outdated packages/ember-data/lib/serializers/json_serializer.js Outdated
@tchak

View changes

Show outdated Hide outdated packages/ember-data/lib/serializers/json_serializer.js Outdated
@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Feb 26, 2013

Contributor

@tchak I have updated the code to reflect your comments

Contributor

cyril-sf commented Feb 26, 2013

@tchak I have updated the code to reflect your comments

@ahawkins

This comment has been minimized.

Show comment
Hide comment
@ahawkins

ahawkins Mar 1, 2013

Contributor

yes dear lord I want this :)

Contributor

ahawkins commented Mar 1, 2013

yes dear lord I want this :)

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 1, 2013

Contributor

@wycats this is good for review. Let me know what you think.

Contributor

cyril-sf commented Mar 1, 2013

@wycats this is good for review. Let me know what you think.

@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 7, 2013

@cyril-sf I'm testing this branch out now as I need polymorphic associations ASAP. After building ember-data from your branch and updating my store revision to 12 I'm getting some funny behaviour with belongsTo associations. If the store does not have the ID of the model for the association in its store, it doesn't automatically fetch the record from the server.

seanrucker commented Mar 7, 2013

@cyril-sf I'm testing this branch out now as I need polymorphic associations ASAP. After building ember-data from your branch and updating my store revision to 12 I'm getting some funny behaviour with belongsTo associations. If the store does not have the ID of the model for the association in its store, it doesn't automatically fetch the record from the server.

@hjdivad

This comment has been minimized.

Show comment
Hide comment
@hjdivad

hjdivad Mar 7, 2013

Member

@seanrucker do you have a jsfiddle?

Member

hjdivad commented Mar 7, 2013

@seanrucker do you have a jsfiddle?

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 7, 2013

Contributor

@seanrucker does the behavior you describe for a monomorphic or a polymorphic association?

In the network panel of your browser do you see a request being fired?

A jsfiddle would actually be a great help to figure out what's going on, or at least the definition of your models.

Contributor

cyril-sf commented Mar 7, 2013

@seanrucker does the behavior you describe for a monomorphic or a polymorphic association?

In the network panel of your browser do you see a request being fired?

A jsfiddle would actually be a great help to figure out what's going on, or at least the definition of your models.

@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 7, 2013

Its just a monomorphic association. In the network panel there is no request being fired. The only changes I made were updating the ember-data.js to one I built from your branch and updated the store revision to 12. I can try to put a jsfiddle together. What's the best way to get a jsfiddle environment setup with the proper libraries?

seanrucker commented Mar 7, 2013

Its just a monomorphic association. In the network panel there is no request being fired. The only changes I made were updating the ember-data.js to one I built from your branch and updated the store revision to 12. I can try to put a jsfiddle together. What's the best way to get a jsfiddle environment setup with the proper libraries?

@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 7, 2013

I repeated the same steps using a fresh build of the latest ember-data on master and the problem did not exist. So it would appear that it is indeed related to this branch.

seanrucker commented Mar 7, 2013

I repeated the same steps using a fresh build of the latest ember-data on master and the problem did not exist. So it would appear that it is indeed related to this branch.

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 8, 2013

Contributor

@seanrucker I can reproduce it, I will debug and let you know.

Contributor

cyril-sf commented Mar 8, 2013

@seanrucker I can reproduce it, I will debug and let you know.

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 8, 2013

Contributor

@seanrucker I've fixed that issue. This branch doesn't pass on Travis because I rebased without realizing that master is broken.

Contributor

cyril-sf commented Mar 8, 2013

@seanrucker I've fixed that issue. This branch doesn't pass on Travis because I rebased without realizing that master is broken.

@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 8, 2013

Awesome! Thank you so much for your hard work and contributions. I'll be putting this feature through its paces. I'll let you know if I find anything else. Thanks again.

seanrucker commented Mar 8, 2013

Awesome! Thank you so much for your hard work and contributions. I'll be putting this feature through its paces. I'll let you know if I find anything else. Thanks again.

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 8, 2013

Contributor

@seanrucker Thanks for taking the time to report the issue!

Contributor

cyril-sf commented Mar 8, 2013

@seanrucker Thanks for taking the time to report the issue!

opichals added a commit to opichals/emberjs-data that referenced this pull request Mar 8, 2013

Merge pull request emberjs#750 from Cyril-sf/polymorphic_associations
Added support for polymorphic associations.
@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 8, 2013

@cyril-sf The serializer is always instantiating the base type. I must be missing something.

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

App.DocumentMessage = App.Message.extend();

DS.RESTAdapter.configure('App.DocumentMessage', {
  alias: 'document_message'
});

App.Conversation = DS.Model.extend({
  messages: DS.hasMany('App.Message', {
    polymorphic: true 
  })
});

A call to the endpoint /conversations/1 returns something like this:

{
  "conversation": { 
    "id": 1, 
    "message_ids": [1] 
  }
}

Which triggers a call to /messages?ids[]=1 and returns something like this:

{
  "messages": [
    {
      "id": 1,
      "type": "document_message"
    }
  ]
}

The issue is that Ember is serializing the message as App.Message rather than App.DocumentMessage. What am I missing?

seanrucker commented Mar 8, 2013

@cyril-sf The serializer is always instantiating the base type. I must be missing something.

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

App.DocumentMessage = App.Message.extend();

DS.RESTAdapter.configure('App.DocumentMessage', {
  alias: 'document_message'
});

App.Conversation = DS.Model.extend({
  messages: DS.hasMany('App.Message', {
    polymorphic: true 
  })
});

A call to the endpoint /conversations/1 returns something like this:

{
  "conversation": { 
    "id": 1, 
    "message_ids": [1] 
  }
}

Which triggers a call to /messages?ids[]=1 and returns something like this:

{
  "messages": [
    {
      "id": 1,
      "type": "document_message"
    }
  ]
}

The issue is that Ember is serializing the message as App.Message rather than App.DocumentMessage. What am I missing?

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 8, 2013

Contributor

@seanrucker The payload you get from your server isn't valid for polymorphic associations.

The payload you receive from your server needs to be like this:

{
  "conversation": { 
    "id": 1, 
    "messages": [{"id": 1, "type": "document_message"}] 
  }
}

If the server doesn't return the type with the id, the framework has no way to figure out which model to instantiate.

I'll check if I can have a good error message for that case.

Let me know if that fixes your problem.

Contributor

cyril-sf commented Mar 8, 2013

@seanrucker The payload you get from your server isn't valid for polymorphic associations.

The payload you receive from your server needs to be like this:

{
  "conversation": { 
    "id": 1, 
    "messages": [{"id": 1, "type": "document_message"}] 
  }
}

If the server doesn't return the type with the id, the framework has no way to figure out which model to instantiate.

I'll check if I can have a good error message for that case.

Let me know if that fixes your problem.

@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 9, 2013

@cyril-sf I've got the payload returning as described but now I'm getting this error:

assertion failed: Unable to resolve type document_message.  You may need to configure your serializer aliases. 

I've tried setting up the alias as described in the PR comments but it doesn't seem to be working:

DS.RESTAdapter.configure('App.DocumentMessage', {
  alias: 'document_message'
});

seanrucker commented Mar 9, 2013

@cyril-sf I've got the payload returning as described but now I'm getting this error:

assertion failed: Unable to resolve type document_message.  You may need to configure your serializer aliases. 

I've tried setting up the alias as described in the PR comments but it doesn't seem to be working:

DS.RESTAdapter.configure('App.DocumentMessage', {
  alias: 'document_message'
});
@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Mar 9, 2013

@cyril-sf I've got it working now. I looked through your tests and changed the way I'm creating the aliases:

var adapter = DS.RESTAdapter.create();

App.Store = DS.Store.extend({
  revision: 12,
  adapter: adapter
});

var serializer = adapter.get('serializer');

serializer.configure('App.DocumentMessage', {
  alias: 'document_message'
});

seanrucker commented Mar 9, 2013

@cyril-sf I've got it working now. I looked through your tests and changed the way I'm creating the aliases:

var adapter = DS.RESTAdapter.create();

App.Store = DS.Store.extend({
  revision: 12,
  adapter: adapter
});

var serializer = adapter.get('serializer');

serializer.configure('App.DocumentMessage', {
  alias: 'document_message'
});
@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 9, 2013

Contributor

@seanrucker Good to hear that things are working now.

Maybe you configure your adapter too late. There should be no reason to configure the serializer instead of the adapter. Check this code:

DS.RESTAdapter.configure('App.Post', {
  alias: 'post'
});
DS.RESTAdapter.configure('App.Comment', {
  alias: 'comment'
});

App.Store = DS.Store.extend({
  revision: 12,
  adapter:  DS.RESTAdapter.create()
});

https://github.com/Cyril-sf/ember_data_example/blob/polymorphism/app/assets/javascripts/store.js

I believe you need to configure the adapter before the store instantiates it.

Contributor

cyril-sf commented Mar 9, 2013

@seanrucker Good to hear that things are working now.

Maybe you configure your adapter too late. There should be no reason to configure the serializer instead of the adapter. Check this code:

DS.RESTAdapter.configure('App.Post', {
  alias: 'post'
});
DS.RESTAdapter.configure('App.Comment', {
  alias: 'comment'
});

App.Store = DS.Store.extend({
  revision: 12,
  adapter:  DS.RESTAdapter.create()
});

https://github.com/Cyril-sf/ember_data_example/blob/polymorphism/app/assets/javascripts/store.js

I believe you need to configure the adapter before the store instantiates it.

@zubairov

This comment has been minimized.

Show comment
Hide comment
@zubairov

zubairov Mar 15, 2013

I need this feature. What's the status of it?

zubairov commented Mar 15, 2013

I need this feature. What's the status of it?

@arbales

This comment has been minimized.

Show comment
Hide comment
@arbales

arbales Mar 15, 2013

Contributor

Awaiting this as well 👍

Contributor

arbales commented Mar 15, 2013

Awaiting this as well 👍

@zubairov

This comment has been minimized.

Show comment
Hide comment
@zubairov

zubairov Mar 15, 2013

Tried this pull-request. Works like a charm, with some minor issues:

Does not work for root collections

I can't do following:
App.Account = DS.Model.extend, App.DropboxAccount = App.Account.extend, App.WufooAccount = App.Account.extend
Then do this: App.Account.find() and get a list of all Dropbox and Wufoo accounts in one collection. Reason see below.

Does require backend to deliver types along with IDs

It became apparent that type information is required before actual object will be loaded, hence Parent need to specify IDs. In sample above:

user: {
        id: 3,
        // For a polymorphic hasMany
        message_ids: [
            {id: 1, type:"post"},
            {id:1, type: "comment"}
        ]
    }

Note that type is on the message_ids which is a significant problem - type should be defined not on the parent but on child nodes, parent should't be aware about the types of children.

Need additional tweaking based on loading polymorphic children

IMO polymorphic children should be loaded from one resource (collection). For example in sample above polymorphic Post and Comments should be loaded from /api/1/messages as well as created/updated into the /api/1/messages too and not into /api/1/posts and /api/1/comments.

zubairov commented Mar 15, 2013

Tried this pull-request. Works like a charm, with some minor issues:

Does not work for root collections

I can't do following:
App.Account = DS.Model.extend, App.DropboxAccount = App.Account.extend, App.WufooAccount = App.Account.extend
Then do this: App.Account.find() and get a list of all Dropbox and Wufoo accounts in one collection. Reason see below.

Does require backend to deliver types along with IDs

It became apparent that type information is required before actual object will be loaded, hence Parent need to specify IDs. In sample above:

user: {
        id: 3,
        // For a polymorphic hasMany
        message_ids: [
            {id: 1, type:"post"},
            {id:1, type: "comment"}
        ]
    }

Note that type is on the message_ids which is a significant problem - type should be defined not on the parent but on child nodes, parent should't be aware about the types of children.

Need additional tweaking based on loading polymorphic children

IMO polymorphic children should be loaded from one resource (collection). For example in sample above polymorphic Post and Comments should be loaded from /api/1/messages as well as created/updated into the /api/1/messages too and not into /api/1/posts and /api/1/comments.

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Mar 16, 2013

Contributor

@zubairov what you are describing seems to be single table inheritance, where you actually have a unique ID for a message and you can deduce the type from it.

The problem I can see with it is that you can't get a promise back because you don't know the type of your object at that moment. I'll try to think about it and see how this can be solved.

Contributor

cyril-sf commented Mar 16, 2013

@zubairov what you are describing seems to be single table inheritance, where you actually have a unique ID for a message and you can deduce the type from it.

The problem I can see with it is that you can't get a promise back because you don't know the type of your object at that moment. I'll try to think about it and see how this can be solved.

@zubairov

This comment has been minimized.

Show comment
Hide comment
@zubairov

zubairov Mar 16, 2013

@cyril-sf exactly, there are different approaches on how to map inheritance into the JSON structures, some of them like in your sample are using two different resources (comments & posts) and some are served from the same resource (single resource inheritance ;) ) but with a distinct identifier.
In your example unique identity of the child object is an ID&Type tuple where in mine it's only ID. I need to think more about it, however my gut feeling tells me that ember-data backend (or it's current REST implementation) is not HATEOAS enough, but this might be a topic of the new discussion / pull request.

BTW we really need this feature in our product and we ready to invest time in it, so I would be happy to help developing it and contributing back to your repository and/or ember-data.

zubairov commented Mar 16, 2013

@cyril-sf exactly, there are different approaches on how to map inheritance into the JSON structures, some of them like in your sample are using two different resources (comments & posts) and some are served from the same resource (single resource inheritance ;) ) but with a distinct identifier.
In your example unique identity of the child object is an ID&Type tuple where in mine it's only ID. I need to think more about it, however my gut feeling tells me that ember-data backend (or it's current REST implementation) is not HATEOAS enough, but this might be a topic of the new discussion / pull request.

BTW we really need this feature in our product and we ready to invest time in it, so I would be happy to help developing it and contributing back to your repository and/or ember-data.

@hjdivad

This comment has been minimized.

Show comment
Hide comment
@hjdivad

hjdivad Mar 16, 2013

Member

@zubairov there are a couple of issues with identifying by id only.

Non-shared Id-spaces

Polymorphic associations may not share an id-space. If your server happens to be backing the association using single table inheritance on a relational database, you'll get a unique id-space but it's just a side effect of the implementation.

In principle we could make it possible to have a custom adapter that retrieved from /messages rather than /posts. This would require expanding the adapter api: at the moment when faced with multiple types the store aggregates by type and delegates the find for each type to the adapter. The store could instead aggregate by type and delegate the whole collection to the adapter, giving adapter authors a hook like findManyHeterogeneous, whose default implementation just called findMany for each type.

Promises

The bigger issue is the one @cyril-sf mentioned. In something like rails, it's fine to say user.messages.first and get e.g. a Post from only a message_id because rails will block on the data retrieval. Blocking is reasonable for a web server request to a database server, but is obviously problematic client-side. In the monomorphic case, user.get('bestFriend') returns an actual App.User instance that simply isn't loaded yet. We'd want to do something similar in the polymorphic case: have user.get('messages.firstObject') return a promise of the right type. But we need the type before we can instantiate the object: instantiating an App.Message and turning it into an App.Post doesn't quite work (there are approaches, but they have problems).

Root Type

Getting App.Account.find() to work is not so difficult. The problem is getting App.Account.find( 1 ) to work precisely because of the promise issue.


I hope I've made it clear why we pair ids with types. Do you still think it's a significant problem for association references to need to be id, type tuples rather than just ids? Any other thoughts?

Member

hjdivad commented Mar 16, 2013

@zubairov there are a couple of issues with identifying by id only.

Non-shared Id-spaces

Polymorphic associations may not share an id-space. If your server happens to be backing the association using single table inheritance on a relational database, you'll get a unique id-space but it's just a side effect of the implementation.

In principle we could make it possible to have a custom adapter that retrieved from /messages rather than /posts. This would require expanding the adapter api: at the moment when faced with multiple types the store aggregates by type and delegates the find for each type to the adapter. The store could instead aggregate by type and delegate the whole collection to the adapter, giving adapter authors a hook like findManyHeterogeneous, whose default implementation just called findMany for each type.

Promises

The bigger issue is the one @cyril-sf mentioned. In something like rails, it's fine to say user.messages.first and get e.g. a Post from only a message_id because rails will block on the data retrieval. Blocking is reasonable for a web server request to a database server, but is obviously problematic client-side. In the monomorphic case, user.get('bestFriend') returns an actual App.User instance that simply isn't loaded yet. We'd want to do something similar in the polymorphic case: have user.get('messages.firstObject') return a promise of the right type. But we need the type before we can instantiate the object: instantiating an App.Message and turning it into an App.Post doesn't quite work (there are approaches, but they have problems).

Root Type

Getting App.Account.find() to work is not so difficult. The problem is getting App.Account.find( 1 ) to work precisely because of the promise issue.


I hope I've made it clear why we pair ids with types. Do you still think it's a significant problem for association references to need to be id, type tuples rather than just ids? Any other thoughts?

@calumbrodie

This comment has been minimized.

Show comment
Hide comment
@calumbrodie

calumbrodie Mar 21, 2013

This is a really useful PR. +1 on working out the remaining niggles. I'd also add that this strategy is appropriate for not only STI but also Multiple Table Inheritance (Class Table Inheritance). I think the above concerns are valid but probably don't describe the way that the majority of people would architect a web app. As such it might be a good idea to try and progress this as first implementation and then iterate for the more advanced use cases?

FYI - using a unique ID for an object type is the way of handling this in Doctrine (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html) and Hibernate (http://docs.jboss.org/hibernate/orm/3.3/reference/en/html/inheritance.html) - although hibernate offers insane customisation that could be used to have one type per table, leading to the above issue. Never seen it in IRL though.

Edit: Having re-read the thread it seems like second guessing others architectural decisions is probably something I shouldn't do - looks to be enough variation in this thread alone to justify further discussion. FWIW I have the same schema and concerns as @zubairov (using class table inheritance).

calumbrodie commented Mar 21, 2013

This is a really useful PR. +1 on working out the remaining niggles. I'd also add that this strategy is appropriate for not only STI but also Multiple Table Inheritance (Class Table Inheritance). I think the above concerns are valid but probably don't describe the way that the majority of people would architect a web app. As such it might be a good idea to try and progress this as first implementation and then iterate for the more advanced use cases?

FYI - using a unique ID for an object type is the way of handling this in Doctrine (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html) and Hibernate (http://docs.jboss.org/hibernate/orm/3.3/reference/en/html/inheritance.html) - although hibernate offers insane customisation that could be used to have one type per table, leading to the above issue. Never seen it in IRL though.

Edit: Having re-read the thread it seems like second guessing others architectural decisions is probably something I shouldn't do - looks to be enough variation in this thread alone to justify further discussion. FWIW I have the same schema and concerns as @zubairov (using class table inheritance).

@zubairov

This comment has been minimized.

Show comment
Hide comment
@zubairov

zubairov Mar 21, 2013

@hjdivad @cyril-sf @calumbrodie thanks for good discussion!
My 0.02 €

Shaed/Non-shaed ID spaces

Absolutely agree with you @hjdivad it is indeed a huge problem in ORM as separate relational database primary keys are not consistently unique across all database. In the REST world situation is different. As I wrote in my initial comment approach and expectations on which ember-data is based are more dictated by relational (ActiveRecord-style) backend then REST backend. I believe both approaches have their place however once ember-data no longer talking to database we should target more REST-like or to be more precise Hypermedia-driven states.
Decision about shared vs. non-shared ID spaces will be much simpler once we will understand the ID not as string, or tuple (id + type) but as URI.

Promises

Got your point here. It is obviously not a simple problem, however I think Ember-backed infrastructure is there to help (especially in JavaScript-land).
In @hjdivad example user.get('messages.firstObject') should return a promise of the right type, but what is type, and why it is so important to have the right type? As I see it type is a nice thing to have when you serialize - deserialize as the meta-information associated with the given object is required. But as we have multiple states of the record this problem can be solved - Type will materialize only after Record is loaded. I could imagine we would need a new state in the Record's state chart, but that should be doable right?

Root types

I believe in the @hjdivad sample it should return a object of Account's subtype.

zubairov commented Mar 21, 2013

@hjdivad @cyril-sf @calumbrodie thanks for good discussion!
My 0.02 €

Shaed/Non-shaed ID spaces

Absolutely agree with you @hjdivad it is indeed a huge problem in ORM as separate relational database primary keys are not consistently unique across all database. In the REST world situation is different. As I wrote in my initial comment approach and expectations on which ember-data is based are more dictated by relational (ActiveRecord-style) backend then REST backend. I believe both approaches have their place however once ember-data no longer talking to database we should target more REST-like or to be more precise Hypermedia-driven states.
Decision about shared vs. non-shared ID spaces will be much simpler once we will understand the ID not as string, or tuple (id + type) but as URI.

Promises

Got your point here. It is obviously not a simple problem, however I think Ember-backed infrastructure is there to help (especially in JavaScript-land).
In @hjdivad example user.get('messages.firstObject') should return a promise of the right type, but what is type, and why it is so important to have the right type? As I see it type is a nice thing to have when you serialize - deserialize as the meta-information associated with the given object is required. But as we have multiple states of the record this problem can be solved - Type will materialize only after Record is loaded. I could imagine we would need a new state in the Record's state chart, but that should be doable right?

Root types

I believe in the @hjdivad sample it should return a object of Account's subtype.

@heartsentwined

This comment has been minimized.

Show comment
Hide comment
@heartsentwined

heartsentwined May 28, 2013

Contributor

Thanks @hjdivad @cyril-sf , so where would this map to, in the RESTAdapter? Is it /messages or /comments? (Sorry if I'm going too far, because this is one of my initial confusion on how exactly this polymorphism would work.)

Contributor

heartsentwined commented May 28, 2013

Thanks @hjdivad @cyril-sf , so where would this map to, in the RESTAdapter? Is it /messages or /comments? (Sorry if I'm going too far, because this is one of my initial confusion on how exactly this polymorphism would work.)

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf May 28, 2013

Contributor

@heartsentwined Because you call createRecord on App.Comment, ED will use /comments for CRUD operations on this record.

/messages is never used with ED.

Contributor

cyril-sf commented May 28, 2013

@heartsentwined Because you call createRecord on App.Comment, ED will use /comments for CRUD operations on this record.

/messages is never used with ED.

@heartsentwined

This comment has been minimized.

Show comment
Hide comment
@heartsentwined

heartsentwined May 28, 2013

Contributor

Thanks for the clarification @cyril-sf, I shall wait on your example app for more implementation guidance.

Contributor

heartsentwined commented May 28, 2013

Thanks for the clarification @cyril-sf, I shall wait on your example app for more implementation guidance.

aliases.forEach(function(key, type) {
plural = self.pluralize(key);
Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural));

This comment has been minimized.

@KOGI

KOGI Jun 7, 2013

It looks like this assertion needs to also check the aliased type -- same as what's being done on line 1092 below.

Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural) || (aliases.get(plural) === type) );

@KOGI

KOGI Jun 7, 2013

It looks like this assertion needs to also check the aliased type -- same as what's being done on line 1092 below.

Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(plural) || (aliases.get(plural) === type) );

@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Jun 7, 2013

@KOGI Can you take a look at the following PR: #940

It fixes the uncountable plurals issue as well as:

#928
#1003

seanrucker commented Jun 7, 2013

@KOGI Can you take a look at the following PR: #940

It fixes the uncountable plurals issue as well as:

#928
#1003

@kylenathan

This comment has been minimized.

Show comment
Hide comment
@kylenathan

kylenathan Jun 13, 2013

@cyril-sf, the server payload key in the RESTAdapter for the polymorphic hasMany seems to have to be 'message_ids' and not 'messages' (e.g. below). Does this sound right?

"conversation": { 
    "id": 1, 
    "message_ids": [{"id": 1, "type": "document_message"}] 
}

kylenathan commented Jun 13, 2013

@cyril-sf, the server payload key in the RESTAdapter for the polymorphic hasMany seems to have to be 'message_ids' and not 'messages' (e.g. below). Does this sound right?

"conversation": { 
    "id": 1, 
    "message_ids": [{"id": 1, "type": "document_message"}] 
}

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Jun 13, 2013

Contributor

@kylenathan correct. There is no specific code for the key of a polymorphic hasMany, it uses the same convention.

Contributor

cyril-sf commented Jun 13, 2013

@kylenathan correct. There is no specific code for the key of a polymorphic hasMany, it uses the same convention.

@kevinansfield

This comment has been minimized.

Show comment
Hide comment
@kevinansfield

kevinansfield Jun 20, 2013

Contributor

@cyril-sf I've run into an issue when a model has multiple polymorphic associations - a Post can be Commentable, Attachable, and Notifiable - what is the best way to handle that right now?

I've partly worked around it by having each of my "polymorphic" models extend from the last but this means I have no control over which attributes appear on each model.

Contributor

kevinansfield commented Jun 20, 2013

@cyril-sf I've run into an issue when a model has multiple polymorphic associations - a Post can be Commentable, Attachable, and Notifiable - what is the best way to handle that right now?

I've partly worked around it by having each of my "polymorphic" models extend from the last but this means I have no control over which attributes appear on each model.

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Jun 20, 2013

Contributor

@kevinansfield That's a good question. I would try to declare them as mixins.

App.Notifiable = Ember.Mixin.create({
  notifications: DS.hasMany('notifications')
});

App.Notification = DS.Model.extend({
  notifiable: DS.belongsTo('App.Notifiable', {
    polymorphic: true,
    inverse: 'notifications'
  })
});

App.Commentable = Ember.Mixin.create({
  comments: DS.hasMany('comments')
});

App.Comment = DS.Model.extend({
  message: DS.belongsTo('App.Commentable', {
    polymorphic: true,
    inverse: 'comments'
  });
});

App.Post = DS.Model.extend(App.Commentable, App.Notifiable, {

});

I haven't tried this and I don't know if you would run into more problems.

Contributor

cyril-sf commented Jun 20, 2013

@kevinansfield That's a good question. I would try to declare them as mixins.

App.Notifiable = Ember.Mixin.create({
  notifications: DS.hasMany('notifications')
});

App.Notification = DS.Model.extend({
  notifiable: DS.belongsTo('App.Notifiable', {
    polymorphic: true,
    inverse: 'notifications'
  })
});

App.Commentable = Ember.Mixin.create({
  comments: DS.hasMany('comments')
});

App.Comment = DS.Model.extend({
  message: DS.belongsTo('App.Commentable', {
    polymorphic: true,
    inverse: 'comments'
  });
});

App.Post = DS.Model.extend(App.Commentable, App.Notifiable, {

});

I haven't tried this and I don't know if you would run into more problems.

@kevinansfield

This comment has been minimized.

Show comment
Hide comment
@kevinansfield

kevinansfield Jun 20, 2013

Contributor

@cyril-sf Thanks. That does look like a nice way to handle it but unfortunately the app errors with the following:

screen shot 2013-06-20 at 16 42 26

And this is the code I'm using:

App.Commentable = Ember.Mixin.create
  comments: DS.hasMany('comments')

App.Comment = App.Model.extend
  commentable: DS.belongsTo 'App.Commentable',
    polymorphic: true,
    inverse: 'comments'

App.Notifiable = Ember.Mixin.create
  notifications: DS.hasMany('notifications')

App.Notification = App.Model.extend
  notifiable: DS.belongsTo 'App.Notifiable',
    polymorphic: true
    inverse: 'notifications'

App.Attachable = Ember.Mixin.create
  attachments: DS.hasMany('attachments')

App.Attachment = App.Model.extend App.Commentable, App.Notifiable,
  attachable: DS.belongsTo 'App.Attachable',
    polymorphic: true,
    inverse: 'attachments'

App.Post = App.Model.extend App.Attachable, App.Commentable, App.Notifiable,
Contributor

kevinansfield commented Jun 20, 2013

@cyril-sf Thanks. That does look like a nice way to handle it but unfortunately the app errors with the following:

screen shot 2013-06-20 at 16 42 26

And this is the code I'm using:

App.Commentable = Ember.Mixin.create
  comments: DS.hasMany('comments')

App.Comment = App.Model.extend
  commentable: DS.belongsTo 'App.Commentable',
    polymorphic: true,
    inverse: 'comments'

App.Notifiable = Ember.Mixin.create
  notifications: DS.hasMany('notifications')

App.Notification = App.Model.extend
  notifiable: DS.belongsTo 'App.Notifiable',
    polymorphic: true
    inverse: 'notifications'

App.Attachable = Ember.Mixin.create
  attachments: DS.hasMany('attachments')

App.Attachment = App.Model.extend App.Commentable, App.Notifiable,
  attachable: DS.belongsTo 'App.Attachable',
    polymorphic: true,
    inverse: 'attachments'

App.Post = App.Model.extend App.Attachable, App.Commentable, App.Notifiable,
@kevinansfield

This comment has been minimized.

Show comment
Hide comment
@kevinansfield

kevinansfield Jun 20, 2013

Contributor

Changing the DS.hasMany('comments') back to DS.hasMany('App.Comment') fixed the above error but now errors with this:

screen shot 2013-06-20 at 16 58 02

Contributor

kevinansfield commented Jun 20, 2013

Changing the DS.hasMany('comments') back to DS.hasMany('App.Comment') fixed the above error but now errors with this:

screen shot 2013-06-20 at 16 58 02

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Jun 20, 2013

Contributor

A jsfiddle might help. Have you also changed

notifications: DS.hasMany('notifications')
attachments: DS.hasMany('attachments')

to

notifications: DS.hasMany('App.Notifications')
attachments: DS.hasMany('App.Attachments')

?

Contributor

cyril-sf commented Jun 20, 2013

A jsfiddle might help. Have you also changed

notifications: DS.hasMany('notifications')
attachments: DS.hasMany('attachments')

to

notifications: DS.hasMany('App.Notifications')
attachments: DS.hasMany('App.Attachments')

?

@kevinansfield

This comment has been minimized.

Show comment
Hide comment
@kevinansfield

kevinansfield Jun 20, 2013

Contributor

Yes, I have also changed those. I'll see what I can do about sorting out a fiddle.

Contributor

kevinansfield commented Jun 20, 2013

Yes, I have also changed those. I'll see what I can do about sorting out a fiddle.

@hjdivad

This comment has been minimized.

Show comment
Hide comment
@hjdivad

hjdivad Jun 20, 2013

Member

@cyril-sf @kevinansfield you should probably get this working with DS.hasMany('App.Notifications') fully-qualified style first, but it should be possible to DS.hasMany('notifications') if you configure the serializer.

Something like:

DS.RESTAdapter.configure('App.Notification' {
  alias: 'notification'
});
Member

hjdivad commented Jun 20, 2013

@cyril-sf @kevinansfield you should probably get this working with DS.hasMany('App.Notifications') fully-qualified style first, but it should be possible to DS.hasMany('notifications') if you configure the serializer.

Something like:

DS.RESTAdapter.configure('App.Notification' {
  alias: 'notification'
});
@ayrton

This comment has been minimized.

Show comment
Hide comment
@ayrton

ayrton Jun 26, 2013

Does anyone has a working example for the reversed rails polymorphic models yet? /cc @heartsentwined @seanrucker @cyril-sf I'd be happy to help, but I can't get this working just yet.

ayrton commented Jun 26, 2013

Does anyone has a working example for the reversed rails polymorphic models yet? /cc @heartsentwined @seanrucker @cyril-sf I'd be happy to help, but I can't get this working just yet.

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Jun 26, 2013

Contributor

@ayrton I've started on updating the sample project I have to use this. It's not ready yet.

Contributor

cyril-sf commented Jun 26, 2013

@ayrton I've started on updating the sample project I have to use this. It's not ready yet.

@pzuraq

This comment has been minimized.

Show comment
Hide comment
@pzuraq

pzuraq Jun 30, 2013

@cyril-sf I'm trying to setup some polymorphic associations and am encountering the following error:

Uncaught Error: assertion failed: Unable to resolve type vehicle.  You may need to configure your serializer aliases.

I tried to add the configurations to the adapter, and got this error:

Uncaught Error: assertion failed: The 'vehicle' alias has already been defined.

When I dumped the aliases variable, it was a mapping with the following values under keys:

["vehicles", "vehicless"]

Shouldn't it be

["vehicle", "vehicles"]

?

Have you ever encountered this issue before?

pzuraq commented Jun 30, 2013

@cyril-sf I'm trying to setup some polymorphic associations and am encountering the following error:

Uncaught Error: assertion failed: Unable to resolve type vehicle.  You may need to configure your serializer aliases.

I tried to add the configurations to the adapter, and got this error:

Uncaught Error: assertion failed: The 'vehicle' alias has already been defined.

When I dumped the aliases variable, it was a mapping with the following values under keys:

["vehicles", "vehicless"]

Shouldn't it be

["vehicle", "vehicles"]

?

Have you ever encountered this issue before?

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Jun 30, 2013

Contributor

@pzuraq There is a bug when detecting if an alias has already been defined. I have a fix and need to make a PR.

How do you define your aliases?

Contributor

cyril-sf commented Jun 30, 2013

@pzuraq There is a bug when detecting if an alias has already been defined. I have a fix and need to make a PR.

How do you define your aliases?

@pzuraq

This comment has been minimized.

Show comment
Hide comment
@pzuraq

pzuraq Jun 30, 2013

@cyril-sf I've been following the examples above, so like so:

DS.RESTAdapter.configure('App.Vehicle', {
  alias: 'vehicle'
});

However, it seems to me that the problem is in the creation of the aliases mapping. At least in my case, the aliases mapping is initially populated with only the plurals, not the singular forms of any of the aliases. Pluralize is redundant in this case, unless I'm missing something. I was trying to find out how the aliases mapping is initialized, but I couldn't find it.

pzuraq commented Jun 30, 2013

@cyril-sf I've been following the examples above, so like so:

DS.RESTAdapter.configure('App.Vehicle', {
  alias: 'vehicle'
});

However, it seems to me that the problem is in the creation of the aliases mapping. At least in my case, the aliases mapping is initially populated with only the plurals, not the singular forms of any of the aliases. Pluralize is redundant in this case, unless I'm missing something. I was trying to find out how the aliases mapping is initialized, but I couldn't find it.

@triptec

This comment has been minimized.

Show comment
Hide comment
@triptec

triptec Jul 2, 2013

I've been struggling with this aswell, any news?

triptec commented Jul 2, 2013

I've been struggling with this aswell, any news?

@pzuraq

This comment has been minimized.

Show comment
Hide comment
@pzuraq

pzuraq Jul 4, 2013

@cyril-sf Still having this issue, I've been looking for days but I can't find the source. Can you or someone who knows explain how the aliases mapping is initialized? As far as I can see it is created in DS.Serializer.init but I can't figure out how the values are added to the mapping.

pzuraq commented Jul 4, 2013

@cyril-sf Still having this issue, I've been looking for days but I can't find the source. Can you or someone who knows explain how the aliases mapping is initialized? As far as I can see it is created in DS.Serializer.init but I can't figure out how the values are added to the mapping.

@pzuraq

This comment has been minimized.

Show comment
Hide comment
@pzuraq

pzuraq Jul 4, 2013

@triptec I'm currently overriding _completeAliases to get the functionality needed. I use a custom _singularizeAliases function, code sample below:

DS.Serializer.reopen({
  _completeAliases: function() {
    this._singularizeAliases();
    this._reifyAliases();
  },

  _singularizeAliases: function() {
    if (this._didSingularizeAliases) { return; }

    var aliases = this.aliases,
        sideloadMapping = this.aliases.sideloadMapping,
        singular,
        self = this;

    aliases.forEach(function(key, type) {
      singular = self.singularize(key);
      Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(singular));
      aliases.set(singular, type);
    });

    // This map is only for backward compatibility with the `sideloadAs` option.
    if (sideloadMapping) {
      sideloadMapping.forEach(function(key, type) {
        Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(key) || (aliases.get(key)===type) );
        aliases.set(key, type);
      });
      delete this.aliases.sideloadMapping;
    }

    this._didSingularizeAliases = true;
  }
});

pzuraq commented Jul 4, 2013

@triptec I'm currently overriding _completeAliases to get the functionality needed. I use a custom _singularizeAliases function, code sample below:

DS.Serializer.reopen({
  _completeAliases: function() {
    this._singularizeAliases();
    this._reifyAliases();
  },

  _singularizeAliases: function() {
    if (this._didSingularizeAliases) { return; }

    var aliases = this.aliases,
        sideloadMapping = this.aliases.sideloadMapping,
        singular,
        self = this;

    aliases.forEach(function(key, type) {
      singular = self.singularize(key);
      Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(singular));
      aliases.set(singular, type);
    });

    // This map is only for backward compatibility with the `sideloadAs` option.
    if (sideloadMapping) {
      sideloadMapping.forEach(function(key, type) {
        Ember.assert("The '" + key + "' alias has already been defined", !aliases.get(key) || (aliases.get(key)===type) );
        aliases.set(key, type);
      });
      delete this.aliases.sideloadMapping;
    }

    this._didSingularizeAliases = true;
  }
});
@seanrucker

This comment has been minimized.

Show comment
Hide comment
@seanrucker

seanrucker Jul 9, 2013

Hey guys, I've been away for 2 weeks, just catching up on emails and this thread.

I've encountered issues with the serializing of aliases as well. The main issue I've found is discussed in this issue:

#1003

I have a PR which fixes it along with another issue here:

#940

seanrucker commented Jul 9, 2013

Hey guys, I've been away for 2 weeks, just catching up on emails and this thread.

I've encountered issues with the serializing of aliases as well. The main issue I've found is discussed in this issue:

#1003

I have a PR which fixes it along with another issue here:

#940

@cyril-sf

This comment has been minimized.

Show comment
Hide comment
@cyril-sf

cyril-sf Jul 9, 2013

Contributor

@triptec @pzuraq @seanrucker I've been busy lately, things should get back to normal this week

Contributor

cyril-sf commented Jul 9, 2013

@triptec @pzuraq @seanrucker I've been busy lately, things should get back to normal this week

@miguelcobain

This comment has been minimized.

Show comment
Hide comment
@miguelcobain

miguelcobain Nov 26, 2013

I never really understood why does polymorphism support is added on associations.
Wouldn't it be easier to add it on Model itself?

Otherwise we need to have dirty hacks like these.

Polymorphism in JSON, as I understand it, is basically a reserved object property (ideally configurable) which value defines what concrete instance to instantiate. Well known libraries like Jackson do this, and I think this covers more use cases.

If we're defining polymorphism on associations we're never going to get polymorphism in root objects, without associations to them. A children hasMany polymorphic collection of models shouldn't be different from querying a base polymorphic collection of models.

Am I missing something?

miguelcobain commented Nov 26, 2013

I never really understood why does polymorphism support is added on associations.
Wouldn't it be easier to add it on Model itself?

Otherwise we need to have dirty hacks like these.

Polymorphism in JSON, as I understand it, is basically a reserved object property (ideally configurable) which value defines what concrete instance to instantiate. Well known libraries like Jackson do this, and I think this covers more use cases.

If we're defining polymorphism on associations we're never going to get polymorphism in root objects, without associations to them. A children hasMany polymorphic collection of models shouldn't be different from querying a base polymorphic collection of models.

Am I missing something?

@lolmaus

This comment has been minimized.

Show comment
Hide comment
@lolmaus

lolmaus Jan 1, 2015

Contributor

Is this feature documented? I struggle to understand how it works, but i failed to find a single working JSBin demo.

Contributor

lolmaus commented Jan 1, 2015

Is this feature documented? I struggle to understand how it works, but i failed to find a single working JSBin demo.

@lolmaus

This comment has been minimized.

Show comment
Hide comment
@lolmaus

lolmaus Jan 1, 2015

Contributor

Okay, i got it working for a polymorphic hasMany: http://emberjs.jsbin.com/rivav/4/

And here's a working example with the DS.EmbeddedRecordsMixin: http://emberjs.jsbin.com/rivav/8/edit?html,js,output

Thank you for the awesome functionality! ^_^

Contributor

lolmaus commented Jan 1, 2015

Okay, i got it working for a polymorphic hasMany: http://emberjs.jsbin.com/rivav/4/

And here's a working example with the DS.EmbeddedRecordsMixin: http://emberjs.jsbin.com/rivav/8/edit?html,js,output

Thank you for the awesome functionality! ^_^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment