© The Author(s), under exclusive license to APress Media, LLC , part of Springer Nature 2021
D. TangPro Ember Datahttps://doi.org/10.1007/978-1-4842-6561-1_13

13. Polymorphic Relationships

David Tang1  
(1)
Playa Vista, CA, USA
 

In this chapter, we will look at polymorphism and how it relates to Ember Data. If we look up the definition of polymorphism as it relates to programming, we’ll see it described as “the ability of an object to take on many forms.” For example, a product could come in the form of a book or a course. In an Ember application, we would have a book model and a course model that extend from a base product model. If we extend the definition of polymorphism to relationships in our Ember Data models, it means our relationships can hold objects of different types that share a common interface, like inheriting from the same product model. Let’s look at the two examples we’ll be covering in this chapter.

Example 1 – Purchased Products

Imagine we have a site where users can purchase different products like courses and books. We would create a course model and a book model that extend from a base product model. We would also have a user model to represent the current user. Typically with these types of sites, there is a page that lists all of the products that the authenticated user has purchased. We could express the relationship between a user and the various products that they purchased as a hasMany relationship.

Going back to the definition of polymorphism, a user’s purchased products can take on multiple forms such as a course or a book. This is an example of a polymorphic hasMany relationship, because the relationship can contain records of multiple types that all inherit from a base product class.

Example 2 – Content with Comments

Let’s say our application also offers content in the form of blog posts and videos. We want users to be able to comment on each of these types of content. As such, we’ll have a post model and a video model that extend from a base content model. We’ll also have a comment model. Posts and videos can have many comments. A comment however can belong to a single piece of content, which in our application can be a post or a video. We could express this relationship between the comment and the content as a polymorphic belongsTo relationship, because the content associated with a comment can be of different types as long as the record inherits from our base content class.

Now that we have a few conceptual examples, let’s see how we can implement this data model in Ember Data.

Setup

I have created a simple application that implements the preceding examples. The code for this application can be found in the chapter-13-restserializer folder of the source code for this book. We will use this application as a way of testing our model definitions and polymorphic relationships.

Similar to previous chapters, the API is mocked using Ember CLI Mirage.1 In this chapter, our mock API will be following the RESTSerializer conventions, but the patterns and techniques covered also apply to APIs that follow the JSONSerializer and JSONAPISerializer conventions with some minor differences. If you’d like to see the code for the application when using the JSONSerializer, take a look at the chapter-13-jsonserializer folder of the source code for this book. If you’d like to see the code for the application when using the JSONAPISerializer, take a look at the chapter-13-jsonapiserializer folder of the source code for this book.

Polymorphic “hasMany” Relationships

Let’s start by defining the models for Example 1 which include a product model, a course model, and a book model:
app/models/product.js
import Model, { attr } from '@ember-data/model';
export default class ProductModel extends Model {
  @attr('string') title;
}
app/models/course.js
import ProductModel from './product';
import { attr } from '@ember-data/model';
export default class CourseModel extends ProductModel {
  @attr('string') length;
}
app/models/book.js
import ProductModel from './product';
import { attr } from '@ember-data/model';
export default class BookModel extends ProductModel {
  @attr('number') pages;
}

As mentioned earlier, our course and book models will extend from a base product model which contains functionality common among all products, like having a title attribute.

Earlier we discussed that a user can have many purchased products. We can implement this relationship as a hasMany relationship :
app/models/user.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class UserModel extends Model {
  @hasMany('product', {
    polymorphic: true,
    async: false
  })
  purchasedProducts;
}

The purchasedProducts relationship is a standard hasMany relationship but with a polymorphic option. This will allow us to store records of different types as long as they extend from the product model. I have made this a synchronous relationship in the example application, but the relationship could have been asynchronous.

Next, let’s look at the expected response payload if we were to fetch a user who has purchased products. Again, our mock API will be following the RESTSerializer conventions.

In our sample application, we have the following endpoint to return the current user:
mirage/config.js
this.get('/users/:id', function () {
  return {
    users: {
      id: '1',
      name: 'David',
      purchasedProducts: [
        { id: '5', type: 'course' },
        { id: '10', type: 'book' }
      ]
    }
  };
});

As we learned in Chapter 3 – API Response Formats and Serializers, when using the RESTSerializer or the JSONSerializer , hasMany relationships should be represented in API payloads as an array of IDs. In order to use polymorphic relationships, purchasedProducts must be an array of JSON:API Resource Identifier Objects, that is, objects with id and type properties. The type property corresponds to the name of an Ember Data model in either the singular or plural form, which in this case is either course or book.

