Page Contents
REVIEW COMMENT from Yaapa
The docs for "Polymorphic relations", "HasManyThrough relations", and "Embedded models and relations" all share common areas of improvement.
  • Some example files lack context.
  • There are references to objects in the example files, without any explanation of where they came from or how they may be initialized.
  • There are certain properties in the objects in examples files, they need to be explained, and the product informed of other possible properties. For example, here are my observations on Polymorphic relations

common/models/employee.json: Should explain what "imageable" in { "polymorphic": "imageable" } means. And, list other possible properties.

common/models/product.json:

   "polymorphic": {
    "as": "imageable",
    "foreignKey": "imageableId",
    "discriminator": "imageableType"
   }
- The structure has changed from "common/models/employee.json". Should explain "as", "foreignKey", and "discriminator". And, list other possible properties.
common/models/employee.js - Where does the Picture object come from?
common/models/picture.js - belongsTo is undefined in Picture.
common/models/model.js - Is this file created by default? Where do Employee, Product, Picture objects come from?
As mentioned in the beginning, "HasManyThrough relations" and "Embedded models and relations" have similar issues. I can come up with a wholesome example which includes all these three types of model relations coming week.

LoopBack supports polymorphic relations in which a model can belong to more than one other model, on a single association. For example, you might have a Picture model that belongs to either an Employee model or a Product model. 

The examples below use three example models: Picture, Employee, and Product, where a picture can belong to either an employee or a product.

REVIEW COMMENT from Rand

Is it actually "... a picture can belong to both an employee and a product." ?

Do polymorphic relations add methods to the models as the standard relations do? If so, what?

HasMany polymorphic relations

Take the following scenario as example:

A Picture model belongs to either an Employee model or a Product model

A hasMany polymorphic relation means:

  • There are two properties created in model Picture, one serves as a discriminator, which states what model the picture belongs to, and the other one serves as a foreignKey, which is the id(or primaryKey) value of either the employee or product.

  • There are CRUD apis added to modelFrom(Employee and Product), by which you can create/modify/delete modelTo instance that belongsTo it. For example, employee.pictures.create() creates a new picture belongsTo employee.

If you want to have the api which retrieves the model that a modelTo instance belongsTo, which in our case is picture.imageable(), make sure you also define the corresponding belongsTo relation described in the next section BelongsTo polymorphic relations

Parameters for the definition

  • type - the relation type, in this case is ‘hasMany’
  • as - redefines this relation’s name (optional)
  • model - name of modelTo
  • polymorphic

    typeof polymorphic === Object(complete declaration)

    • selector(suggested) or as - matching belongsTo relation name

      (required) if both foreignKey and discriminator are NOT provided

      (extraneous) throws error if BOTH foreignKey and discriminator are provided

    • foreignKey: A property of modelTo, representing the fk to modelFrom’s id

      generated by default as as + 'Id'

    • discriminator: A property of modelTo, representing the actual modelFrom to be looked up and defined dynamically

      generated by default as as + 'Type'

    typeOf polymorphic === String(shorthand declaration)

    matching belongsTo relation name

    foreignKey is generated as polymorphic + 'Id'

    discriminator is generated as polymorphic + 'Type'

Please note as inside polymorphic object will be deprecated in LoopBack 4, we suggest use selector.

In the following example, model ‘Employee’ defines the relation with a shorthand declaration, and model ‘Product’ defines it with a complete polymorphic object declaration.

common/models/employee.json

