Sequelize polymorphic association learning records

jenemy
中文
When learning Sequelize, make a small note of this part of understanding and share it, so that it is convenient to check and avoid detours with other friends who need the same needs.

A polymorphic association composed of two (or more) associations that occur using the same foreign key.

For example: Consider the models Article , Video , Image and Comment . The first three represent the content that the user may post. We want all three to have comments. We can define the relationship as follows:

Article.hasMany(Comment)
Comment.belongsTo(Article)

Video.hasMany(Comment)
Comment.belongsTo(Video)

Image.hasMany(Comment)
Comment.belongsTo(Image)

The above method will result in the creation of 3 foreign keys articleId , videoId , imageId on the Comment table. This is obviously very troublesome and redundant. A better way is to implement the following table structure:

{
  id: Number // 主键,由数据库生成
  commentId: Number // 外键,对应 articleId/videoId/imageId 其中一个
  commentType: 'article' | 'video' | 'image' // 类型
  title: String // 评论内容
  // 其它字段定义...
}

Association part in the advanced association status of the official website document after minor changes through its own DEMO practice.

Here is the basic shelf of the code:

const { Sequelize, Op, Model, DataTypes, QueryTypes } = require('sequelize')
const sequelize = new Sequelize( 'test', 'root', 'xx', {
  dialect: 'mysql',
  host: 'localhost',
  logging: false,
  port: 3306,
  timezone: '+08:00',
});

(async () => {
  try {
    await sequelize.authenticate()
    console.log( 'Connection has been established successfully.' )
  } catch ( error ) {
    console.error( 'Unable to connect to the database:', error )
  }
})();

// 表关系定义写这儿

(async () => {
  await sequelize.sync({ alter: true })

  // 操作代码写这儿...
})();

// 方便重置数据库表
// (async () => {
//   await sequelize.drop()
// })()

The following is the implementation code (cannot be run directly, it needs to be placed in a suitable location)

const uppercaseFirst = str => `${str[0].toUpperCase()}${str.substr(1)}`;

const Article = sequelize.define('article', {
  title: DataTypes.STRING,
  content: DataTypes.TEXT
});

const Image = sequelize.define('image', {
  title: DataTypes.STRING,
  url: DataTypes.STRING
});

const Video = sequelize.define('video', {
  title: DataTypes.STRING,
  text: DataTypes.STRING
});

const Comment = sequelize.define('comment', {
  title: DataTypes.STRING,
  commentId: DataTypes.INTEGER,
  commentType: DataTypes.STRING
});

// 获取包装后的评论数据
Comment.prototype.getCommentDataValue = function(options) {
  if (!this.commentType) return Promise.resolve(null);
  const mixinMethodName = `get${uppercaseFirst(this.commentType)}`;
  return this[mixinMethodName](options);
};

Image.hasMany(Comment, {
  foreignKey: 'commentId',
  constraints: false,
  scope: {
    commentType: 'image'
  }
});
Comment.belongsTo(Image, { foreignKey: 'commentId', constraints: false });

Article.hasMany(Comment, {
  foreignKey: 'commentId',
  constraints: false,
  scope: {
    commentType: 'article'
  }
});
Comment.belongsTo(Article, { foreignKey: 'commentId', constraints: false });

Video.hasMany(Comment, {
  foreignKey: 'commentId',
  constraints: false,
  scope: {
    commentType: 'video'
  }
});
Comment.belongsTo(Video, { foreignKey: 'commentId', constraints: false });

// 为了防止预先加载的 bug/错误, 在相同的 afterFind hook 中从 Comment 实例中删除具体字段,仅保留抽象的 commentDataValue 字段可用.
Comment.addHook("afterFind", findResult => {
  // 关联的模型,实际项目走配置
  const commentTypes = ['article', 'image', 'video'];
  if (!Array.isArray(findResult)) findResult = [findResult];
  for (const instance of findResult) {
    for (const type of commentTypes) {
      if (instance.commentType === type) {
        if (instance[type] !== undefined) {
          // 存放处理后的数据
          instance.commentDataValue = instance[type]
        } else {
          instance.commentDataValue = instance[`get${uppercaseFirst(type)}`]()
        }
      }
    }

    // 防止错误:
    for (const type of commentTypes) {
      delete instance[type]
      delete instance.dataValues[type]
    }
  }
});

Next, simply add 2 pieces of data

const image = await Image.create({ title: '图片', url: "https://placekitten.com/408/287" });
const comment = await image.createComment({ title: "Awesome!" });

const article = await Article.create({ title: '文章标题', content: '文章内容' })
const comment = await article.createComment({ title: "文章写得不错!" });