With this API response, our purchasedProducts hasMany relationship will be polymorphic. That is, purchasedProducts can contain course records and book records.

As we learned in Chapter 1 – Ember Data Overview, an error will be thrown if we access the purchasedProducts relationship and the related records aren’t loaded into the store because the relationship has been declared as synchronous. In the example application, we have the following endpoint to load the related course and book records into the store:
mirage/config.js
this.get('/products', function () {
  return {
    courses: [
      {
        id: '5',
        title: 'Introduction to Ember Octane',
        length: '2 hours'
      }
    ],
    books: [
      {
        id: '10',
        title: 'Ember Data in the Wild',
        pages: 100
      }
    ]
  };
});
We could also return these resources under a single products key when using the RESTSerializer :
mirage/config.js
this.get('/products', function () {
  return {
    products: [
      {
        id: '5',
        type: 'course',
        title: 'Introduction to Ember Octane',
        length: '2 hours'
      },
      {
        id: '10',
        type: 'book',
        title: 'Ember Data in the Wild',
        pages: 100
      }
    ]
  };
});

The only requirement here is that we need to include a type property in each product resource, which corresponds to the model name of a “product” subclass.

Polymorphic “belongsTo” Relationships

Now let’s see how we can implement the polymorphic belongsTo relationship in Example 2. We’ll start by defining content, post, and video models:
app/models/content.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class ContentModel extends Model {
  @attr('string') title;
  @hasMany('comment', { async: false }) comments;
}
app/models/post.js
export default class PostModel extends ContentModel {
  @attr('number') wordCount;
}
app/models/video.js
import { attr } from '@ember-data/model';
import ContentModel from './content';
export default class VideoModel extends ContentModel {
  @attr('number') length;
}

Our post and video models will extend from a base content model which contains the comments relationship.

A comment will belong to a single piece of content, which can be either a post or a video. We can implement this relationship as a polymorphic belongsTo relationship as follows:
app/models/comment.js
import Model, { attr, belongsTo } from '@ember-data/model';
export default class CommentModel extends Model {
  @attr('string') body;
  @belongsTo('content', {
    polymorphic: true,
    async: false
  })
  content;
}

The content relationship is a standard belongsTo relationship but with a polymorphic option. This will allow us to assign different types of content records to the content relationship as long as those records inherit from the content model.

Next, let’s look at the expected response payload for an endpoint that returns all of a user’s comments:
mirage/config.js
this.get('/comments', function () {
  return {
    comments: [
      {
        id: '1',
        body: 'Looking forward to using all the new features!',
        content: {
          id: '1',
          type: 'post'
        }
      },
      {
        id: '2',
        body: 'Great video! Please make more!',
        content: {
          id: '2',
          type: 'video'
        }
      }
    ],
    posts: [
      {
        id: '1',
        title: 'Ember Octane Released',
        wordCount: 300,
        comments: ['1']
      }
    ],
    videos: [
      {
        id: '2',
        title: 'Introduction to Ember.js',
        length: 300,
        comments: ['2']
      }
    ]
  };
});

Notice how each resource under comments contains a JSON:API Resource Identifier Object under the content property. Similar to the polymorphic hasMany relationship, the id and type will be used to establish a belongsTo relationship with the correct content subclass record.

Customizing Polymorphic Relationship Serialization

In the last two sections, we learned how to define our models and the expected response formats so that hasMany and belongsTo polymorphic relationships get wired up correctly. Now we will look at how to customize a serializer for when an API doesn’t follow the default conventions for polymorphic relationships.

In the sample application, head over to http://localhost:4200/recent-comments and add a comment to one of the items. If you open up the browser console, you will see a request payload with a similar structure to the following:
{
  "comment": {
    "body": "My new comment",
    "content": "1",
    "contentType": "post"
  }
}

This serialized payload is similar to what we’d expect when serializing any belongsTo relationship, except there is a new key, contentType. The default behavior of the RESTSerializer when serializing polymorphic belongsTo relationships is to create a key where “Type” is appended to the name of the polymorphic relationship. In this case, our polymorphic relationship is named content so the new key becomes contentType.

