Advanced Concepts
Scopes
This section concerns association scopes. For a definition of association scopes vs. scopes on associated models, see Scopes.
Association scopes allow you to place a scope (a set of default attributes for get
and create
) on the association. Scopes can be placed both on the associated model (the target of the association), and on the through table for n:m relations.
1:n
Assume we have models Comment, Post, and Image. A comment can be associated to either an image or a post via commentableId
and commentable
- we say that Post and Image are Commentable
class Post extends Model {}
Post.init({
title: Sequelize.STRING,
text: Sequelize.STRING
}, { sequelize, modelName: 'post' });
class Image extends Model {}
Image.init({
title: Sequelize.STRING,
link: Sequelize.STRING
}, { sequelize, modelName: 'image' });
class Comment extends Model {
getItem(options) {
return this[
'get' +
this.get('commentable')
[0]
.toUpperCase() +
this.get('commentable').substr(1)
](options);
}
}
Comment.init({
title: Sequelize.STRING,
commentable: Sequelize.STRING,
commentableId: Sequelize.INTEGER
}, { sequelize, modelName: 'comment' });
Post.hasMany(Comment, {
foreignKey: 'commentableId',
constraints: false,
scope: {
commentable: 'post'
}
});
Comment.belongsTo(Post, {
foreignKey: 'commentableId',
constraints: false,
as: 'post'
});
Image.hasMany(Comment, {
foreignKey: 'commentableId',
constraints: false,
scope: {
commentable: 'image'
}
});
Comment.belongsTo(Image, {
foreignKey: 'commentableId',
constraints: false,
as: 'image'
});
constraints: false
disables references constraints, as commentableId
column references several tables, we cannot add a REFERENCES
constraint to it.
Note that the Image -> Comment and Post -> Comment relations define a scope, commentable: 'image'
and commentable: 'post'
respectively. This scope is automatically applied when using the association functions:
image.getComments()
// SELECT "id", "title", "commentable", "commentableId", "createdAt", "updatedAt" FROM "comments" AS
// "comment" WHERE "comment"."commentable" = 'image' AND "comment"."commentableId" = 1;
image.createComment({
title: 'Awesome!'
})
// INSERT INTO "comments" ("id","title","commentable","commentableId","createdAt","updatedAt") VALUES
// (DEFAULT,'Awesome!','image',1,'2018-04-17 05:36:40.454 +00:00','2018-04-17 05:36:40.454 +00:00')
// RETURNING *;
image.addComment(comment);
// UPDATE "comments" SET "commentableId"=1,"commentable"='image',"updatedAt"='2018-04-17 05:38:43.948
// +00:00' WHERE "id" IN (1)
The getItem
utility function on Comment
completes the picture - it simply converts the commentable
string into a call to either getImage
or getPost
, providing an abstraction over whether a comment belongs to a post or an image. You can pass a normal options object as a parameter to getItem(options)
to specify any where conditions or includes.
n:m
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several items.
For brevity, the example only shows a Post model, but in reality Tag would be related to several other models.
class ItemTag extends Model {}
ItemTag.init({
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
tagId: {
type: Sequelize.INTEGER,
unique: 'item_tag_taggable'
},
taggable: {
type: Sequelize.STRING,
unique: 'item_tag_taggable'
},
taggableId: {
type: Sequelize.INTEGER,
unique: 'item_tag_taggable',
references: null
}
}, { sequelize, modelName: 'item_tag' });
class Tag extends Model {}
Tag.init({
name: Sequelize.STRING,
status: Sequelize.STRING
}, { sequelize, modelName: 'tag' });
Post.belongsToMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
foreignKey: 'taggableId',
constraints: false
});
Tag.belongsToMany(Post, {
through: {
model: ItemTag,
unique: false
},
foreignKey: 'tagId',
constraints: false
});
Notice that the scoped column (taggable
) is now on the through model (ItemTag
).
We could also define a more restrictive association, for example, to get all pending tags for a post by applying a scope of both the through model (ItemTag
) and the target model (Tag
):
Post.belongsToMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
scope: {
status: 'pending'
},
as: 'pendingTags',
foreignKey: 'taggableId',
constraints: false
});
post.getPendingTags();
SELECT
"tag"."id",
"tag"."name",
"tag"."status",
"tag"."createdAt",
"tag"."updatedAt",
"item_tag"."id" AS "item_tag.id",
"item_tag"."tagId" AS "item_tag.tagId",
"item_tag"."taggable" AS "item_tag.taggable",
"item_tag"."taggableId" AS "item_tag.taggableId",
"item_tag"."createdAt" AS "item_tag.createdAt",
"item_tag"."updatedAt" AS "item_tag.updatedAt"
FROM
"tags" AS "tag"
INNER JOIN "item_tags" AS "item_tag" ON "tag"."id" = "item_tag"."tagId"
AND "item_tag"."taggableId" = 1
AND "item_tag"."taggable" = 'post'
WHERE
("tag"."status" = 'pending');
constraints: false
disables references constraints on the taggableId
column. Because the column is polymorphic, we cannot say that it REFERENCES
a specific table.
Creating with associations
An instance can be created with nested association in one step, provided all elements are new.
BelongsTo / HasMany / HasOne association
Consider the following models:
class Product extends Model {}
Product.init({
title: Sequelize.STRING
}, { sequelize, modelName: 'product' });
class User extends Model {}
User.init({
firstName: Sequelize.STRING,
lastName: Sequelize.STRING
}, { sequelize, modelName: 'user' });
class Address extends Model {}
Address.init({
type: Sequelize.STRING,
line1: Sequelize.STRING,
line2: Sequelize.STRING,
city: Sequelize.STRING,
state: Sequelize.STRING,
zip: Sequelize.STRING,
}, { sequelize, modelName: 'address' });
Product.User = Product.belongsTo(User);
User.Addresses = User.hasMany(Address);
// Also works for `hasOne`
A new Product
, User
, and one or more Address
can be created in one step in the following way:
return Product.create({
title: 'Chair',
user: {
firstName: 'Mick',
lastName: 'Broadstone',
addresses: [{
type: 'home',
line1: '100 Main St.',
city: 'Austin',
state: 'TX',
zip: '78704'
}]
}
}, {
include: [{
association: Product.User,
include: [ User.Addresses ]
}]
});
Here, our user model is called user
, with a lowercase u - This means that the property in the object should also be user
. If the name given to sequelize.define
was User
, the key in the object should also be User
. Likewise for addresses
, except it's pluralized being a hasMany
association.
BelongsTo association with an alias
The previous example can be extended to support an association alias.
const Creator = Product.belongsTo(User, { as: 'creator' });
return Product.create({
title: 'Chair',
creator: {
firstName: 'Matt',
lastName: 'Hansen'
}
}, {
include: [ Creator ]
});
HasMany / BelongsToMany association
Let's introduce the ability to associate a product with many tags. Setting up the models could look like:
class Tag extends Model {}
Tag.init({
name: Sequelize.STRING
}, { sequelize, modelName: 'tag' });
Product.hasMany(Tag);
// Also works for `belongsToMany`.
Now we can create a product with multiple tags in the following way:
Product.create({
id: 1,
title: 'Chair',
tags: [
{ name: 'Alpha'},
{ name: 'Beta'}
]
}, {
include: [ Tag ]
})
And, we can modify this example to support an alias as well:
const Categories = Product.hasMany(Tag, { as: 'categories' });
Product.create({
id: 1,
title: 'Chair',
categories: [
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Beta' }
]
}, {
include: [{
association: Categories,
as: 'categories'
}]
})