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

  1. class Post extends Model {}
  2. Post.init({
  3. title: Sequelize.STRING,
  4. text: Sequelize.STRING
  5. }, { sequelize, modelName: 'post' });
  6. class Image extends Model {}
  7. Image.init({
  8. title: Sequelize.STRING,
  9. link: Sequelize.STRING
  10. }, { sequelize, modelName: 'image' });
  11. class Comment extends Model {
  12. getItem(options) {
  13. return this[
  14. 'get' +
  15. this.get('commentable')
  16. [0]
  17. .toUpperCase() +
  18. this.get('commentable').substr(1)
  19. ](options);
  20. }
  21. }
  22. Comment.init({
  23. title: Sequelize.STRING,
  24. commentable: Sequelize.STRING,
  25. commentableId: Sequelize.INTEGER
  26. }, { sequelize, modelName: 'comment' });
  27. Post.hasMany(Comment, {
  28. foreignKey: 'commentableId',
  29. constraints: false,
  30. scope: {
  31. commentable: 'post'
  32. }
  33. });
  34. Comment.belongsTo(Post, {
  35. foreignKey: 'commentableId',
  36. constraints: false,
  37. as: 'post'
  38. });
  39. Image.hasMany(Comment, {
  40. foreignKey: 'commentableId',
  41. constraints: false,
  42. scope: {
  43. commentable: 'image'
  44. }
  45. });
  46. Comment.belongsTo(Image, {
  47. foreignKey: 'commentableId',
  48. constraints: false,
  49. as: 'image'
  50. });

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:

  1. image.getComments()
  2. // SELECT "id", "title", "commentable", "commentableId", "createdAt", "updatedAt" FROM "comments" AS
  3. // "comment" WHERE "comment"."commentable" = 'image' AND "comment"."commentableId" = 1;
  4. image.createComment({
  5. title: 'Awesome!'
  6. })
  7. // INSERT INTO "comments" ("id","title","commentable","commentableId","createdAt","updatedAt") VALUES
  8. // (DEFAULT,'Awesome!','image',1,'2018-04-17 05:36:40.454 +00:00','2018-04-17 05:36:40.454 +00:00')
  9. // RETURNING *;
  10. image.addComment(comment);
  11. // UPDATE "comments" SET "commentableId"=1,"commentable"='image',"updatedAt"='2018-04-17 05:38:43.948
  12. // +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.

  1. class ItemTag extends Model {}
  2. ItemTag.init({
  3. id: {
  4. type: Sequelize.INTEGER,
  5. primaryKey: true,
  6. autoIncrement: true
  7. },
  8. tagId: {
  9. type: Sequelize.INTEGER,
  10. unique: 'item_tag_taggable'
  11. },
  12. taggable: {
  13. type: Sequelize.STRING,
  14. unique: 'item_tag_taggable'
  15. },
  16. taggableId: {
  17. type: Sequelize.INTEGER,
  18. unique: 'item_tag_taggable',
  19. references: null
  20. }
  21. }, { sequelize, modelName: 'item_tag' });
  22. class Tag extends Model {}
  23. Tag.init({
  24. name: Sequelize.STRING,
  25. status: Sequelize.STRING
  26. }, { sequelize, modelName: 'tag' });
  27. Post.belongsToMany(Tag, {
  28. through: {
  29. model: ItemTag,
  30. unique: false,
  31. scope: {
  32. taggable: 'post'
  33. }
  34. },
  35. foreignKey: 'taggableId',
  36. constraints: false
  37. });
  38. Tag.belongsToMany(Post, {
  39. through: {
  40. model: ItemTag,
  41. unique: false
  42. },
  43. foreignKey: 'tagId',
  44. constraints: false
  45. });

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):

  1. Post.belongsToMany(Tag, {
  2. through: {
  3. model: ItemTag,
  4. unique: false,
  5. scope: {
  6. taggable: 'post'
  7. }
  8. },
  9. scope: {
  10. status: 'pending'
  11. },
  12. as: 'pendingTags',
  13. foreignKey: 'taggableId',
  14. constraints: false
  15. });
  16. post.getPendingTags();
  1. SELECT
  2. "tag"."id",
  3. "tag"."name",
  4. "tag"."status",
  5. "tag"."createdAt",
  6. "tag"."updatedAt",
  7. "item_tag"."id" AS "item_tag.id",
  8. "item_tag"."tagId" AS "item_tag.tagId",
  9. "item_tag"."taggable" AS "item_tag.taggable",
  10. "item_tag"."taggableId" AS "item_tag.taggableId",
  11. "item_tag"."createdAt" AS "item_tag.createdAt",
  12. "item_tag"."updatedAt" AS "item_tag.updatedAt"
  13. FROM
  14. "tags" AS "tag"
  15. INNER JOIN "item_tags" AS "item_tag" ON "tag"."id" = "item_tag"."tagId"
  16. AND "item_tag"."taggableId" = 1
  17. AND "item_tag"."taggable" = 'post'
  18. WHERE
  19. ("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:

  1. class Product extends Model {}
  2. Product.init({
  3. title: Sequelize.STRING
  4. }, { sequelize, modelName: 'product' });
  5. class User extends Model {}
  6. User.init({
  7. firstName: Sequelize.STRING,
  8. lastName: Sequelize.STRING
  9. }, { sequelize, modelName: 'user' });
  10. class Address extends Model {}
  11. Address.init({
  12. type: Sequelize.STRING,
  13. line1: Sequelize.STRING,
  14. line2: Sequelize.STRING,
  15. city: Sequelize.STRING,
  16. state: Sequelize.STRING,
  17. zip: Sequelize.STRING,
  18. }, { sequelize, modelName: 'address' });
  19. Product.User = Product.belongsTo(User);
  20. User.Addresses = User.hasMany(Address);
  21. // Also works for `hasOne`

A new Product, User, and one or more Address can be created in one step in the following way:

  1. return Product.create({
  2. title: 'Chair',
  3. user: {
  4. firstName: 'Mick',
  5. lastName: 'Broadstone',
  6. addresses: [{
  7. type: 'home',
  8. line1: '100 Main St.',
  9. city: 'Austin',
  10. state: 'TX',
  11. zip: '78704'
  12. }]
  13. }
  14. }, {
  15. include: [{
  16. association: Product.User,
  17. include: [ User.Addresses ]
  18. }]
  19. });

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.

  1. const Creator = Product.belongsTo(User, { as: 'creator' });
  2. return Product.create({
  3. title: 'Chair',
  4. creator: {
  5. firstName: 'Matt',
  6. lastName: 'Hansen'
  7. }
  8. }, {
  9. include: [ Creator ]
  10. });

HasMany / BelongsToMany association

Let's introduce the ability to associate a product with many tags. Setting up the models could look like:

  1. class Tag extends Model {}
  2. Tag.init({
  3. name: Sequelize.STRING
  4. }, { sequelize, modelName: 'tag' });
  5. Product.hasMany(Tag);
  6. // Also works for `belongsToMany`.

Now we can create a product with multiple tags in the following way:

  1. Product.create({
  2. id: 1,
  3. title: 'Chair',
  4. tags: [
  5. { name: 'Alpha'},
  6. { name: 'Beta'}
  7. ]
  8. }, {
  9. include: [ Tag ]
  10. })

And, we can modify this example to support an alias as well:

  1. const Categories = Product.hasMany(Tag, { as: 'categories' });
  2. Product.create({
  3. id: 1,
  4. title: 'Chair',
  5. categories: [
  6. { id: 1, name: 'Alpha' },
  7. { id: 2, name: 'Beta' }
  8. ]
  9. }, {
  10. include: [{
  11. association: Categories,
  12. as: 'categories'
  13. }]
  14. })