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
。
BelongsTo
BelongsTo 關聯是在source model 上存在一對一關係的外來鍵的關聯。
一個簡單的例子是Player 通過 player 的外來鍵作為Team 的一部分。
const Player = this.sequelize.define('player', {/* attributes */}); const Team= this.sequelize.define('team', {/* attributes */}); Player.belongsTo(Team); // 將向 Player 新增一個 teamId 屬性以儲存 Team 的主鍵值
預設情況下,將從目標模型名稱和目標主鍵名稱生成 belongsTo 關係的外來鍵。預設的樣式是camelCase
,但是如果源模型配置為underscored: true
,那麼將使用欄位snake_case
建立 foreignKey。比如:
const User = this.sequelize.define('user', {/* attributes */}, {underscored: true}) const Company= this.sequelize.define('company', { uuid: { type: Sequelize.UUID, primaryKey: true } }); // 將用欄位 company_uuid 新增 companyUuid 到 user User.belongsTo(Company);
此外,預設外來鍵可以用foreignKey
選項覆蓋。 當設定外來鍵選項時,Sequelize 將使用設定的引數值:
const User = this.sequelize.define('user', {/* attributes */}) const Company= this.sequelize.define('company', {/* attributes */}); User.belongsTo(Company, { foreignKey: 'fk_company' }); // 將 fk_company 新增到 User
HasOne
HasOne 關聯是在target model 上存在一對一關係的外來鍵的關聯。
const User = sequelize.define('user', {/* ... */}) const Project = sequelize.define('project', {/* ... */}) // 單向關聯 Project.hasOne(User)
以上示例中,hasOne 將向 User 模型新增一個 projectId 屬性。此外,Project.prototype 將根據傳遞給定義的第一個引數獲取 getUser 和 setUser 的方法。 如果啟用了 underscore 樣式,則新增的屬性將是 project_id 而不是 projectId。外來鍵將放在 users 表上。
你也可以定義外來鍵,比如如果你已經有一個現有的資料庫並且想要處理它:
Project.hasOne(User, { foreignKey: 'initiator_id' })
HasOne vs BelongsTo
在 Sequelize 1:1 關係中可以使用 HasOne 和 BelongsTo 進行設定,它們適用於不同的場景。
我們先來定義以下兩個模型:
const Player = this.sequelize.define('player', {/* attributes */}) const Team= this.sequelize.define('team', {/* attributes */});
當我們連線 Sequelize 中的兩個模型時,我們可以將它們稱為一對source 和target 模型。
將Player 作為source 而Team 作為target
Player.belongsTo(Team); //或 Player.hasOne(Team);
將Team 作為source 而Player 作為target
Team.belongsTo(Player); //Or Team.hasOne(Player);
HasOne 和 BelongsTo 將關聯鍵插入到不同的模型中。 HasOne 在target 模型中插入關聯鍵,而 BelongsTo 將關聯鍵插入到source 模型中。
一對一關係
模型定義
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/account.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const Account = sequelize.define("account", { email: { type: Sequelize.CHAR(20), allowNull: false } }); return Account; };
資料庫連線及關係定義
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 Account = require('./model/account')(sequelize); sequelize.sync({ // force: true }) .then(() => { console.log(`Database & tables created!`) // User的例項物件將擁有getAccount、setAccount、createAccount方法 User.hasOne(Account); // 在target模型中插入關聯鍵 // Account的例項物件將擁有getUser、setUser、createUser方法 Account.belongsTo(User); // 將關聯鍵插入到source模型中 }) }) .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;
- 新建 accounts 表
CREATE TABLE IF NOT EXISTS `accounts` ( `id` INTEGER NOT NULL auto_increment , `email` CHAR(20) 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;
通過觀察上面的 accounts 建表語句,我們發現Sequelize 自動為 accounts 表新增了 userId 欄位,同時生成了相應的外來鍵約束。
一般來說,外來鍵約束可能會導致一些效能問題。所以,建表時我們一般會去掉約束,同時給外來鍵加一個索引(加速查詢),但之後的資料的一致性就需要應用層來保證了。
關係操作
- 新增
const user = await User.create({ empId: '1' }); // (1) const account = await user.createAccount({ email: '[email protected]' }); // (2) console.log(account.get({ plain: true }));
步驟一:新建使用者,對應的 SQL 語句如下:
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-09 04:18:23','2018-10-09 04:18:23');
步驟二:建立賬號,對應的 SQL 語句如下:
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'[email protected]','2018-10-09 04:18:23','2018-10-09 04:18:23',1);
可以看出,當呼叫 user.createAccount 方法時,會使用新建使用者的 userId 作為外來鍵在 accounts 表中插入一條新的資料。
- 修改
const user = await User.findById(1); // (1) const newAccount = await Account.create({ email: '[email protected]' }); // (2) user.setAccount(newAccount); // (3) console.log(newAccount.get({ plain: true }));
步驟一:查詢 id 為 1 的使用者,對應的 SQL 語句如下:
SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;
步驟二:建立新賬號,對應的 SQL 語句如下:
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'[email protected]','2018-10-09 05:46:11','2018-10-09 05:46:11');
該 SQL 語句會插入一條新的 account 記錄,此時 userId 的值為空,還未關聯 user。
步驟三:關聯新的賬號,對應的 SQL 語句如下:
UPDATE `accounts` SET `userId`=NULL,`updatedAt`='2018-10-09 05:46:11' WHERE `id` = 1 UPDATE `accounts` SET `userId`=1,`updatedAt`='2018-10-09 05:46:11' WHERE `id` = 2
以上 SQL 語句,首先會找出當前 user 所關聯的 account 並將其 userId 設定為NULL
(為了保證一對一關係)。
然後設定新的 account 的外來鍵 userId 為當前 user 的 id,從而建立關係。
- 刪除
const user = await User.findById(1); // (1) user.setAccount(null); // (2)
步驟一:查詢 id 為 1 的使用者,對應的 SQL 語句如下:
SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;
步驟二:查詢 userId 為 1 的賬號,對應的 SQL 語句如下:
SELECT `id`, `email`, `createdAt`, `updatedAt`, `userId` FROM `accounts` AS `account` WHERE `account`.`userId` = 1 LIMIT 1;
步驟三:當 userId 的賬號存在時,才會執行該步驟,即更新相應的 account 記錄,對應的 SQL 語句如下:
UPDATE `accounts` SET `userId`=NULL,`updatedAt`='2018-10-09 06:19:30' WHERE `id` = 2
通過觀察以上的 SQL 語句,我們發現執行刪除操作時,並不會真正的刪除物理記錄,只是執行對應的軟刪除操作。即通過將外來鍵 userId 設定為NULL
,完成表關係的切除。
- 查詢
const user = await User.findById(1); // (1) user.getAccount(); // (2)
步驟一:查詢 id 為 1 的使用者,對應的 SQL 語句如下:
SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;
步驟二:獲取 id 為 1 的使用者相關聯的賬號,對應的 SQL 語句如下:
SELECT `id`, `email`, `createdAt`, `updatedAt`, `userId` FROM `accounts` AS `account` WHERE `account`.`userId` = 1 LIMIT 1;
以上的 SQL 語句就是根據外來鍵 userId 來獲取相關聯的 account。
eager loading
對於開發者來說,我們更習慣通過.
操作來快速訪問物件的屬性,比如user.account
。前面我們就已經提到過Sequelize
功能很強大,它當然也支援這種操作。但需要藉助Sequelize
的eager loading
(急載入,和懶載入相反)特性來實現。eager loading
的含義是說,取一個模型的時候,同時也自動獲取相關的模型資料。
const user = await User.findById(1, { include: [Account] }); console.log(user.get({ plain: true }));
以上操作對應的 SQL 語句如下:
SELECT `user`.`id`, `user`.`empId`, `user`.`createdAt`, `user`.`updatedAt`, `account`.`id` AS `account.id`, `account`.`email` AS `account.email`, `account`.`createdAt` AS `account.createdAt`, `account`.`updatedAt` AS `account.updatedAt`, `account`.`userId` AS `account.userId` FROM `users` AS `user` LEFT OUTER JOIN `accounts` AS `account` ON `user`.`id` = `account`.`userId` WHERE `user`.`id` = 1;
即通過左外連線在獲取 id 為 1 的使用者時,同時獲取其關聯的賬號。此外,命令列還會輸出相應的 user 物件:
{ id: 1, empId: '1', createdAt: 2018-10-09T04:18:23.000Z, updatedAt: 2018-10-09T04:18:23.000Z, account: { id: 3, email: '[email protected]', createdAt: 2018-10-09T06:49:44.000Z, updatedAt: 2018-10-09T06:49:44.000Z, userId: 1 } }
相關說明
-
要避免重複呼叫
user.createAccount
方法,這樣會在資料庫裡面生成多條userId
一樣的記錄,並不是真正的一對一關係。
const user = await User.findById(1); // (1) const account1 = await user.createAccount({ email: '[email protected]' }); // (2) const account2 = await user.createAccount({ email: '[email protected]' }); // (3)
步驟一:查詢 id 為 1 的使用者,對應的 SQL 語句如下:
SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;
步驟二:為 id 為 1 的使用者,建立新的賬號,對應的 SQL 語句如下:
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'[email protected]','2018-10-09 07:05:57','2018-10-09 07:05:57',1);
步驟三:為 id 為 1 的使用者,建立新的賬號,對應的 SQL 語句如下:
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'[email protected]','2018-10-09 07:05:57','2018-10-09 07:05:57',1);
上面的 SQL 成功執行之後 accounts 表將會生成兩條新記錄,具體如下:
id | createdAt | updatedAt | userId | |
---|---|---|---|---|
4 | [email protected] | 2018-10-09 07:05:57 | 2018-10-09 07:05:57 | 1 |
5 | [email protected] | 2018-10-09 07:05:57 | 2018-10-09 07:05:57 | 1 |
可以看到上面並不是我們想要的結果,在應用層要保證資料一致性,我們就需要遵循良好的編碼約定。新增使用者賬號時使用user.createAccount
方法,更新使用者賬號時就使用user.setAccount
方法。
當然也可以為 account 表的 userId 欄位,增加一個UNIQUE
唯一約束,在資料庫層面保證一致性,這時就需要做好try/catch
,發生插入異常的時候能夠知道是因為插入了為同一使用者建立了多個賬號。具體的實現方式如下:
model/account.js
const Sequelize = require("sequelize"); module.exports = sequelize => { const Account = sequelize.define("account", { email: { type: Sequelize.CHAR(20), allowNull: false }, userId: { type: Sequelize.INTEGER, unique: true }, }); return Account; };
定義一對一的表關係:
// User的例項物件將擁有getAccount、setAccount、createAccount方法 User.hasOne(Account, { foreignKey: 'userId' });
-
上面的示例,我們都是通過 user 物件來操作 account。實際上也可以通過 account 來操作 user,這是因為我們定義了
Account.belongsTo(User)
。在Sequelize 裡面定義關係時,關係的呼叫方會獲得相關聯的方法,一般為了兩邊都能操作,會同時定義雙向關係(這裡雙向關係指的是模型層面,並不會在資料庫表中出現兩個表都加上外來鍵的情況 )。