Association scopes allow you to place a scope (a set of default attributes for 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
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
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
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
):
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
class Product extends Model {}
Product.init({
title: Sequelize.STRING
}, { sequelize, modelName: 'product' });
class User extends Model {}
User.init({
firstName: Sequelize.STRING,
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:
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:
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'
}]