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 anObject
: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 providedforeignKey
: A property of modelTo, representing the fk to modelFrom’s id generated by default asas + 'Id'
discriminator
: A property of modelTo, representing the actual modelFrom to be looked up and defined dynamically generated by default asas + 'Type'
- When the
polymorphic
property is aString
(shorthand declaration):- Matching belongsTo relation name
foreignKey
is generated aspolymorphic + 'Id'
discriminator
is generated aspolymorphic + 'Type'
- Matching belongsTo relation name
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.
{
"name": "Employee",
"base": "PersistedModel",
...
"relations": {
"pictures": {
"type": "hasMany",
"model": "Picture",
"polymorphic": "imageable"
}
}
...
And:
{
"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:
Employee.hasMany(Picture, { polymorphic: 'imageable' });
And:
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 asrelationName + 'Id'
.discriminator
is generated asrelationName + 'Type'
.
- When its value is an object:
Note:
Do not provide a model
field in the relation definition. If you define it, LoopBack throws an error as relation validation.
Also, do not provide selector
or as
inside polymorphic object.
{
"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:
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.
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
orProduct
) - 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.
{
"name": "ImageLink",
"base": "PersistedModel",
...
"relations": {
"picture": {
"type": "belongsTo",
"model": "Picture",
"foreignKey": ""
},
"imageable": {
"type": "belongsTo",
"polymorphic": true
}
}
...
}
Then in the Employee (or Product) model.
{
"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.
{
"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:
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})
.
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();
});
});