Ember Data: Polymorphic Associations



Ember Data: Polymorphic Associations

1 3


ember_data_polymorphic_presentation


On Github lukegalea / ember_data_polymorphic_presentation

Ember Data: Polymorphic Associations

Created by Luke Galea / @lukegalea

  • Introduction
  • Agenda
    • Primer
    • Crazy / Unexpected use cases
    • Problems / Solutions

Nutrition and fitness coaching

  • CTO at PN
  • Nutrition + Fitness Coaching

"We use ED Polymorphic assocations a lot." - Me

  • Problem domain well suited?
  • Learning Management System
  • Core tech that drives our system
  • 3rd talk so far
    • 1st time: cautionary tale
    • 2nd time: help navigate minefield
    • now: Actually pretty much works / has solutions to all problems

What are polymorphic associations?

"A relationship from one class to multiple classes."
Person = DS.Model.extend({
  name: attr('string'),
  neckbeard: true
});

Average ember developer

Person = DS.Model.extend({
  name: attr('string'),
  neckbeard: true,
  pets: hasMany('pet', { polymorphic: true })
});
Person = DS.Model.extend({
  name: attr('string'),
  neckbeard: true,
  pets: hasMany('pet', { polymorphic: true })
});

Pet = DS.Model.extend({
  owner: belongsTo('person')
});

Cat = Pet.extend({
  lives: attr('number', {defaultValue: 9})
});
App.Person = DS.Model.extend({
  name: attr('string'),
  neckbeard: true,
  pets: hasMany('pet', { polymorphic: true })
});

Pet = DS.Model.extend({
  owner: belongsTo('person')
});

Cat = Pet.extend({
  lives: attr('number', {defaultValue: 9})
});

Dog = Pet.extend({
  fleas: attr('number', {defaultValue: 0})
});
App.Person = DS.Model.extend({
  name: attr('string'),
  neckbeard: true,
  pets: hasMany('pet', { polymorphic: true })
});

Pet = DS.Model.extend({
  owner: belongsTo('person')
});

Cat = Pet.extend({
  lives: attr('number', {defaultValue: 9})
});

Dog = Pet.extend({
  fleas: attr('number', {defaultValue: 0})
});

HoneyBadger = Pet.extend({
  viciousness: attr('number', {defaultValue: 0})
});

As of Ember-Data 1.0b11

a common base class is required

App.Pet = DS.Model.extend({
  ...
});

App.Cat = App.Pet.extend({
  ...
});

App.Dog = App.Pet.extend({
  ...
});

There's a Pull Request open to allow unrelated polymorphic types

Assuming that covers the ember side, how do you actually wire it up with a backend?

  • Goal for a simple implementation is request like these
  • Thinking: JSON-API
  • No, we can't let things like warnings from experts in the field
  • or lack of documentation deter us.

{
  "drawing":{
    "id":1,
    "title":"A house",
    "shapes":[
      {
        "id":1,
        "type":"Rectangle"
      },
      {
        "id":2,
        "type":"Rectangle"
      },
      {
        "id":3,
        "type":"Triangle"
      }
    ]
  }
}
  • Demo App
  • Ember 1.8, Ember-Data 1.0 Beta 11
  • (Always use beta E-D)
  • Rails backend

Or for a belongsTo

