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 reader informed of other possible properties. For example, here are my observations on Polymorphic relations

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

common/models/reader.json:

   "polymorphic": {
    "as": "imageable",
    "foreignKey": "imageableId",
    "discriminator": "imageableType"
   }
- The structure has changed from "common/models/author.json". Should explain "as", "foreignKey", and "discriminator". And, list other possible properties.
common/models/author.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 Author, Reader, 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 Author model or a Reader model. 

The examples below use three example models: Picture, Author, and Reader, where a picture can belong to either an author or a reader.

REVIEW COMMENT from Rand

Is it actually "... a picture can belong to both an author and a reader." ?

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 Author model or a Reader 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 author or reader.

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

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

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

  • 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 ‘Author’ defines the relation with a shorthand declaration, and model ‘Reader’ defines it with a complete polymorphic object declaration.

common/models/author.json

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

And:

common/models/reader.json

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

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

Alternatively, you can define the relation in code:

common/models/author.js

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

And:

common/models/reader.js

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

// Alternative use `selector`

Reader.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});
Author.hasOne(Picture, {as: 'avatar', polymorphic: 'imageable'});
Reader.hasOne(Picture, {polymorphic: {as: 'imageable'}});

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

HasManyThrough polymorphic relations

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

Author hasMany Picture through ImageLink polymorphically

Reader 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 Author or Reader) and the other one is foreignKey(the id(primarykey) value of either an author or a reader). 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 Author(or Reader) model.

/common/models/Author.json

{
  "name": "Author",
  "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": {
    "authors": {
      "type": "hasMany",
      "model": "Author",
      "through": "ImageLink",
      "invert": true,
      "polymorphic": "imageable"
    },
    "readers": {
      "type": "hasMany",
      "model": "Reader",
      "through": "ImageLink",
      "invert": true,
      "polymorphic": "imageable"
    }
  },
...
}

Equivalently, in JavaScript:

/server/boot/boot-script.js

Author.hasMany(Picture, {
  as: 'pictures',
  polymorphic: {
    foreignKey: 'imageableId',
    discriminator: 'imageableType'
  },
  through: ImageLink
});
Reader.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(Author, {through: ImageLink, polymorphic: 'imageable', invert: true});
Picture.hasMany(Reader, {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

Author.hasAndBelongsToMany(Picture, {
  through: PictureLink,
  polymorphic: 'imageable'
});
Reader.hasAndBelongsToMany(Picture, {
  through: PictureLink,
  polymorphic: 'imageable'
});
// Optionally, define inverse hasMany relations with '(invert: true)'
Picture.hasMany(Author, {
  through: PictureLink,
  polymorphic: 'imageable',
  invert: true
});
Picture.hasMany(Reader, {
  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: 'Author',
  imageableId: '1',
  id: 1
}, {
  url: 'joe.jpg',
  imageableType: 'Reader',
  imageableId: '1',
  id: 2
}]

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

Readers: [{
  name: 'Joe',
  id: 1
}]
var Author = app.models.Author;
var Reader = app.models.Reader;
var Picture = app.models.Picture;

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

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

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

//Creating demo author, reader pictures then listing them
function createAuthor(cb) {
  Author.create({
    username: "John"
  }).then(function(author) {
    author.avatar.create({
      url: "john.jpg"
    }, function() {
      cb();
    });
  });
}

function createReader(cb) {
  Reader.create({
    name: "Joe"
  }).then(function(reader) {
    reader.imageable.create({
      url: "joe.jpg"
    }, function() {
      cb();
    });
  });
}

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

function listReaders() {
  Reader.find(function(err, res) {
    console.log("\nReaders:\n", res);
  })
}

function listAuthors() {
  Author.find(function(err, res) {
    console.log("\nAuthors:\n", res);
  })
}

//executing the demo
createAuthor(function() {
  createReader(function() {
    listPictures();
    listAuthors();
    listReaders();
  });
});