Sequelize|Sequelize 多态关联学习记录

学习 Sequelize 时对这部分理解作一个小小的笔记分享出来,方便查阅和其他需要同样需求的小伙伴少走弯路。
一个 多态关联 由使用同一外键发生的两个(或多个)关联组成.
例如:考虑模型 Article, Video, ImageComment. 前3个代表用户可能发布的内容. 我们希望3者都拥有评论,我们可以这样去定义关系:
Article.hasMany(Comment) Comment.belongsTo(Article)Video.hasMany(Comment) Comment.belongsTo(Video)Image.hasMany(Comment) Comment.belongsTo(Image)

上面的方式会导致在 Comment 表上创建3个外键 articleId, videoId, imageId. 这很显然很麻烦,冗余,更好的办法是实现下面的表结构:
{ id: Number // 主键,由数据库生成 commentId: Number // 外键,对应 articleId/videoId/imageId 其中一个 commentType: 'article' | 'video' | 'image' // 类型 title: String // 评论内容 // 其它字段定义... }

下面是根据官方网站文档高级关联状态中的多太关联部分经过自己DEMO实践小改而来。
下面是代码的基本架子:
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() // })()

下面是实现代码(不能直接运行,需要放置在合适的位置)
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 = https://www.it610.com/article/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 = https://www.it610.com/article/instance[type] } else { instance.commentDataValue = instance[`get${uppercaseFirst(type)}`]() } } }// 防止错误: for (const type of commentTypes) { delete instance[type] delete instance.dataValues[type] } } });

接下来简单添加2条数据
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: "文章写得不错!" });

数据库 comments 表数据如下:
id title commentId commentType createdAt updatedAt
1 Awesome! 1 image 2021-09-18 15:20:29 2021-09-18 15:20:29
2 文章写得不错! 1 article 2021-09-18 15:20:29 2021-09-18 15:20:29
数据库 articles 表数据如下:
id title content createdAt updatedAt
1 文章标题 文章内容 2021-09-18 15:20:29 2021-09-18 15:20:29
数据库 images 表数据如下:
id title url createdAt updatedAt
1 图片 https://placekitten.com/408/287 2021-09-18 15:20:29 2021-09-18 15:20:29
操作示例:
  • 查询图片评论
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" //} // ]

  • 根据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" // }

  • 查询所有评论
由于没有了约束,所关联的模型数据需要自行处理,这里选项中使用 include 不起任何作用
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" //} // ]

  • 查询所有评论并关联模型
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" //} //} // ]

【Sequelize|Sequelize 多态关联学习记录】最后,如果有什么好的实践还希望留言一起学习探讨一下,学习互相促进。

    推荐阅读