關聯
Sequelize 支援標準的關聯:一對一、一對多 和 多對多。
為此,Sequelize 提供了 四種 關聯類型,應該組合使用來建立它們
HasOne
關聯BelongsTo
關聯HasMany
關聯BelongsToMany
關聯
本指南將首先說明如何定義這四種關聯類型,然後接著說明如何組合它們來定義三種標準關聯類型(一對一、一對多 和 多對多)。
定義 Sequelize 關聯
這四種關聯類型的定義方式非常相似。假設我們有兩個模型,A
和 B
。告訴 Sequelize 您想要這兩者之間建立關聯只需要呼叫一個函式
const A = sequelize.define('A' /* ... */);
const B = sequelize.define('B' /* ... */);
A.hasOne(B); // A HasOne B
A.belongsTo(B); // A BelongsTo B
A.hasMany(B); // A HasMany B
A.belongsToMany(B, { through: 'C' }); // A BelongsToMany B through the junction table C
它們都接受一個選項物件作為第二個參數(前三個選項是可選的,belongsToMany
則為必要選項,至少包含 through
屬性)
A.hasOne(B, {
/* options */
});
A.belongsTo(B, {
/* options */
});
A.hasMany(B, {
/* options */
});
A.belongsToMany(B, { through: 'C' /* options */ });
定義關聯的順序很重要。換句話說,這四種情況的順序都很重要。在以上所有範例中,A
被稱為 來源 模型,而 B
被稱為 目標 模型。這個術語很重要。
A.hasOne(B)
關聯表示 A
和 B
之間存在一對一關係,外鍵定義在目標模型 (B
) 中。
A.belongsTo(B)
關聯表示 A
和 B
之間存在一對一關係,外鍵定義在來源模型 (A
) 中。
A.hasMany(B)
關聯表示 A
和 B
之間存在一對多關係,外鍵定義在目標模型 (B
) 中。
這三個呼叫會導致 Sequelize 自動將外鍵新增到適當的模型中(除非它們已經存在)。
A.belongsToMany(B, { through: 'C' })
關聯表示 A
和 B
之間存在多對多關係,使用資料表 C
作為連接資料表,其中將具有外鍵(例如,aId
和 bId
)。Sequelize 會自動建立此模型 C
(除非它已經存在)並在其上定義適當的外鍵。
注意:在上述 belongsToMany
的範例中,一個字串 ('C'
) 被傳遞到 through 選項。在這種情況下,Sequelize 會自動產生一個具有此名稱的模型。但是,如果您已經定義了模型,也可以直接傳遞模型。
這些是每種關聯類型中涉及的主要概念。但是,這些關聯通常成對使用,以便更好地與 Sequelize 搭配使用。稍後將會看到這一點。
建立標準關聯
如前所述,通常 Sequelize 關聯會成對定義。總結如下
- 若要建立 一對一 關係,請一起使用
hasOne
和belongsTo
關聯; - 若要建立 一對多 關係,請一起使用
hasMany
和belongsTo
關聯; - 若要建立 多對多 關係,請一起使用兩個
belongsToMany
呼叫。- 注意:還有一個超級多對多關係,它一次使用六個關聯,將在進階多對多關係指南中討論。
接下來將詳細介紹所有內容。使用這些配對而不是單一關聯的優勢將在本章末尾討論。
一對一關係
哲學
在深入探討使用 Sequelize 的各個方面之前,回頭思考一下一對一關係會發生什麼事是很有用的。
假設我們有兩個模型,Foo
和 Bar
。我們想要在 Foo 和 Bar 之間建立一對一關係。我們知道在關聯式資料庫中,這將透過在其中一個資料表中建立外鍵來完成。因此,在這種情況下,一個非常重要的問題是:我們希望將這個外鍵放在哪個資料表中?換句話說,我們希望 Foo
具有 barId
欄位,還是 Bar
應該具有 fooId
欄位?
原則上,這兩種選項都是在 Foo 和 Bar 之間建立一對一關係的有效方法。但是,當我們說類似 「Foo 和 Bar 之間存在一對一關係」 時,關係是否為強制性或可選,並不清楚。換句話說,Foo 是否可以在沒有 Bar 的情況下存在?Bar 是否可以在沒有 Foo 的情況下存在?這些問題的答案有助於找出我們希望外鍵欄位在哪裡。
目標
在本範例的其餘部分,假設我們有兩個模型,Foo
和 Bar
。我們想要在它們之間設定一對一關係,以便讓 Bar
取得 fooId
欄位。
實作
達成目標的主要設定如下
Foo.hasOne(Bar);
Bar.belongsTo(Foo);
由於沒有傳遞任何選項,Sequelize 將根據模型的名稱推斷出要做什麼。在這種情況下,Sequelize 知道必須將 fooId
欄位新增到 Bar
。
這樣,在執行上述操作後呼叫 Bar.sync()
將產生以下 SQL(例如在 PostgreSQL 上)
CREATE TABLE IF NOT EXISTS "foos" (
/* ... */
);
CREATE TABLE IF NOT EXISTS "bars" (
/* ... */
"fooId" INTEGER REFERENCES "foos" ("id") ON DELETE SET NULL ON UPDATE CASCADE
/* ... */
);
選項
可以將各種選項作為關聯呼叫的第二個參數傳遞。
onDelete
和 onUpdate
例如,若要設定 ON DELETE
和 ON UPDATE
行為,您可以執行以下操作
Foo.hasOne(Bar, {
onDelete: 'RESTRICT',
onUpdate: 'RESTRICT',
});
Bar.belongsTo(Foo);
可能的選擇有 RESTRICT
、CASCADE
、NO ACTION
、SET DEFAULT
和 SET NULL
。
一對一關聯的預設值是 ON DELETE
的 SET NULL
和 ON UPDATE
的 CASCADE
。
自訂外鍵
上述顯示的 hasOne
和 belongsTo
呼叫都會推斷出要建立的外鍵應該稱為 fooId
。若要使用不同的名稱,例如 myFooId
// Option 1
Foo.hasOne(Bar, {
foreignKey: 'myFooId',
});
Bar.belongsTo(Foo);
// Option 2
Foo.hasOne(Bar, {
foreignKey: {
name: 'myFooId',
},
});
Bar.belongsTo(Foo);
// Option 3
Foo.hasOne(Bar);
Bar.belongsTo(Foo, {
foreignKey: 'myFooId',
});
// Option 4
Foo.hasOne(Bar);
Bar.belongsTo(Foo, {
foreignKey: {
name: 'myFooId',
},
});
如上所示,foreignKey
選項接受字串或物件。當接收到物件時,此物件將用作欄位的定義,就像在標準 sequelize.define
呼叫中所做的一樣。因此,指定諸如 type
、allowNull
、defaultValue
等選項即可運作。
例如,若要使用 UUID
作為外鍵資料類型,而不是預設值 (INTEGER
),您可以簡單地執行以下操作
const { DataTypes } = require('Sequelize');
Foo.hasOne(Bar, {
foreignKey: {
// name: 'myFooId'
type: DataTypes.UUID,
},
});
Bar.belongsTo(Foo);
強制與可選關聯
依預設,關聯會被視為可選。換句話說,在我們的範例中,允許 fooId
為 null,表示一個 Bar 可以在沒有 Foo 的情況下存在。變更此設定只需在外鍵選項中指定 allowNull: false
Foo.hasOne(Bar, {
foreignKey: {
allowNull: false,
},
});
// "fooId" INTEGER NOT NULL REFERENCES "foos" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
一對多關係
哲學
一對多關聯是指一個來源與多個目標連接,而所有這些目標都僅與這單一來源連接。
這意味著,與一對一關聯不同,在一對一關聯中我們必須選擇外鍵的放置位置,但在 一對多關聯中只有一個選擇。例如,如果一個 Foo 有多個 Bar(因此每個 Bar 都屬於一個 Foo),那麼唯一合理的實作方式是在 Bar
表格中設置一個 fooId
欄位。反之則不可能,因為一個 Foo 有多個 Bar。
目標
在這個例子中,我們有 Team
和 Player
模型。我們想告訴 Sequelize 它們之間存在一對多關係,這意味著一個 Team 有多個 Player,而每個 Player 屬於單一的 Team。
實作
主要的方法如下:
Team.hasMany(Player);
Player.belongsTo(Team);
再次強調,如同前面提到的,主要的方法是使用一對 Sequelize 關聯 (hasMany
和 belongsTo
)。
例如,在 PostgreSQL 中,上述設定在 sync()
後會產生以下 SQL:
CREATE TABLE IF NOT EXISTS "Teams" (
/* ... */
);
CREATE TABLE IF NOT EXISTS "Players" (
/* ... */
"TeamId" INTEGER REFERENCES "Teams" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
/* ... */
);
選項
在這種情況下應用的選項與一對一情況相同。例如,要更改外鍵的名稱並確保關係是強制性的,我們可以這樣做:
Team.hasMany(Player, {
foreignKey: 'clubId',
});
Player.belongsTo(Team);
像一對一關係一樣,ON DELETE
預設為 SET NULL
,而 ON UPDATE
預設為 CASCADE
。
多對多關係
概念
多對多關聯將一個來源與多個目標連接,而所有這些目標又可以與第一個來源以外的其他來源連接。
這無法像其他關係一樣,透過在其中一個表格中添加一個外鍵來表示。相反地,使用了連接模型的概念。這會是一個額外的模型(以及資料庫中的額外表格),其中將有兩個外鍵欄位,並追蹤關聯。連接表格有時也稱為關聯表格或透過表格。
目標
在這個例子中,我們將考慮 Movie
和 Actor
模型。一位演員可能參與過多部電影,而一部電影有多位演員參與製作。追蹤關聯的連接表格將被稱為 ActorMovies
,其中包含外鍵 movieId
和 actorId
。
實作
在 Sequelize 中實現此目的的主要方法如下:
const Movie = sequelize.define('Movie', { name: DataTypes.STRING });
const Actor = sequelize.define('Actor', { name: DataTypes.STRING });
Movie.belongsToMany(Actor, { through: 'ActorMovies' });
Actor.belongsToMany(Movie, { through: 'ActorMovies' });
由於在 belongsToMany
呼叫的 through
選項中給定了一個字串,Sequelize 將自動建立 ActorMovies
模型,該模型將充當連接模型。例如,在 PostgreSQL 中:
CREATE TABLE IF NOT EXISTS "ActorMovies" (
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"MovieId" INTEGER REFERENCES "Movies" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"ActorId" INTEGER REFERENCES "Actors" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("MovieId","ActorId")
);
除了字串之外,也支援直接傳遞模型,在這種情況下,給定的模型將被用作連接模型(且不會自動建立模型)。例如:
const Movie = sequelize.define('Movie', { name: DataTypes.STRING });
const Actor = sequelize.define('Actor', { name: DataTypes.STRING });
const ActorMovies = sequelize.define('ActorMovies', {
MovieId: {
type: DataTypes.INTEGER,
references: {
model: Movie, // 'Movies' would also work
key: 'id',
},
},
ActorId: {
type: DataTypes.INTEGER,
references: {
model: Actor, // 'Actors' would also work
key: 'id',
},
},
});
Movie.belongsToMany(Actor, { through: ActorMovies });
Actor.belongsToMany(Movie, { through: ActorMovies });
以上在 PostgreSQL 中產生以下 SQL,與上面顯示的 SQL 等效:
CREATE TABLE IF NOT EXISTS "ActorMovies" (
"MovieId" INTEGER NOT NULL REFERENCES "Movies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
"ActorId" INTEGER NOT NULL REFERENCES "Actors" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE ("MovieId", "ActorId"), -- Note: Sequelize generated this UNIQUE constraint but
PRIMARY KEY ("MovieId","ActorId") -- it is irrelevant since it's also a PRIMARY KEY
);
選項
與一對一和一對多關係不同,多對多關係的 ON UPDATE
和 ON DELETE
預設值都為 CASCADE
。
Belongs-To-Many 在 through 模型上建立唯一鍵。此唯一鍵名稱可以使用 uniqueKey 選項覆寫。要防止建立此唯一鍵,請使用 unique: false 選項。
Project.belongsToMany(User, {
through: UserProjects,
uniqueKey: 'my_custom_unique',
});
涉及關聯的查詢基礎
在介紹了定義關聯的基礎知識後,我們可以看看涉及關聯的查詢。在這方面最常見的查詢是讀取查詢(即 SELECT)。稍後將介紹其他類型的查詢。
為了研究這一點,我們將考慮一個範例,其中我們有 Ships 和 Captains,它們之間存在一對一關係。我們將允許外鍵為 null(預設值),這意味著 Ship 可以存在而沒有 Captain,反之亦然。
// This is the setup of our models for the examples below
const Ship = sequelize.define(
'ship',
{
name: DataTypes.TEXT,
crewCapacity: DataTypes.INTEGER,
amountOfSails: DataTypes.INTEGER,
},
{ timestamps: false },
);
const Captain = sequelize.define(
'captain',
{
name: DataTypes.TEXT,
skillLevel: {
type: DataTypes.INTEGER,
validate: { min: 1, max: 10 },
},
},
{ timestamps: false },
);
Captain.hasOne(Ship);
Ship.belongsTo(Captain);
提取關聯 - 預先載入 vs 延遲載入
預先載入和延遲載入的概念是了解 Sequelize 中提取關聯如何運作的基礎。延遲載入指的是僅在您真正需要時才提取關聯資料的技術;另一方面,預先載入指的是一開始就透過更大的查詢一次提取所有內容的技術。
延遲載入範例
const awesomeCaptain = await Captain.findOne({
where: {
name: 'Jack Sparrow',
},
});
// Do stuff with the fetched captain
console.log('Name:', awesomeCaptain.name);
console.log('Skill Level:', awesomeCaptain.skillLevel);
// Now we want information about his ship!
const hisShip = await awesomeCaptain.getShip();
// Do stuff with the ship
console.log('Ship Name:', hisShip.name);
console.log('Amount of Sails:', hisShip.amountOfSails);
請注意,在上面的範例中,我們發出了兩個查詢,僅在我們想使用它時才提取相關的 ship。如果我們可能需要或不需要 ship,也許我們只想在少數情況下有條件地提取它,這可能會特別有用;這樣一來,我們可以透過僅在必要時才提取它來節省時間和記憶體。
注意:上面使用的 getShip()
實例方法是 Sequelize 自動添加到 Captain
實例的方法之一。還有其他方法。您將在本指南的後面部分學到更多關於它們的知識。
預先載入範例
const awesomeCaptain = await Captain.findOne({
where: {
name: 'Jack Sparrow',
},
include: Ship,
});
// Now the ship comes with it
console.log('Name:', awesomeCaptain.name);
console.log('Skill Level:', awesomeCaptain.skillLevel);
console.log('Ship Name:', awesomeCaptain.ship.name);
console.log('Amount of Sails:', awesomeCaptain.ship.amountOfSails);
如上所示,在 Sequelize 中,透過使用 include
選項來執行預先載入。請注意,這裡僅對資料庫執行了一個查詢(該查詢將關聯資料與實例一起帶入)。
這只是對 Sequelize 中預先載入的快速介紹。還有很多關於它的內容,您可以在預先載入專用指南中了解。
建立、更新和刪除
上面展示了關於提取涉及關聯資料的查詢基礎知識。對於建立、更新和刪除,您可以選擇以下其中一種方式:
-
直接使用標準模型查詢
// Example: creating an associated model using the standard methods
Bar.create({
name: 'My Bar',
fooId: 5,
});
// This creates a Bar belonging to the Foo of ID 5 (since fooId is
// a regular column, after all). Nothing very clever going on here. -
或使用適用於關聯模型的特殊方法/混入,這些方法稍後將在本頁說明。
注意:save()
實例方法並不知道關聯。換句話說,如果您更改了從與父物件一起預先載入的子物件中的值,則在父物件上呼叫 save()
將完全忽略發生在子物件上的更改。
關聯別名 & 自訂外鍵
在以上所有範例中,Sequelize 自動定義了外鍵名稱。例如,在 Ship 和 Captain 範例中,Sequelize 在 Ship 模型上自動定義了 captainId
欄位。但是,很容易指定自訂外鍵。
讓我們以簡化的形式考慮 Ship 和 Captain 模型,以便專注於目前的主題,如下所示(欄位較少):
const Ship = sequelize.define('ship', { name: DataTypes.TEXT }, { timestamps: false });
const Captain = sequelize.define('captain', { name: DataTypes.TEXT }, { timestamps: false });
有三種方法可以為外鍵指定不同的名稱:
- 直接提供外鍵名稱
- 定義別名
- 同時執行這兩件事
回顧:預設設定
透過簡單地使用 Ship.belongsTo(Captain)
,sequelize 將自動產生外鍵名稱。
Ship.belongsTo(Captain); // This creates the `captainId` foreign key in Ship.
// Eager Loading is done by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON());
// Or by providing the associated model name:
console.log((await Ship.findAll({ include: 'captain' })).toJSON());
// Also, instances obtain a `getCaptain()` method for Lazy Loading:
const ship = Ship.findOne();
console.log((await ship.getCaptain()).toJSON());
直接提供外鍵名稱
外鍵名稱可以直接在關聯定義中透過選項提供,如下所示:
Ship.belongsTo(Captain, { foreignKey: 'bossId' }); // This creates the `bossId` foreign key in Ship.
// Eager Loading is done by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON());
// Or by providing the associated model name:
console.log((await Ship.findAll({ include: 'Captain' })).toJSON());
// Also, instances obtain a `getCaptain()` method for Lazy Loading:
const ship = await Ship.findOne();
console.log((await ship.getCaptain()).toJSON());
定義別名
定義別名比僅僅為外鍵指定自訂名稱更強大。透過一個範例可以更好地理解這一點:
Ship.belongsTo(Captain, { as: 'leader' }); // This creates the `leaderId` foreign key in Ship.
// Eager Loading no longer works by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // Throws an error
// Instead, you have to pass the alias:
console.log((await Ship.findAll({ include: 'leader' })).toJSON());
// Or you can pass an object specifying the model and alias:
console.log(
(
await Ship.findAll({
include: {
model: Captain,
as: 'leader',
},
})
).toJSON(),
);
// Also, instances obtain a `getLeader()` method for Lazy Loading:
const ship = await Ship.findOne();
console.log((await ship.getLeader()).toJSON());
當您需要在相同模型之間定義兩個不同的關聯時,別名特別有用。例如,如果我們有 Mail
和 Person
模型,我們可能想將它們關聯兩次,以表示 Mail 的 sender
和 receiver
。在這種情況下,我們必須為每個關聯使用別名,否則像 mail.getPerson()
這樣的呼叫將會是模稜兩可的。透過 sender
和 receiver
別名,我們將可以使用這兩種方法:mail.getSender()
和 mail.getReceiver()
,它們都傳回 Promise<Person>
。
當為 hasOne
或 belongsTo
關聯定義別名時,您應該使用單數形式的詞(例如,上例中的 leader
)。另一方面,當為 hasMany
和 belongsToMany
定義別名時,您應該使用複數形式。在進階多對多關聯指南中介紹了為多對多關係(使用 belongsToMany
)定義別名的內容。
同時執行這兩件事
我們可以定義別名並同時直接定義外鍵:
Ship.belongsTo(Captain, { as: 'leader', foreignKey: 'bossId' }); // This creates the `bossId` foreign key in Ship.
// Since an alias was defined, eager Loading doesn't work by simply passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // Throws an error
// Instead, you have to pass the alias:
console.log((await Ship.findAll({ include: 'leader' })).toJSON());
// Or you can pass an object specifying the model and alias:
console.log(
(
await Ship.findAll({
include: {
model: Captain,
as: 'leader',
},
})
).toJSON(),
);
// Also, instances obtain a `getLeader()` method for Lazy Loading:
const ship = await Ship.findOne();
console.log((await ship.getLeader()).toJSON());
添加到實例的特殊方法/混入
當在兩個模型之間定義關聯時,這些模型的實例會獲得與其關聯對應項互動的特殊方法。
例如,如果我們有兩個模型 Foo
和 Bar
,並且它們相關聯,則它們的實例將具有以下可用方法/混入,具體取決於關聯類型:
Foo.hasOne(Bar)
fooInstance.getBar()
fooInstance.setBar()
fooInstance.createBar()
範例
const foo = await Foo.create({ name: 'the-foo' });
const bar1 = await Bar.create({ name: 'some-bar' });
const bar2 = await Bar.create({ name: 'another-bar' });
console.log(await foo.getBar()); // null
await foo.setBar(bar1);
console.log((await foo.getBar()).name); // 'some-bar'
await foo.createBar({ name: 'yet-another-bar' });
const newlyAssociatedBar = await foo.getBar();
console.log(newlyAssociatedBar.name); // 'yet-another-bar'
await foo.setBar(null); // Un-associate
console.log(await foo.getBar()); // null
Foo.belongsTo(Bar)
與 Foo.hasOne(Bar)
中的方法相同
fooInstance.getBar()
fooInstance.setBar()
fooInstance.createBar()
Foo.hasMany(Bar)
fooInstance.getBars()
fooInstance.countBars()
fooInstance.hasBar()
fooInstance.hasBars()
fooInstance.setBars()
fooInstance.addBar()
fooInstance.addBars()
fooInstance.removeBar()
fooInstance.removeBars()
fooInstance.createBar()
範例
const foo = await Foo.create({ name: 'the-foo' });
const bar1 = await Bar.create({ name: 'some-bar' });
const bar2 = await Bar.create({ name: 'another-bar' });
console.log(await foo.getBars()); // []
console.log(await foo.countBars()); // 0
console.log(await foo.hasBar(bar1)); // false
await foo.addBars([bar1, bar2]);
console.log(await foo.countBars()); // 2
await foo.addBar(bar1);
console.log(await foo.countBars()); // 2
console.log(await foo.hasBar(bar1)); // true
await foo.removeBar(bar2);
console.log(await foo.countBars()); // 1
await foo.createBar({ name: 'yet-another-bar' });
console.log(await foo.countBars()); // 2
await foo.setBars([]); // Un-associate all previously associated bars
console.log(await foo.countBars()); // 0
getter 方法接受選項,就像常用的 finder 方法(例如 findAll
)一樣。
const easyTasks = await project.getTasks({
where: {
difficulty: {
[Op.lte]: 5,
},
},
});
const taskTitles = (
await project.getTasks({
attributes: ['title'],
raw: true,
})
).map(task => task.title);
Foo.belongsToMany(Bar, { through: Baz })
與 Foo.hasMany(Bar)
中的方法相同
fooInstance.getBars()
fooInstance.countBars()
fooInstance.hasBar()
fooInstance.hasBars()
fooInstance.setBars()
fooInstance.addBar()
fooInstance.addBars()
fooInstance.removeBar()
fooInstance.removeBars()
fooInstance.createBar()
對於 belongsToMany 關係,預設情況下 getBars()
將會回傳連接表中的所有欄位。請注意,任何 include
選項都會套用至目標 Bar
物件,因此嘗試像使用 find
方法進行預先載入時一樣設定連接表的選項是不可行的。若要選擇要包含連接表的哪些屬性,getBars()
支援 joinTableAttributes
選項,其用法與在 include
中設定 through.attributes
類似。舉例來說,假設 Foo belongsToMany Bar,以下兩種寫法都會輸出不包含連接表欄位的結果。
const foo = Foo.findByPk(id, {
include: [
{
model: Bar,
through: { attributes: [] },
},
],
});
console.log(foo.bars);
const foo = Foo.findByPk(id);
console.log(foo.getBars({ joinTableAttributes: [] }));
注意:方法名稱
如上例所示,Sequelize 給予這些特殊方法的名稱是由一個前綴(例如 get
、add
、set
)加上模型名稱(首字母大寫)串接而成。必要時會使用複數形式,例如在 fooInstance.setBars()
中。同樣地,不規則複數也會由 Sequelize 自動處理。例如,Person
會變成 People
,而 Hypothesis
會變成 Hypotheses
。
如果定義了別名,則會使用別名而不是模型名稱來形成方法名稱。例如
Task.hasOne(User, { as: 'Author' });
taskInstance.getAuthor()
taskInstance.setAuthor()
taskInstance.createAuthor()
為什麼關聯關係要成對定義?
如前所述以及在大多數範例中所展示的,Sequelize 中的關聯關係通常會成對定義。
- 若要建立 一對一 關係,請一起使用
hasOne
和belongsTo
關聯; - 若要建立 一對多 關係,請一起使用
hasMany
和belongsTo
關聯; - 若要建立 多對多 關係,請一起使用兩個
belongsToMany
呼叫。
當在兩個模型之間定義 Sequelize 關聯關係時,只有*來源*模型*知道*該關係的存在。因此,舉例來說,當使用 Foo.hasOne(Bar)
時(因此 Foo
是來源模型,而 Bar
是目標模型),只有 Foo
知道此關聯關係的存在。這就是為什麼在這種情況下,如上所示,Foo
實例會獲得 getBar()
、setBar()
和 createBar()
方法,而另一方面 Bar
實例則不會獲得任何方法。
同樣地,對於 Foo.hasOne(Bar)
,由於 Foo
知道此關聯關係,我們可以執行預先載入,例如 Foo.findOne({ include: Bar })
,但我們不能執行 Bar.findOne({ include: Foo })
。
因此,為了充分發揮 Sequelize 的功能,我們通常會成對設定關聯關係,以便兩個模型都能*知道該關係*。
實際示範
-
如果我們沒有定義成對的關聯關係,例如只呼叫
Foo.hasOne(Bar)
// This works...
await Foo.findOne({ include: Bar });
// But this throws an error:
await Bar.findOne({ include: Foo });
// SequelizeEagerLoadingError: foo is not associated to bar! -
如果我們按照建議定義成對的關聯關係,即同時使用
Foo.hasOne(Bar)
和Bar.belongsTo(Foo)
// This works!
await Foo.findOne({ include: Bar });
// This also works!
await Bar.findOne({ include: Foo });
多個涉及相同模型的關聯關係
在 Sequelize 中,可以定義相同模型之間的多個關聯關係。您只需要為它們定義不同的別名即可。
Team.hasOne(Game, { as: 'HomeTeam', foreignKey: 'homeTeamId' });
Team.hasOne(Game, { as: 'AwayTeam', foreignKey: 'awayTeamId' });
Game.belongsTo(Team);
建立參照非主鍵欄位的關聯關係
在以上所有範例中,關聯關係都是透過參照相關模型的主鍵(在我們的例子中是它們的 ID)來定義的。然而,Sequelize 允許您定義一個使用另一個欄位,而不是主鍵欄位來建立關聯關係。
這個其他欄位必須具有唯一約束(否則就沒有意義)。
對於 belongsTo
關係
首先,回想一下 A.belongsTo(B)
關聯關係會將外鍵放在*來源模型*(即 A
)中。
讓我們再次使用 Ships 和 Captains 的範例。此外,我們假設船長的名字是唯一的。
const Ship = sequelize.define('ship', { name: DataTypes.TEXT }, { timestamps: false });
const Captain = sequelize.define(
'captain',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
這樣,我們可以將 captainName
保留在我們的 Ships 上,而不是保留 captainId
,並將其用作我們的關聯關係追蹤器。換句話說,我們的關係將參照目標模型(Captain)的另一個欄位:name
欄位,而不是參照目標模型(Captain)的 id
。為了指定這一點,我們必須定義一個*目標鍵*。我們還必須為外鍵本身指定一個名稱。
Ship.belongsTo(Captain, { targetKey: 'name', foreignKey: 'captainName' });
// This creates a foreign key called `captainName` in the source model (Ship)
// which references the `name` field from the target model (Captain).
現在我們可以做一些事情,例如:
await Captain.create({ name: 'Jack Sparrow' });
const ship = await Ship.create({
name: 'Black Pearl',
captainName: 'Jack Sparrow',
});
console.log((await ship.getCaptain()).name); // "Jack Sparrow"
對於 hasOne
和 hasMany
關係
相同的概念可以應用於 hasOne
和 hasMany
關聯關係,但當定義關聯關係時,我們不是提供 targetKey
,而是提供 sourceKey
。這是因為與 belongsTo
不同,hasOne
和 hasMany
關聯關係將外鍵保留在目標模型上。
const Foo = sequelize.define(
'foo',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
const Bar = sequelize.define(
'bar',
{
title: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
const Baz = sequelize.define('baz', { summary: DataTypes.TEXT }, { timestamps: false });
Foo.hasOne(Bar, { sourceKey: 'name', foreignKey: 'fooName' });
Bar.hasMany(Baz, { sourceKey: 'title', foreignKey: 'barTitle' });
// [...]
await Bar.setFoo("Foo's Name Here");
await Baz.addBar("Bar's Title Here");
對於 belongsToMany
關係
相同的概念也可以應用於 belongsToMany
關係。然而,與其他只涉及一個外鍵的情況不同,belongsToMany
關係涉及兩個外鍵,這兩個外鍵都保留在額外的表格(連接表)中。
考慮以下設定
const Foo = sequelize.define(
'foo',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
const Bar = sequelize.define(
'bar',
{
title: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
有四種情況需要考慮
- 我們可能想要一個使用
Foo
和Bar
的預設主鍵的多對多關係
Foo.belongsToMany(Bar, { through: 'foo_bar' });
// This creates a junction table `foo_bar` with fields `fooId` and `barId`
- 我們可能想要一個使用
Foo
的預設主鍵,但使用Bar
的不同欄位來建立多對多關係
Foo.belongsToMany(Bar, { through: 'foo_bar', targetKey: 'title' });
// This creates a junction table `foo_bar` with fields `fooId` and `barTitle`
- 我們可能想要一個使用
Foo
的不同欄位,但使用Bar
的預設主鍵來建立多對多關係
Foo.belongsToMany(Bar, { through: 'foo_bar', sourceKey: 'name' });
// This creates a junction table `foo_bar` with fields `fooName` and `barId`
- 我們可能想要一個同時使用
Foo
和Bar
的不同欄位來建立多對多關係
Foo.belongsToMany(Bar, {
through: 'foo_bar',
sourceKey: 'name',
targetKey: 'title',
});
// This creates a junction table `foo_bar` with fields `fooName` and `barTitle`
注意事項
別忘了,關聯關係中參照的欄位必須具有唯一的約束。否則會拋出錯誤(有時會出現神秘的錯誤訊息,例如 SQLite 的 SequelizeDatabaseError: SQLITE_ERROR: foreign key mismatch - "ships" referencing "captains"
)。
判斷使用 sourceKey
還是 targetKey
的訣竅,就是記住每個關聯關係將其外鍵放置在哪裡。正如本指南開頭所提到的:
-
A.belongsTo(B)
將外鍵保留在來源模型(A
)中,因此參照的鍵在目標模型中,因此使用targetKey
。 -
A.hasOne(B)
和A.hasMany(B)
將外鍵保留在目標模型(B
)中,因此參照的鍵在來源模型中,因此使用sourceKey
。 -
A.belongsToMany(B)
涉及一個額外的表格(連接表),因此sourceKey
和targetKey
都是可用的,其中sourceKey
對應於A
(來源)中的某些欄位,而targetKey
對應於B
(目標)中的某些欄位。