Let’s say we wanted the polymorphic key to be type instead of following the pattern “***Type”. We could override the keyForPolymorphicType() method in a serializer:
app/serializers/application.js
export default class ApplicationSerializer extends RESTSerializer {
  keyForPolymorphicType(key, typeClass, method) {
    return 'type';
  }
}
Now the request payload will look like the following:
{
  "comment": {
    "body": "My new comment",
    "content": "1",
    "type": "post"
  }
}
Maybe we would like the request payload to include a JSON:API Resource Identifier Object instead so that there is symmetry between request and response payloads, for example:
{
  "comment": {
    "body": "My new comment",
    "content": {
      "id": "1",
      "type": "post"
    }
  }
}
We can achieve this by overriding the serializePolymorphicType() method:
app/serializers/application.js
export default class ApplicationSerializer extends RESTSerializer {
  serializePolymorphicType(snapshot, json, relationship) {
    super.serializePolymorphicType(snapshot, json, relationship);
    let { name } = relationship.meta;
    json[name] = {
      id: json[name],
      type: json[`${name}Type`]
    };
    delete json[`${name}Type`];
  }
}
In the preceding code, we are calling super.serializePolymorphicType() to perform the default serialization behavior. This will result in the json variable containing an object with a similar structure to the following:
{
  comment: {
    body: 'My new comment',
    content: '1',
    contentType: 'post'
  }
}

Then, we override the comment.content property to be { id: '1', type: 'post' } and delete the comment.contentType property.

As we learned in previous chapters, we could also add this logic to a serializer’s serialize() method, but using the keyForPolymorphicType() and serializePolymorphicType() methods will likely reduce the amount of code needed for polymorphic relationship serialization.

Customizing Polymorphic Relationship Normalization

Imagine the API serialized the polymorphic content relationship in each comment resource in the form type:id. If this were the case in our sample application, GET /comments would return the following JSON:
{
  "comments": [
    {
      "id": "1",
      "body": "Looking forward to using all the new features!",
      "content": "post:1"
    },
    {
      "id": "2",
      "body": "Great video! Please make more!",
      "content": "video:2"
    }
  ],
  "posts": [
    {
      "id": "1",
      "title": "Ember Octane Released",
      "comments": ["1"]
    }
  ],
  "videos": [
    {
      "id": "2",
      "title": "Introduction to Ember.js",
      "comments": ["2"]
    }
  ]
}

I know this example is a bit contrived, but you never know what kinds of APIs you’ll see in the wild! Before, content contained a JSON:API Resource Identifier Object such as { "id": "1", "type": "post" }. How can we normalize our polymorphic content relationship? We can override one of the many normalization methods that we covered in previous chapters, or we can override the extractPolymorphicRelationship() method. This method gets called for relationships declared with the polymorphic: true option and returns a JSON:API Resource Identifier Object.

Let’s assume our polymorphic relationships follow this custom convention and the standard convention. We can override extractPolymorphicRelationship() in our application serializer to account for both scenarios:
app/serializers/application.js
export default class ApplicationSerializer extends RESTSerializer {
  extractPolymorphicRelationship(
    relationshipType,
    relationshipHash,
    relationshipOptions
  ) {
    if (typeof relationshipHash === 'string') {
      let [type, id] = relationshipHash.split(':');
      return { id, type };
    }
    return super.extractPolymorphicRelationship(...arguments);
  }
}

In the implementation earlier, relationshipHash will contain post:1 and video:2 for the preceding response. We can split on the colon and create a new object that matches the structure of a JSON:API Resource Identifier Object and return that in the scenario where our custom polymorphic relationship convention is used. Otherwise, we will let the default polymorphic extraction behavior run via super.extractPolymorphicRelationship(...arguments).

Summary

Polymorphism is a powerful concept that allows us to have models that relate to each other through inheritance, and Ember Data relationships can support this. This ultimately allows a belongsTo relationship to contain records of different types through a single relationship as long as those records inherit from a common base class. This also allows for a hasMany relationship to contain records of multiple types as long as those records inherit from a common base class.

In this chapter, we looked at how to set up our models and API to support polymorphic relationships. We then looked at how we can customize serialization and normalization of polymorphic relationships if our API doesn’t follow the default conventions that Ember Data expects. Even though we looked at the implementation of polymorphic relationships with the RESTSerializer, the model definitions are the same if you are using one of the other built-in serializers. Additionally, the serializer methods that we overrode are the same, except the JSONSerializer and JSONAPISerializer don’t support the keyForPolymorphicType() method. There are also some slight differences in the default API conventions for the JSONSerializer and the JSONAPISerializer. Despite these minor differences, the patterns and techniques covered in this chapter should equip you to handle polymorphic relationships regardless of which serializer your application is using.

Well, this is the end. Thanks for reading my book! Hopefully, this has helped you and has put you in a good position to work with Ember Data and any custom API. If you have any questions, please reach out to me on Twitter @iamdtang or by email at [email protected]. I would love to hear about your experiences with Ember Data. You can also stay up to date with my Ember adventures on my blog at https://davidtang.io.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.121.55