Sequelize 多型關聯學習記錄

語言: CN / TW / HK

學習 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 = 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"
//     }
//   }
// ]

最後,如果有什麼好的實踐還希望留言一起學習探討一下,學習互相促進。