學習 Sequelize 時對這部分理解作一個小小的筆記分享出來,方便查閲和其他需要同樣需求的小夥伴少走彎路。
一個 多態關聯 由使用同一外鍵發生的兩個(或多個)關聯組成.
例如:考慮模型 Article, Video, Image 和 Comment. 前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 = 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]
}
}
});
接下來簡單添加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"
// }
// }
// ]
最後,如果有什麼好的實踐還希望留言一起學習探討一下,學習互相促進。