The data of comments database is as follows:

idtitlecommentIdcommentTypecreatedAtupdatedAt
1Awesome!1image2021-09-18 15:20:292021-09-18 15:20:29
2The article is well written!1article2021-09-18 15:20:292021-09-18 15:20:29

The table data of the articles

idtitlecontentcreatedAtupdatedAt
1Article titleArticle content2021-09-18 15:20:292021-09-18 15:20:29

The data of images database is as follows:

idtitleurlcreatedAtupdatedAt
1picturehttps://placekitten.com/408/2872021-09-18 15:20:292021-09-18 15:20:29

Operation example:

  • Query picture comments
const image = await Image.findByPk(1)
// 结果
// {
//   "id": 1,
//   "title": "图片",
//   "url": "https://placekitten.com/408/287",
//   "createdAt": "2021-09-18T07:20:29.000Z",
//   "updatedAt": "2021-09-18T07:20:29.000Z"
// }
await image.getComments()
// [
//   {
//     "id": 1,
//     "title": "Awesome!",
//     "commentId": 1,
//     "commentType": "image",
//     "createdAt": "2021-09-18T07:20:29.000Z",
//     "updatedAt": "2021-09-18T07:20:29.000Z"
//   }
// ]
  • Query comments by ID
const comment = await Comment.findByPk(1)
// 结果
// {
//   "id": 1,
//   "title": "Awesome!",
//   "commentId": 1,
//   "commentType": "image",
//   "createdAt": "2021-09-18T07:20:29.000Z",
//   "updatedAt": "2021-09-18T07:20:29.000Z"
// }
await comment.getCommentDataValue()
await comment.commentDataValue // or
// 结果
// {
//   "id": 1,
//   "title": "图片",
//   "url": "https://placekitten.com/408/287",
//   "createdAt": "2021-09-18T07:20:29.000Z",
//   "updatedAt": "2021-09-18T07:20:29.000Z"
// }
  • Check all comments

Since there are no constraints, the associated model data needs to be processed by itself. The use of include this option has no effect

const comments = await Comment.findAll()
// 结果
// [
//   {
//     "id": 1,
//     "title": "Awesome!",
//     "commentId": 1,
//     "commentType": "image",
//     "createdAt": "2021-09-18T07:20:29.000Z",
//     "updatedAt": "2021-09-18T07:20:29.000Z"
//   },
//   {
//     "id": 2,
//     "title": "文章写得不错!",
//     "commentId": 1,
//     "commentType": "article",
//     "createdAt": "2021-09-18T07:20:29.000Z",
//     "updatedAt": "2021-09-18T07:20:29.000Z"
//   }
// ]
  • Query all reviews and associate models
const result = []
for (const comment of comments) {
  // 传入选项过滤数据
  comment.dataValues[comment.commentType] = await comment.getCommentDataValue({
    // 注意,这里的值要根据 `comment.commentType` 来区分,不同的模型字段不一样
    attributes: [
      'title'
    ]
  })
  // or 直接获取所有数据
  comment.dataValues[comment.commentType] = await comment.commentDataValue
  result.push(comment.dataValues)
}
// 结果
// [
//   {
//     "id": 1,
//     "title": "Awesome!",
//     "commentId": 1,
//     "commentType": "image",
//     "createdAt": "2021-09-18T07:20:29.000Z",
//     "updatedAt": "2021-09-18T07:20:29.000Z",
//     "image": {
//       "id": 1,
//       "title": "图片",
//       "url": "https://placekitten.com/408/287",
//       "createdAt": "2021-09-18T07:20:29.000Z",
//       "updatedAt": "2021-09-18T07:20:29.000Z"
//     }
//   },
//   {
//     "id": 2,
//     "title": "文章写得不错!",
//     "commentId": 1,
//     "commentType": "article",
//     "createdAt": "2021-09-18T07:20:29.000Z",
//     "updatedAt": "2021-09-18T07:20:29.000Z",
//     "article": {
//       "id": 1,
//       "title": "文章标题",
//       "content": "文章内容",
//       "createdAt": "2021-09-18T07:20:29.000Z",
//       "updatedAt": "2021-09-18T07:20:29.000Z"
//     }
//   }
// ]

Finally, if there is any good practice, I hope to leave a message to learn and discuss together, and learn to promote each other.

阅读 442

wujie520303
记录开发项目开发过程中的问题,分享点滴知识。

从事前端多年,技术依然很渣的IT程序员。

1.5k 声望
721 粉丝
0 条评论

从事前端多年,技术依然很渣的IT程序员。

1.5k 声望
721 粉丝
文章目录
宣传栏