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
。
HasMany
一對多關聯將一個來源與多個目標連線起來。 而多個目標接到同一個特定的源。
const User = sequelize.define('user', {/* ... */}) const Project = sequelize.define('project', {/* ... */}) // 首先我們來定義一個 hasMany 關聯 Project.hasMany(User, {as: 'Workers'})
這會將projectId
屬性新增到 User。 根據當前的設定,表中的列將被稱為projectId
或project_id
。 Project 的例項將獲得訪問器getWorkers
和setWorkers
。
有時你可能需要在不同的列上關聯記錄,這時候你可以使用sourceKey
選項:
const City = sequelize.define('city', { countryCode: Sequelize.STRING }); const Country = sequelize.define('country', { isoCode: Sequelize.STRING }); // 在這裡,我們可以根據國家程式碼連線國家和城市 Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'}); City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});
一對多關係
模型定義
model/user.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const User = sequelize.define("user", { empId: { type: Sequelize.STRING, allowNull: false, unique: true } }); return User; };
model/note.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const Note = sequelize.define("note", { title: { type: Sequelize.CHAR(64), allowNull: false } }); return Note; };
資料庫連線及關係定義
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 User = require('./model/user')(sequelize); const Note = require('./model/note')(sequelize); // User的例項物件將擁有:getNotes、setNotes、addNote、createNote、 // removeNote、hasNote方法 User.hasMany(Note); // Note的例項物件將擁有getUser、setUser、createUser方法 Note.belongsTo(User); sequelize.sync({ // force: true }) .then(async () => { console.log(`Database & tables created!`); }) }) .catch(err => { console.error('Unable to connect to the database:', err); });
以上程式碼執行後,終端將會輸出以下資訊:
- 新建 users 表:
CREATE TABLE IF NOT EXISTS `users` ( `id` INTEGER NOT NULL auto_increment , `empId` VARCHAR(255) NOT NULL UNIQUE, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
- 新建 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, `userId` INTEGER, PRIMARY KEY (`id`), FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE) ENGINE=InnoDB;
通過觀察上面的 notes 建表語句,我們發現Sequelize 自動為 notes 表新增了 userId 欄位,同時生成了相應的外來鍵約束。
一般來說,外來鍵約束可能會導致一些效能問題。所以,建表時我們一般會去掉約束,同時給外來鍵加一個索引(加速查詢),但之後的資料的一致性就需要應用層來保證了。
關係操作
- 新增
方式一
const user = await User.create({ empId: '1' }); // (1) const note = await user.createNote({ title: 'learn sequelize' }); // (2)
步驟一:新建使用者,對應的 SQL 語句如下:
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 07:42:26','2018-10-10 07:42:26');
步驟二:建立 Note,對應的 SQL 語句如下:
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'learn sequelize','2018-10-10 07:42:26','2018-10-10 07:42:26',1);
可以看出,當呼叫 user.createNote 方法時,會使用新建使用者的 userId 作為外來鍵在 notes 表中插入一條新的資料。
方式二
const user = await User.create({ empId: '1' }); // (1) const note = await Note.create({ title: 'learn sequelize' }); // (2) await user.addNote(note); // (3)
步驟一:新建使用者,對應的 SQL 語句如下:
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 07:53:26','2018-10-10 07:53:26');
步驟二:建立 Note,對應的 SQL 語句如下:
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'learn sequelize','2018-10-10 07:53:26','2018-10-10 07:53:26');
以上 SQL 執行後,會插入一條 note 資料,但此時該條記錄的外來鍵userId
為空。
步驟三:使用已建立使用者的 id 值,設定步驟二 note 記錄的外來鍵userId
的值,對應的 SQL 語句如下:
UPDATE `notes` SET `userId`=1,`updatedAt`='2018-10-10 07:53:26' WHERE `id` IN (1)
- 修改
const user = await User.create({ empId: '1' }); // (1) const note1 = await user.createNote({ title: 'learn node.js' }); // (2) const note2 = await user.createNote({ title: 'learn rx.js' }); // (3) const note3 = await Note.create({ title: 'learn angular.js' }); // (4) const note4 = await Note.create({ title: 'learn typescript.js' }); // (5) await user.setNotes([note3, note4]); // (6)
步驟一:新建使用者,對應的 SQL 語句如下:
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 08:09:13','2018-10-10 08:09:13');
步驟二與步驟三:建立 Note1 和 Note2,對應的 SQL 語句如下:
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'learn node.js','2018-10-10 08:12:49','2018-10-10 08:12:49',1); INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'learn rx.js','2018-10-10 08:12:49','2018-10-10 08:12:49',1);
步驟四與步驟五:建立 Note3 和 Note4,對應的 SQL 語句如下:
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'learn angular.js','2018-10-10 08:12:49','2018-10-10 08:12:49'); INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'learn typescript.js','2018-10-10 08:12:49','2018-10-10 08:12:49');
步驟六:設定關聯關係,執行流程及對應的 SQL 語句如下:
- 查詢 userId 為 1 的使用者的所有 note 記錄:
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE `note`.`userId` = 1;
- 將 note1、note2 記錄的外來鍵 userId 的值置為 NULL,切斷之間的關係:
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:12:49' WHERE `id` IN (1, 2)
- 將 note3、note4 記錄的外來鍵 userId 的值置為當前使用者的 id,完成關係的建立:
UPDATE `notes` SET `userId`=1,`updatedAt`='2018-10-10 08:12:49' WHERE `id` IN (3, 4)
因為我們需要根據傳人setNotes
的陣列來計算出哪些note
要切斷關係、哪些要新增關係,所以就需要查出來進行一個計算集合的 “交集” 運算。
- 刪除
方式一
const user = await User.create({ empId: '1' }); // (1) const note1 = await user.createNote({ title: 'learn node.js' }); // (2) const note2 = await user.createNote({ title: 'learn rx.js' }); // (3) await user.setNotes([]); // (4)
步驟一至三的執行流程及對應 SQL 語句請參考修改環節,這裡不再介紹。
步驟四:呼叫user.setNotes([])
方法,刪除當前使用者下的所有 note 記錄,執行流程及對應的 SQL 語句如下:
- 查詢 userId 為 1 的使用者的所有 note 記錄:
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE `note`.`userId` = 1;
- userId 為 1 的使用者的所有 note 記錄的外來鍵 userId 置為 NULL,切斷關係:
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:25:04' WHERE `id` IN (1, 2)
通過以上的 SQL 語句,我們知道呼叫user.setNotes([])
會刪除當前使用者下所關聯的所有 note 記錄,若需要刪除指定 note 記錄,則可以呼叫user.removeNote
方法。
方式二
const user = await User.create({ empId: '1' }); // (1) const note1 = await user.createNote({ title: 'learn node.js' }); // (2) const note2 = await user.createNote({ title: 'learn rx.js' }); // (3) user.removeNote(note2);
步驟一至三的執行流程及對應 SQL 語句請參考修改環節,這裡不再介紹。
步驟四:呼叫user.removeNote(note2)
方法,將刪除當前使用者下指定的 note2 記錄,對應的 SQL 語句如下:
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:38:40' WHERE `userId` = 1 AND `id` IN (2)
- 查詢
- 查詢當前使用者下所有滿足條件的 note 資料:
const Op = Sequelize.Op const user = await User.findById(1); // (1) const notes = await user.getNotes({ // (2) where: { title: { [Op.like]: '%node%' } } }); console.log(`User ${user.id}: has ${notes.length} notes`);
步驟一:查詢 id 為 1 的使用者,對應的 SQL 語句如下:
SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;
步驟二:根據查詢條件,獲取 id 為 1 的使用者下的所有滿足條件的 note 記錄,對應的 SQL 語句如下:
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE (`note`.`userId` = 1 AND `note`.`title` LIKE '%node%');
- 查詢所有滿足條件的 note,同時獲取 note 所屬的 user:
const Op = Sequelize.Op const notes = await Note.findAll({ include: [User], where: { title: { [Op.like]: '%node%' } } }); // 當前note屬於哪個user可以通過note.user訪問 console.log(`Has found ${notes.length} notes`);
以上操作對應的 SQL 語句如下:
SELECT `note`.`id`, `note`.`title`, `note`.`createdAt`, `note`.`updatedAt`, `note`.`userId`, `user`.`id` AS `user.id`, `user`.`empId` AS `user.empId`, `user`.`createdAt` AS `user.createdAt`, `user`.`updatedAt` AS `user.updatedAt` FROM `notes` AS `note` LEFT OUTER JOIN `users` AS `user` ON `note`.`userId` = `user`.`id` WHERE `note`.`title` LIKE '%node1%';
- 查詢所有滿足條件的 user,同時獲取該 user 所有滿足條件的 note:
const Op = Sequelize.Op const users = await User.findAll({ include: [Note], where: { createdAt: { [Op.lt]: new Date() } } }); // user的notes可以通過user.notes訪問 console.log(`Has found ${users.length} users`);
以上操作對應的 SQL 語句如下:
SELECT `user`.`id`, `user`.`empId`, `user`.`createdAt`, `user`.`updatedAt`, `notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`createdAt` AS `notes.createdAt`, `notes`.`updatedAt` AS `notes.updatedAt`, `notes`.`userId` AS `notes.userId` FROM `users` AS `user` LEFT OUTER JOIN `notes` AS `notes` ON `user`.`id` = `notes`.`userId` WHERE `user`.`createdAt` < '2018-10-10 09:21:15';
這裡需要注意的是,eager loading
中include
傳遞的是需獲取的相關模型,預設是獲取全部,我們也可以根據實際需求再對這個模型進行一層過濾。比如:
const Op = Sequelize.Op const users = await User.findAll({ include: [{ model: Note, where: { title: { [Op.like]: '%node%' } } }], where: { createdAt: { [Op.lt]: new Date() } } });
以上操作對應的 SQL 語句如下:
SELECT `user`.`id`, `user`.`empId`, `user`.`createdAt`, `user`.`updatedAt`, `notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`createdAt` AS `notes.createdAt`, `notes`.`updatedAt` AS `notes.updatedAt`, `notes`.`userId` AS `notes.userId` FROM `users` AS `user` INNER JOIN `notes` AS `notes` ON `user`.`id` = `notes`.`userId` AND `notes`.`title` LIKE '%node%' WHERE `user`.`createdAt` < '2018-10-10 09:42:26';
當我們對include
的模型加了where
過濾條件時,會使用inner join
來進行查詢,這樣保證只有那些擁有標題含有node
關鍵詞note
的使用者才會返回。關於各種 join 的區別,可以參考:a-visual-explanation-of-sql-joins
。