Sequelize 系列教程之多對多模型關係
ofollow,noindex">Sequelize 是一個基於 Promise 的 Node.js ORM,目前支援 Postgres、SQL/">MySQL、SQLite 和 Microsoft SQL Server。它具有強大的事務支援,關聯關係、讀取和複製等功能。在閱讀本文前,如果你對 Sequelize 還不瞭解,建議先閱讀Sequelize 快速入門 這篇文章。
資料模型中的表關係一般有三種:一對一、一對多、多對多。Sequelize 為開發者提供了清晰易用的介面來定義關係、進行表之間的操作。本文我們將介紹在Sequelize 中如何定義多對多的表關係。
基本概念
Source & Target
我們首先從一個基本概念開始,你將會在大多數關聯中使用source
和target
模型。 假設您正試圖在兩個模型之間新增關聯。 這裡我們在User
和Project
之間新增一個hasOne
關聯。
const User = sequelize.define('User', { name: Sequelize.STRING, email: Sequelize.STRING }); const Project = sequelize.define('Project', { name: Sequelize.STRING }); User.hasOne(Project);
User
模型(函式被呼叫的模型)是source
。Project
模型(作為引數傳遞的模型)是target
。
belongsToMany
多對多關聯用於將源與多個目標相連線。 此外,目標也可以連線到多個源。
Project.belongsToMany(User, { through: 'UserProject' }); User.belongsToMany(Project, { through: 'UserProject' });
這將建立一個名為 UserProject 的新模型,具有等效的外來鍵projectId
和userId
。 屬性是否為camelcase
取決於由表(在這種情況下為User
和Project
)連線的兩個模型。
-
User.belongsToMany(Project, {through: 'UserProject'})
—— 將新增方法getUsers
,setUsers
,addUser
,addUsers
到Project
上。 -
User.belongsToMany(Project, {through: 'UserProject'})
—— 將新增方法getPorjects
,setProjects
,addProject
,addProjects
到User
上。
有時,您可能需要在關聯中使用它們時重新命名模型。 讓我們通過使用別名(as
)選項將 users 定義為 workers 而 projects 定義為 tasks。 我們還將手動定義要使用的外來鍵:
User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId' }); Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey: 'projectId' })
如果你想要連線表中的其他屬性,則可以在定義關聯之前為連線表定義一個模型,然後再說明它應該使用該模型進行連線,而不是建立一個新的關聯:
const User = sequelize.define('user', {}) const Project = sequelize.define('project', {}) const UserProjects = sequelize.define('userProjects', { status: DataTypes.STRING }) User.belongsToMany(Project, { through: UserProjects }) Project.belongsToMany(User, { through: UserProjects })
預設情況下,上面的程式碼會將 projectId 和 userId 新增到 UserProjects 表中, 刪除任何先前定義的主鍵屬性 - 表將由兩個表的鍵的組合唯一標識,並且沒有其他主鍵列。 若需要在UserProjects
模型上新增一個主鍵,你可以手動新增它。
const UserProjects = sequelize.define('userProjects', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, status: DataTypes.STRING })
使用多對多你可以基於through 關係查詢並選擇特定屬性,比如:
User.findAll({ include: [{ model: Project, through: { attributes: ['createdAt', 'startedAt', 'finishedAt'], where: {completed: true} } }] });
多對多關係
模型定義
model/note.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const Note = sequelize.define("note", { title: { type: Sequelize.CHAR(64), allowNull: false } }); return Note; };
model/tag.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const Tag = sequelize.define("tag", { name: { type: Sequelize.CHAR(64), allowNull: false, unique: true } }); return Tag; };
model/tagging.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const Tagging = sequelize.define("tag", { type: { type: Sequelize.INTEGER, allowNull: false } }); return Tagging; };
資料庫連線及關係定義
db.js
const Sequelize = require('sequelize'); const sequelize = new Sequelize('exe', 'root', '', { host: 'localhost', dialect: 'mysql', operatorsAliases: false, pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } }); sequelize .authenticate() .then(async () => { console.log('Connection has been established successfully.'); const Note = require('./model/note')(sequelize); const Tag = require('./model/tag')(sequelize); const Tagging = require('./model/tagging')(sequelize); // Note的例項擁有getTags、setTags、addTag、addTags、createTag、 // removeTag、hasTag方法 Note.belongsToMany(Tag, { through: Tagging }); // Tag的例項擁有getNotes、setNotes、addNote、addNotes、createNote、 // removeNote、hasNote方法 Tag.belongsToMany(Note, { through: Tagging }); sequelize.sync({ force: true }) .then(async () => { }) }) .catch(err => { console.error('Unable to connect to the database:', err); });
以上程式碼執行後,終端將會輸出以下資訊:
- 新建 notes 表
CREATE TABLE IF NOT EXISTS `notes` ( `id` INTEGER NOT NULL auto_increment , `title` CHAR(64) NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
- 新建 tags 表
CREATE TABLE IF NOT EXISTS `tags` ( `id` INTEGER NOT NULL auto_increment , `type` INTEGER NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
- 新建 taggings 表
CREATE TABLE IF NOT EXISTS `taggings` ( `type` INTEGER NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `noteId` INTEGER , `tagId` INTEGER , PRIMARY KEY (`noteId`, `tagId`), FOREIGN KEY (`noteId`) REFERENCES `notes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`tagId`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB;
可以看到,多對多關係中我們單獨生成了一張關係表,並設定了 2 個外來鍵tagId
和noteId
來和tags
和notes
進行關聯。
關係操作
- 新增
方式一
const note = await Note.create({ title: 'note' }); // (1) await note.createTag({ name: 'tag' }, { through: { type: 0 }}); // (2)
步驟一:新增 note 記錄,對應的 SQL 語句如下:
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'note','2018-10-12 09:19:11','2018-10-12 09:19:11');
步驟二(1):新建 tag 記錄,對應的 SQL 語句如下:
INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag','2018-10-12 09:19:11','2018-10-12 09:19:11');
步驟二(2):新建 tagging 記錄,對應的 SQL 語句如下:
INSERT INTO `taggings` (`type`,`createdAt`,`updatedAt`,`noteId`,`tagId`) VALUES (0,'2018-10-12 09:19:11','2018-10-12 09:19:11',1,1);
關係表本身需要的屬性,通過傳遞一個額外的物件給設定方法來實現。
方式二
const note = await Note.create({ title: 'note' }); const tag = await Tag.create({ name: 'tag' }); await note.addTag(tag, { through: { type: 0 } });
這種方法和上面的方法實際上是一樣的。只是我們先手動create
了一個Tag
模型。
方式三
const note = await Note.create({ title: 'note' }); // (1) const tag1 = await Tag.create({ name: 'tag1' }); // (2) const tag2 = await Tag.create({ name: 'tag2' }); // (3) await note.addTags([tag1, tag2], { through: { type: 2 }}); // (4)
步驟一:新增 note 記錄,對應的 SQL 語句如下:
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'note','2018-10-12 11:33:17','2018-10-12 11:33:17');
步驟二:新建第一條 tag 記錄,對應的 SQL 語句如下:
INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag1','2018-10-12 11:33:17','2018-10-12 11:33:17');
步驟三:新建第二條 tag 記錄,對應的 SQL 語句如下:
INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag2','2018-10-12 11:33:17','2018-10-12 11:33:17');
步驟四:新增兩條 tagging 記錄,對應的 SQL 語句如下:
INSERT INTO `taggings` (`type`,`createdAt`,`updatedAt`,`noteId`,`tagId`) VALUES (2,'2018-10-12 11:33:17','2018-10-12 11:33:17',1,1),(2,'2018-10-12 11:33:17','2018-10-12 11:33:17',1,2);
- 修改
const note = await Note.create({ title: 'note' }); const tag1 = await Tag.create({ name: 'tag1'}); const tag2 = await Tag.create({ name: 'tag2'}); await note.addTags([tag1, tag2], { through: { type: 2 }}); const tag3 = await Tag.create({ name: 'tag3'}); // (1) const tag4 = await Tag.create({ name: 'tag4'}); // (2) await note.setTags([tag3, tag4], { through: { type: 3 }}); // (3)
步驟一:新建第三條 tag 記錄,對應的 SQL 語句如下:
INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag3','2018-10-12 11:43:16','2018-10-12 11:43:16');
步驟二:新建第四條 tag 記錄,對應的 SQL 語句如下:
INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag4','2018-10-12 11:43:16','2018-10-12 11:43:16');
步驟三(1):刪除當前 note 記錄,與 tag1、tag2 之間的關聯資訊,對應的 SQL 語句如下:
DELETE FROM `taggings` WHERE `noteId` = 1 AND `tagId` IN (1, 2)
步驟三(2):設定當前 note 記錄,與 tag3、tag4 之間的關聯資訊,對應的 SQL 語句如下:
INSERT INTO `taggings` (`type`,`createdAt`,`updatedAt`,`noteId`,`tagId`) VALUES (3,'2018-10-12 11:43:16','2018-10-12 11:43:16',1,3),(3,'2018-10-12 11:43:16','2018-10-12 11:43:16',1,4);
- 刪除
刪除單條記錄
const note = await Note.create({ title: 'note' }); const tag1 = await Tag.create({ name: 'tag1' }); const tag2 = await Tag.create({ name: 'tag2' }); await note.addTags([tag1, tag2], { through: { type: 2 }}); await note.removeTag(tag1); // (1)
步驟一:刪除 tag1 記錄,對應的 SQL 語句如下:
DELETE FROM `taggings` WHERE `noteId` = 1 AND `tagId` IN (1)
刪除單條記錄很簡單,直接將關係表 taggings 中的資料刪除。
全部刪除
const note = await Note.create({ title: 'note' }); const tag1 = await Tag.create({ name: 'tag1' }); const tag2 = await Tag.create({ name: 'tag2' }); await note.addTags([tag1, tag2], { through: { type: 2 }}); await note.setTags([]); // (1)
步驟一(1):查詢關係表 taggings 中與當前 note 相關的記錄,對應的 SQL 語句如下:
SELECT `type`, `createdAt`, `updatedAt`, `noteId`, `tagId` FROM `taggings` AS `tagging` WHERE `tagging`.`noteId` = 1;
步驟一(2):刪除所有匹配的資料,對應的 SQL 語句如下:
DELETE FROM `taggings` WHERE `noteId` = 1 AND `tagId` IN (1, 2)
- 查詢
- 查詢當前 note 中所有滿足條件的 tag:
const Op = Sequelize.Op const tags = await note.getTags({ where: { name: { [Op.like]: 'tag%' } } }); console.log(`Note has ${tags.length} tags`);
以上操作對應的 SQL 語句如下:
SELECT `tag`.`id`, `tag`.`name`, `tag`.`createdAt`, `tag`.`updatedAt`, `tagging`.`type` AS `tagging.type`, `tagging`.`createdAt` AS `tagging.createdAt`, `tagging`.`updatedAt` AS `tagging.updatedAt`, `tagging`.`noteId` AS `tagging.noteId`, `tagging`.`tagId` AS `tagging.tagId` FROM `tags` AS `tag` INNER JOIN `taggings` AS `tagging` ON `tag`.`id` = `tagging`.`tagId` AND `tagging`.`noteId` = 1 WHERE (`tag`.`name` LIKE 'tag%');
- 查詢所有滿足條件的 tag,同時獲取每個 tag 所在的 note:
const tags = await Tag.findAll({ include: { model: Note } }); // tag的notes可以通過tag.notes訪問,關係模型可以通過tag.notes[0].tagging訪問 console.log(`Has found ${tags.length} tags`);
以上操作對應的 SQL 語句如下:
SELECT `tag`.`id`, `tag`.`name`, `tag`.`createdAt`, `tag`.`updatedAt`, `notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`createdAt` AS `notes.createdAt`, `notes`.`updatedAt` AS `notes.updatedAt`, `notes->tagging`.`type` AS `notes.tagging.type`, `notes->tagging`.`createdAt` AS `notes.tagging.createdAt`, `notes->tagging`.`updatedAt` AS `notes.tagging.updatedAt`, `notes->tagging`.`noteId` AS `notes.tagging.noteId`, `notes->tagging`.`tagId` AS `notes.tagging.tagId` FROM `tags` AS `tag` LEFT OUTER JOIN ( `taggings` AS `notes->tagging` INNER JOIN `notes` AS `notes` ON `notes`.`id` = `notes->tagging`.`noteId` ) ON `tag`.`id` = `notes->tagging`.`tagId`;
首先是notes
和taggings
進行了一個inner join
,選出notes
,然後tags
和剛join
出的集合再做一次left join
,得到結果。
- 查詢所有滿足條件的 note,同時獲取每個 note 相關聯的 tag:
const notes = await Note.findAll({ include: [ { model: Tag // 支援tags設定查詢條件 } ] });
以上操作對應的 SQL 語句如下:
SELECT `note`.`id`, `note`.`title`, `note`.`createdAt`, `note`.`updatedAt`, `tags`.`id` AS `tags.id`, `tags`.`name` AS `tags.name`, `tags`.`createdAt` AS `tags.createdAt`, `tags`.`updatedAt` AS `tags.updatedAt`, `tags->tagging`.`type` AS `tags.tagging.type`, `tags->tagging`.`createdAt` AS `tags.tagging.createdAt`, `tags->tagging`.`updatedAt` AS `tags.tagging.updatedAt`, `tags->tagging`.`noteId` AS `tags.tagging.noteId`, `tags->tagging`.`tagId` AS `tags.tagging.tagId` FROM `notes` AS `note` LEFT OUTER JOIN ( `taggings` AS `tags->tagging` INNER JOIN `tags` AS `tags` ON `tags`.`id` = `tags->tagging`.`tagId` ) ON `note`.`id` = `tags->tagging`.`noteId`;
首先是tags
和taggins
進行了一個inner join
,選出tags
,然後notes
和剛join
出的集合再做一次left join
,得到結果。