{
  "name": "Employee",
  "base": "PersistedModel",
  ...
  "relations": {
    "pictures": {
      "type": "hasMany",
      "model": "Picture",
      "polymorphic": "imageable"
    }
  }
...

And:

common/models/product.json

{
  "name": "Product",
  "base": "PersistedModel",
  ...
  "relations": {
    "pictures": {
      "type": "hasMany",
      "model": "Picture",
      "polymorphic": {
        "foreignKey": "imageableId",
        "discriminator": "imageableType"
       } 
    }
  }
...

// Alternately provide `selector` in `polymorphic` object
{
  "name": "Product",
  "base": "PersistedModel",
  ...
  "relations": {
    "pictures": {
      "type": "hasMany",
      "model": "Picture",
      "polymorphic": {
        "selector": "imageable"
       } 
    }
  }
...

Alternatively, you can define the relation in code:

common/models/employee.js

Employee.hasMany(Picture, { polymorphic: 'imageable' });

And:

common/models/product.js

Product.hasMany(Picture, { polymorphic: {
  foreignKey: 'imageableId',
  discriminator: 'imageableType'
  } 
});

// Alternative use `selector`

Product.hasMany(Picture, { polymorphic: {
  selector: "imageable"
  } 
});

BelongsTo polymorphic relations

Because you define the related model dynamically, you cannot declare it up front. So, instead of passing in the related model (name), you specify the name of the polymorphic relation.

To define a belongsTo polymorphic relation, you need to provide the following parameters:

  • type: the relation type, in this case is ‘belongsTo’
  • as: redefines this relation’s name (optional)
  • polymorphic:

    typeOf polymorphic === Object * foreignKey: A property of modelTo, representing the fk to modelFrom’s id. * discriminator: A property of modelTo, representing the actual modelFrom to be looked up and defined dynamically. typeOf polymorphic === Boolean * foreignKey is generated as relationName + 'Id', * discriminator is generated as relationName + 'Type'

Please note:

Do not provide model field in relation definition, if you define it, LoopBack throws an error as relation validation.

Do not provide selector or as inside polymorphic object.

common/models/picture.json

{
  "name": "Picture",
  "base": "PersistedModel",
  ...
  "relations": {
    "imageable": {
      "type": "belongsTo",
      "polymorphic": true
    }
  },
...

// Alternatively, use an object for setup

{
  "name": "Picture",
  "base": "PersistedModel",
  ...
  "relations": {
    "imageable": {
      "type": "belongsTo",
      "polymorphic": {
        "foreignKey": "imageableId",
        "discriminator": "imageableType"
      }
    }
  },
...

Or, in code:

common/models/picture.js

Picture.belongsTo('imageable', {
  polymorphic: true
}); 
// Alternatively, use an object for setup
Picture.belongsTo('imageable', {
  polymorphic: {
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  }
});

HasOne polymorphic relations

The difference between hasOne and hasMany is that, hasOne represents a “one-to-one” relation while hasMany represents a “one-to-many” relation. For details, refer to hasMany relations and hasOne relations.

The relation definitions in ‘HasOne’ is almost same as ‘HasMany’, the only difference is type should be ‘hasOne’, for details, refer to HasMany polymorphic relations.

The following code shows how to dynamically define a ‘HasOne’ polymorphic relation, and also redefines the relation name with as: you can specify as: 'avatar' to explicitly set the name of the relation. If not set, it defaults to the polymorphic relation name.

/common/models/model.js

Picture.belongsTo('imageable', {polymorphic: true});
Employee.hasOne(Picture, {as: 'avatar', polymorphic: 'imageable'});
Product.hasOne(Picture, {polymorphic: {as: 'imageable'}});

// To create a picture belongs to an employee, you can use the method below
employee.avatar.create();

HasManyThrough polymorphic relations

To define a hasManyThrough polymorphic relation, there must be a “through” model, for example:

Employee hasMany Picture through ImageLink polymorphically

Product hasMany Picture through ImageLink polymorphically

A hasManyThrough polymorphic relation means the through model has three properties created. The first two are same as those created in the toModel in a hasMany polymorphic relation: one is discriminator(the value is either Employee or Product) and the other one is foreignKey(the id(primarykey) value of either an employee or a product). The third property is a foreignKey property reference model Picture.

Then here’s an example of a polymorphic hasManyThrough relation:

First define relations in through model ImageLink.

/common/models/ImageLink.json

{
  "name": "ImageLink",
  "base": "PersistedModel",
  ...
  "relations": {
    "picture": {
      "type": "belongsTo",
      "model": "Picture",
      "foreignKey": ""
    },
    "imageable": {
      "type": "belongsTo",
      "polymorphic": true
    }
  }
...
}

Then in the Employee(or Product) model.

/common/models/Employee.json

{
  "name": "Employee",
  "base": "PersistedModel",
  ...
  "relations": {
    "pictures": {
      "type": "hasMany",
      "model": "Picture",
      "through": "ImageLink",
      "polymorphic": "imageable"
    }
  },
...
}

OPTIONAL in model Picture.

/common/models/Picture.json

// Optional, define invert hasMany relation in Picture
{
  "name": "Picture",
  "base": "PersistedModel",
  ...
  "relations": {
    "employees": {
      "type": "hasMany",
      "model": "Employee",
      "through": "ImageLink",
      "invert": true,
      "polymorphic": "imageable"
    },
    "products": {
      "type": "hasMany",
      "model": "Product",
      "through": "ImageLink",
      "invert": true,
      "polymorphic": "imageable"
    }
  },
...
}

Equivalently, in JavaScript:

/server/boot/boot-script.js

Employee.hasMany(Picture, {
  as: 'pictures',
  polymorphic: {
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  },
  through: ImageLink
});
Product.hasMany(Picture, {
  as: 'pictures',
  polymorphic: {
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  },
  through: ImageLink
});
ImageLink.belongsTo(Picture, {});
ImageLink.belongsTo(ImageLink, {polymorphic: true});

// Optional define invert hasMany relation in Picture
Picture.hasMany(Employee, {through: ImageLink, polymorphic: 'imageable', invert: true});
Picture.hasMany(Product, {through: ImageLink, polymorphic: 'imageable', invert: true});

HasAndBelongsToMany polymorphic relations

hasAndBelongsToMany polymorphic relation is similar to hasManyThrough polymorphic relation. It also requires an explicit ‘through’ model, in our example: ImageLink

The difference between them is that, hasAndBelongsToMany will automatically setup belongsTo relations in through model: ImageLink.belongsTo(Picture, {}) and ImageLink.belongsTo(ImageLink, {polymorphic: true}).

/common/models/model.js

Employee.hasAndBelongsToMany(Picture, {
  through: PictureLink,
  polymorphic: 'imageable'
});
Product.hasAndBelongsToMany(Picture, {
  through: PictureLink,
  polymorphic: 'imageable'
});
// Optionally, define inverse hasMany relations with '(invert: true)'
Picture.hasMany(Employee, {
  through: PictureLink,
  polymorphic: 'imageable',
  invert: true
});
Picture.hasMany(Product, {
  through: PictureLink,
  polymorphic: 'imageable',
  invert: true
});

Dealing with polymorphic.idType

Because modelTo is unknown up-front (it’s polymorphic), you cannot rely on modelTo for getting the foreignKey type.  You can explicitly declare the idType as shown below. 

The example below should provide the following results:

[{
  url: 'john.jpg',
  imageableType: 'Employee',
  imageableId: '1',
  id: 1
}, {
  url: 'joe.jpg',
  imageableType: 'Product',
  imageableId: '1',
  id: 2
}]

Employees: [{
  username: 'John',
  id: 1
}]

Products: [{
  name: 'Joe',
  id: 1
}]
var Employee = app.models.Employee;
var Product = app.models.Product;
var Picture = app.models.Picture;

Employee.hasOne(Picture, {
  as: 'avatar',
  polymorphic: {
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  }
});

Product.hasOne(Picture, {
  as: 'imageable',
  polymorphic: {
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  }
});

Picture.belongsTo('owner', {
  idName: 'username',
  polymorphic: {
    idType: Employee.definition.properties.username.type,
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  }
});

//Creating demo employee, product pictures then listing them
function createEmployee(cb) {
  Employee.create({
    username: "John"
  }).then(function(employee) {
    employee.avatar.create({
      url: "john.jpg"
    }, function() {
      cb();
    });
  });
}

function createProduct(cb) {
  Product.create({
    name: "Joe"
  }).then(function(product) {
    product.imageable.create({
      url: "joe.jpg"
    }, function() {
      cb();
    });
  });
}

function listPictures() {
  Picture.find(function(err, res) {
    console.log("\nPictures:\n", res);
  })
}

function listProducts() {
  Product.find(function(err, res) {
    console.log("\nProducts:\n", res);
  })
}

function listEmployees() {
  Employee.find(function(err, res) {
    console.log("\nEmployees:\n", res);
  })
}

//executing the demo
createEmployee(function() {
  createProduct(function() {
    listPictures();
    listEmployees();
    listProducts();
  });
});