{
  "dog":{
    ...
    "owner": {
      "id":1,
      "type":"Human"
    }
}
  • Time to implement server side.
  • thinking "I got this one"
  • ActiveModelSerializer supports polymorphic
  • It used to support it (in 0.9)
  • 0.8 (stable) does not
  • Master is based off 0.8
  • If you were using 0.9
  • Otherwise - hack it!

  class DrawingSerializer < ActiveModel::Serializer
    attributes :id, :title, :shapes

    def shapes
      object.shapes.map { |e| { id: e.id, type: e.type } }
    end
  end

belongsTo

  class DogSerializer < ActiveModel::Serializer
    ...
    def owner
      { id: object.owner.id, type: object.owner.type } if object.owner
    end
  end
There's a guide that shows how to backport polymorphic true from 0.9 to 0.8 Assuming you've wired it all up, everything should be good!

Cool Stuff

Composing views using polymorphic models

  • The form builder
  • Every few years I find myself building another
  • Every time it gets better
  • Share in common: Forms has many "things"
  • All different
  • Pull them together to compose an interface

A student curriculum is composed of many types of Cards.

A Card is composed of many primitive elements.

DEMO

A drawing is made of many shapes:

App.Drawing = DS.Model.extend({
  shapes: belongsTo('shape', { polymorphic: true })
});

App.Shape = DS.Model.extend({
  x: DS.attr('number'),
  y: DS.attr('number'),
  color: DS.attr('string')
});

App.Rectangle = App.Shape.extend({
  height: DS.attr('number'),
  width: DS.attr('number')
});

App.Circle = App.Shape.extend({
  radius: DS.attr('number')
});
  • Given the models, we need a way to render the drawing
Shape = DS.Model.extend({

  shapeType: function() {
    this.constructor.typeKey
  }.property()

});

Rendering views dynamically based on type

<p>
  <label>{{name}}</label>
  {{view view.elementViewType}}
</p>

A helper to dynamically render a component

  var H = Em.Handlebars;

  H.registerHelper('renderComponent',
    function(componentPath, options) {
      var component = H.get(this, componentPath, options),
          helper = H.resolveHelper(options.data.view.container, component);

      helper.call(this, options);
    }
  );
  • Poof! Time for another cool thing

Polymorphic Endpoints

this.store.find('shapes').then(function(shapes) {
  // ?
});
  • You are thinking: sweet
  • Try it out..

  • Notice: The shapes are Shapes

  • The "Rectangle"s are unfullfilled promises
  • What do we do?
  class RectangleSerializer < ActiveModel::Serializer
    attributes ..., :type
  end
  • This does nothing, but it gives us enough data to solve
  • E-D decides on the model based on the "root" json key
  • So "shapes" -> Shape
Store = DS.Store.extend({
  push: function(type, data, _partial) {
    var dataType, modelType, oldRecord, oldType;

    modelType = oldType = type;
    dataType = data.type;

    if (dataType && (this.modelFor(oldType) !== this.modelFor(dataType))) {
      modelType = dataType;

      if (oldRecord = this.getById(oldType, data.id)) {
        this.dematerializeRecord(oldRecord);
      }
    }

    return this._super(this.modelFor(modelType), data, _partial);
  }
});
  • Hack the store so it uses the "type" key if it exists
  • Yes, we are making the type key "magic"
  • Like Rails
  • Hooray: We are no less brittle than Rails

Bootstrap Endpoints

this.store.find('bootstrap')
  • Unexpected consequence
  • Allow us to return a bunch of unrelated data
  • For example, to bootstrap an app
  • If you are thinking that's crazy..

Different adapters per type

API can serve references to other APIs

Contrived Example!

{
  "user":{
    ...
    "avatar": {
      "id":"luke@precisionnutrition.com",
      "type":"gravatar"
    }
}
{
  "user":{
    ...
    "avatar": {
      "id":"luke@precisionnutrition.com",
      "type":"s3"
    }
}

The Badness

  • You'll be happy to know that most of the badness has workarounds

Tight coupling between API and Ember-Data

{
  "drawing":{
    "id":1,
    "title":"A house",
    "shapes":[
      {
        "id":1,
        "type":"Rectangle"
      },
      {
        "id":2,
        "type":"Rectangle"
      },
      {
        "id":3,
        "type":"Triangle"
      }
    ]
  }
}

QUADRANGLE

QUADRANGLE

  • Forced to work with an API
  • Where I come from we call them QUADrangles!
  • In the past this meant compromise, but not anymore!

Map type key to ember model name

typeMap = {
  quadrangle: 'rectangle',
  rectangle: 'quadrangle'
};

mapType = function(key) {
  return typeMap[key.underscore()] || key;
};

Endpoint Mapping

this.store.find('quadrangle', 1);

/quadrangles/1 → /rectangles/1

  • 1st problem
  • E-D doesn't know where to find quadrangles in the API

Custom Adapter

ApplicationAdapter = DS.ActiveModelAdapter.extend({
  pathForType: function(type) {
    return this._super( mapType(type) );
  }
});

/quadrangles/1 → /rectangles/1

JSON Mapping

  {
    "rectangle": {
      "id": "1",
      ....
    }
  }

quadrangle

  • 2nd problem
    • Given this json root key
    • ED is going to want to create rectangles

Custom Serializer

ApplicationSerializer = DS.ActiveModelSerializer.extend({

  typeForRoot: function(key) {
    return this._super( mapType(key) );
  },

  serializeIntoHash: function(data, type, record, options) {
    var key, typeKey;
    typeKey = type.typeKey;
    key = mapType(typeKey);
    return data[key] = this.serialize(record, options);
  }

});

↑   JSON → Model ↑   Model → JSON

  • We did it!
  • Type key, no longer brittle
  • No longer fraught with problems

Mission Accomplished?

{
   "drawing":{
      "id":1,
      "title":"A house",
      "shapes":[
         { "id":1,
           "type":"Rectangle"
         },
         { "id":4,
           "type":"Circle"
         }
      ]
   }
}

  • This problem is not unique to polymorphic associations
  • (Sideloading, etc)

3 seperate requests

({Number of types} + 1)

  • Similar to N+1 problem on server side

This can add up pretty quickly

True story

But at least they are executed concurrently

  • On the server side you would solve this with "includes"
  • in E-D you can solve this with sideloading
  • but it's a bit tougher because AMS doesn't support polymorphism
  • HACK it!

Sideload

  class DrawingSerializer < ActiveModel::Serializer
    .......

    has_many :triangles,  include: true, embed: :ids
    has_many :rectangles, include: true, embed: :ids
  end
  • Manually maintain a list of all possible sub-types
  • Not very elegant or DRY

{
  "drawing":{
    ......
    "triangle_ids":[3],    // Redundant
    "rectangle_ids":[1, 2] // Redundant
  },
  "triangles":[
    {
      "id":3,
      "x":0,
      "y":0,
      "fill":"blue",
      "width":"200",
      "height":"100",
      "drawing_id":1
    }
  ],
  "rectangles":[
    .....
  ]
}

STI?

  • Similarly, we can look to the server side solution
  • Single Table Inheritance
  • A single API endpoint for all types

Bestra's STI Guide

https://github.com/Bestra/ember-data-sti-guide

  • Great guide on this.
  • Really complicated. Decide if it's worth it
Ember.MODEL_FACTORY_INJECTIONS = true;
  • Default in Ember-CLI today
  • Allows dependency injection into models

  • Stems from the check for common baseclass
  • slated for removal?
  • Given they are descendent, what is going on?

doubleplusungood

Ember.assert(..., record instanceof this.type);
  • When using MFI, instanceof doesn't work

rectified

App.Shape.detect(myRectangle.constructor)

Pull Request

Thanks

Presentation

http://lukegalea.github.io/ember_data_polymorphic_presentation

 

Demo App

https://github.com/PrecisionNutrition/ember-data-polymorphic-full-stack-example