Page Contents

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.

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
  • When the polymorphic property is an Object:
    • 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'
  • When the polymorphic property is a String (shorthand declaration):
    • Matching belongsTo relation name foreignKey is generated as polymorphic + 'Id'
    • discriminator is generated as polymorphic + 'Type'

Note that as inside polymorphic object will be deprecated in LoopBack 4. Use selector instead.

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 the polymorphic object:

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

You can also define a polymorphic hasMany 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'
  } 
});

Alternatively, 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, provide the following parameters:

  • type: the relation type, in this case is ‘belongsTo’
  • as: redefines this relation’s name (optional)
  • polymorphic: Can be either an object or a Boolean value.
    • When its value is an object:
      • foreignKey: A property of modelTo, representing the foreign key to modelFrom’s id.
      • discriminator: A property of modelTo, representing the actual modelFrom to be looked up and defined dynamically.
    • When its value is Boolean:
      • foreignKey is generated as relationName + 'Id'.
      • discriminator is generated as relationName + 'Type'.

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

A hasOne relation represents a “one-to-one” relation between models while a hasMany relation represents a “one-to-many” relation. For details, see 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 creates three properties in the the “through” model:

  • The discriminator (the value is either Employee or Product)
  • The foreign key (the ID or primary key value of either an employee or a product).
  • A foreignKey property that references the Picture model.

The first two properties above are same as in the “to” model in a hasMany polymorphic relation.

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"
    }
  },
...
}

Optionally, you can define an invert hasMany relation in the Picture model.

/common/models/Picture.json

{
  "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});

// Optionally 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

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

However, a hasAndBelongsToMany relation will automatically set up a belongsTo relation in through model, like this: ImageLink.belongsTo(Picture, {}) and ImageLink.belongsTo(ImageLink, {polymorphic: true}).

/common/models/model.js

Employee.hasAndBelongsToMany(Picture, {
  through: ImageLink,
  polymorphic: 'imageable'
});
Product.hasAndBelongsToMany(Picture, {
  through: ImageLink,
  polymorphic: 'imageable'
});
// Optionally, define inverse hasMany relations with '(invert: true)'
Picture.hasMany(Employee, {
  through: ImageLink,
  polymorphic: 'imageable',
  invert: true
});
Picture.hasMany(Product, {
  through: ImageLink,
  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();
  });
});