sequelize數據庫模型Associations關聯文檔詳解


  Sequelize 支持標准關聯關系: 一對一, 一對多多對多.

  為此,Sequelize 提供了 四種 關聯類型,並將它們組合起來以創建關聯:

  • HasOne 關聯類型
  • BelongsTo 關聯類型
  • HasMany 關聯類型
  • BelongsToMany 關聯類型

  該指南將講解如何定義這四種類型的關聯,然后講解如何將它們組合來定義三種標准關聯類型(一對一, 一對多多對多)

一、定義 Sequelize 關聯

  四種關聯類型的定義非常相似,假設我們有兩個模型 AB. 告訴 Sequelize 兩者之間的關聯僅需要調用一個函數:

const A = sequelize.define('A', /* ... */); const B = sequelize.define('B', /* ... */); A.hasOne(B); // A 有一個 B
A.belongsTo(B); // A 屬於 B
A.hasMany(B); // A 有多個 B
A.belongsToMany(B, { through: 'C' }); // A 屬於多個 B , 通過聯結表 C

  它們都接受一個對象作為第二個參數(前三個參數是可選的,而對於包含 through 屬性的 belongsToMany 則是必需的): They all accept an options object as a second parameter

A.hasOne(B, { /* 參數 */ }); A.belongsTo(B, { /* 參數 */ }); A.hasMany(B, { /* 參數 */ }); A.belongsToMany(B, { through: 'C', /* 參數 */ });

  關聯的定義順序是有關系的。換句話說,對於這四種情況,定義順序很重要. 在上述所有示例中,A 稱為 模型,而 B 稱為 目標 模型. 此術語很重要.

  A.hasOne(B) 關聯意味着 AB 之間存在一對一的關系,外鍵在目標模型(B)中定義.

  A.belongsTo(B)關聯意味着 AB 之間存在一對一的關系,外鍵在源模型中定義(A).

  A.hasMany(B) 關聯意味着 AB 之間存在一對多關系,外鍵在目標模型(B)中定義.

  這三個調用將導致 Sequelize 自動將外鍵添加到適當的模型中(除非它們已經存在).

  A.belongsToMany(B, { through: 'C' }) 關聯意味着將表 C 用作聯結表,在 AB 之間存在多對多關系. 具有外鍵(例如,aIdbId). Sequelize 將自動創建此模型 C(除非已經存在),並在其上定義適當的外鍵.

  注意:在上面的 belongsToMany 示例中,字符串('C')被傳遞給 through 參數. 在這種情況下,Sequelize 會自動使用該名稱生成模型. 但是,如果已經定義了模型,也可以直接傳遞模型.

  這些是每種關聯類型中涉及的主要思想. 但是,這些關系通常成對使用,以便 Sequelize 更好地使用. 這將在后文中看到.

二、創建標准關系

  如前所述,Sequelize 關聯通常成對定義. 綜上所述:

  • 創建一個 一對一 關系, hasOnebelongsTo 關聯一起使用;
  • 創建一個 一對多 關系, hasMany he belongsTo 關聯一起使用;
  • 創建一個 多對多 關系, 兩個 belongsToMany 調用一起使用.

  接下來將進行詳細介紹. 本章末尾將討論使用這些成對而不是單個關聯的優點.

三、一對一關系

1、原理

  在深入探討使用 Sequelize 的各個方面之前,退后一步來考慮一對一關系會發生什么是很有用的.

  假設我們有兩個模型,FooBar.我們要在 Foo 和 Bar 之間建立一對一的關系.我們知道在關系數據庫中,這將通過在其中一個表中建立外鍵來完成.因此,在這種情況下,一個非常關鍵的問題是:我們希望該外鍵在哪個表中?換句話說,我們是要 Foo 擁有 barId 列,還是 Bar 應當擁有 fooId 列?

  原則上,這兩個選擇都是在 Foo 和 Bar 之間建立一對一關系的有效方法.但是,當我們說 "Foo 和 Bar 之間存在一對一關系" 時,尚不清楚該關系是 強制性 的還是可選的.換句話說,Foo 是否可以沒有 Bar 而存在? Foo 的 Bar 可以存在嗎?這些問題的答案有助於幫我們弄清楚外鍵列在哪里.

2、目標

  對於本示例的其余部分,我們假設我們有兩個模型,即 FooBar. 我們想要在它們之間建立一對一的關系,以便 Bar 獲得 fooId 列.

3、實踐

  實現該目標的主要設置如下:

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 /* ... */ );

4、參數

  可以將各種參數作為關聯調用的第二個參數傳遞.

(1)onDeleteonUpdate

  例如,要配置 ON DELETEON UPDATE 行為,你可以執行以下操作

Foo.hasOne(Bar, { onDelete: 'RESTRICT', onUpdate: 'RESTRICT' }); Bar.belongsTo(Foo);

  可用的參數為 RESTRICT, CASCADE, NO ACTION, SET DEFAULTSET NULL.

  一對一關聯的默認值, ON DELETESET NULLON UPDATECASCADE.

5、自定義外鍵

  上面顯示的 hasOnebelongsTo 調用都會推斷出要創建的外鍵應稱為 fooId. 如要使用其他名稱,例如 myFooId

// 方法 1
Foo.hasOne(Bar, { foreignKey: 'myFooId' }); Bar.belongsTo(Foo); // 方法 2
Foo.hasOne(Bar, { foreignKey: { name: 'myFooId' } }); Bar.belongsTo(Foo); // 方法 3
Foo.hasOne(Bar); Bar.belongsTo(Foo, { foreignKey: 'myFooId' }); // 方法 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);

6、強制性與可選性關聯

  默認情況下,該關聯被視為可選. 換句話說,在我們的示例中,fooId 允許為空,這意味着一個 Bar 可以不存在 Foo 而存在. 只需在外鍵選項中指定 allowNull: false 即可更改此設置:

Foo.hasOne(Bar, { foreignKey: { allowNull: false } }); // "fooId" INTEGER NOT NULL REFERENCES "foos" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT

四、一對多關系

1、原理

  一對多關聯將一個源與多個目標連接,而所有這些目標僅與此單個源連接.

  這意味着,與我們必須選擇放置外鍵的一對一關聯不同,在一對多關聯中只有一個選項. 例如,如果一個 Foo 有很多 Bar(因此每個 Bar 都屬於一個 Foo),那么唯一明智的方式就是在 Bar 表中有一個 fooId 列. 而反過來是不可能的,因為一個 Foo 會有很多 Bar.

2、目標

  在這個例子中,我們有模型 TeamPlayer. 我們要告訴 Sequelize,他們之間存在一對多的關系,這意味着一個 Team 有 Player ,而每個 Player 都屬於一個 Team.

3、實踐

  這樣做的主要方法如下:

Team.hasMany(Player);
Player.belongsTo(Team);

  同樣,實現此目標的主要方法是使用一對多 Sequelize 關聯(hasManybelongsTo).

  例如,在 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, /* ... */ );

4、參數

  在這種情況下要應用的參數與一對一情況相同. 例如,要更改外鍵的名稱並確保該關系是強制性的,我們可以執行以下操作:

Team.hasMany(Player, { foreignKey: 'clubId' }); Player.belongsTo(Team);

  如同一對一關系, ON DELETE 默認為 SET NULLON UPDATE 默認為 CASCADE.

五、多對多關系

1、原理

  多對多關聯將一個源與多個目標相連,而所有這些目標又可以與第一個目標之外的其他源相連.

  不能像其他關系那樣通過向其中一個表添加一個外鍵來表示這一點. 取而代之的是使用聯結模型的概念. 這將是一個額外的模型(以及數據庫中的額外表),它將具有兩個外鍵列並跟蹤關聯. 聯結表有時也稱為 join tablethrough table.

2、目標

  對於此示例,我們將考慮模型 MovieActor. 一位 actor 可能參與了許多 movies,而一部 movie 中有許多 actors 參與了其制作. 跟蹤關聯的聯結表將被稱為 ActorMovies,其中將包含外鍵 movieIdactorId.

3、實踐

  在 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' });

  因為在 belongsToManythrough 參數中給出了一個字符串,所以 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' 也可以使用
      key: 'id' } }, ActorId: { type: DataTypes.INTEGER, references: { model: Actor, // 'Actors' 也可以使用
      key: 'id' } } }); Movie.belongsToMany(Actor, { through: 'ActorMovies' }); Actor.belongsToMany(Movie, { through: 'ActorMovies' });

  上面的代碼在 PostgreSQL 中產生了以下 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"),     -- 注意: Sequelize 產生了這個 UNIQUE 約束,但是 PRIMARY KEY ("MovieId","ActorId")  -- 這沒有關系,因為它也是 PRIMARY KEY );

4、參數

  與一對一和一對多關系不同,對於多對多關系,ON UPDATEON DELETE 的默認值為 CASCADE.

  當模型中不存在主鍵時,Belongs-to-Many 將創建一個唯一鍵。 可以使用 uniqueKey 參數覆蓋此唯一鍵名.

Project.belongsToMany(User, { through: UserProjects, uniqueKey: 'my_custom_unique' })

六、基本的涉及關聯的查詢

  了解了定義關聯的基礎知識之后,我們可以查看涉及關聯的查詢. 最常見查詢是 read 查詢(即 SELECT). 稍后,將展示其他類型的查詢.

  為了研究這一點,我們將思考一個例子,其中有船和船長,以及它們之間的一對一關系. 我們將在外鍵上允許 null(默認值),這意味着船可以在沒有船長的情況下存在,反之亦然.

// 這是我們用於以下示例的模型的設置
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);

1、獲取關聯 - 預先加載 vs 延遲加載

  預先加載和延遲加載的概念是理解獲取關聯如何在 Sequelize 中工作的基礎.

  延遲加載是指僅在確實需要時才獲取關聯數據的技術.

  預先加載是指從一開始就通過較大的查詢一次獲取所有內容的技術.

2、延遲加載示例

const awesomeCaptain = await Captain.findOne({ where: { name: "Jack Sparrow" } }); // 用獲取到的 captain 做點什么
console.log('Name:', awesomeCaptain.name); console.log('Skill Level:', awesomeCaptain.skillLevel); // 現在我們需要有關他的 ship 的信息!
const hisShip = await awesomeCaptain.getShip(); // 用 ship 做點什么
console.log('Ship Name:', hisShip.name); console.log('Amount of Sails:', hisShip.amountOfSails);

  請注意,在上面的示例中,我們進行了兩個查詢,僅在要使用它時才獲取關聯的 ship. 如果我們可能需要也可能不需要這艘 ship,或者我們只想在少數情況下有條件地取回它,這會特別有用; 這樣,我們可以僅在必要時提取,從而節省時間和內存.

  注意:上面使用的 getShip() 實例方法是 Sequelize 自動添加到 Captain 實例的方法之一. 還有其他方法, 你將在本指南的后面部分進一步了解它們.

3、預先加載示例

const awesomeCaptain = await Captain.findOne({ where: { name: "Jack Sparrow" }, include: Ship }); // 現在 ship 跟着一起來了
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);

  如上所示,通過使用 include 參數 在 Sequelize 中執行預先加載. 觀察到這里只對數據庫執行了一個查詢(與實例一起帶回關聯的數據).

  這只是 Sequelize 中預先加載的簡單介紹. 還有更多內容,你可以在預先加載的專用指南中學習

七、創建, 更新和刪除

  上面顯示了查詢有關關聯的數據的基礎知識. 對於創建,更新和刪除,你可以:

1、直接使用標准模型查詢:

// 示例:使用標准方法創建關聯的模型
Bar.create({ name: 'My Bar', fooId: 5 }); // 這將創建一個屬於 ID 5 的 Foo 的 Bar // 這里沒有什么特別的東西

2、或使用關聯模型可用的 特殊方法/混合 ,這將在本文稍后進行解釋.

  注意: save()實例方法 並不知道關聯關系. 如果你修改了 父級 對象預先加載的 子級 的值,那么在父級上調用 save() 將會忽略子級上發生的修改.

八、關聯別名 & 自定義外鍵

  在以上所有示例中,Sequelize 自動定義了外鍵名稱. 例如,在船和船長示例中,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 });

  有三種方法可以為外鍵指定不同的名稱:

  • 通過直接提供外鍵名稱
  • 通過定義別名
  • 通過兩個方法同時進行

1、回顧: 默認設置

  通過簡單地使用 Ship.belongsTo(Captain),sequelize 將自動生成外鍵名稱:

Ship.belongsTo(Captain); // 這將在 Ship 中創建 `captainId` 外鍵. // 通過將模型傳遞給 `include` 來完成預先加載:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // 或通過提供關聯的模型名稱:
console.log((await Ship.findAll({ include: 'captain' })).toJSON()); // 同樣,實例獲得用於延遲加載的 `getCaptain()` 方法:
const ship = Ship.findOne(); console.log((await ship.getCaptain()).toJSON());

2、直接提供外鍵名稱

  可以直接在關聯定義的參數中提供外鍵名稱,如下所示:

Ship.belongsTo(Captain, { foreignKey: 'bossId' }); // 這將在 Ship 中創建 `bossId` 外鍵. // 通過將模型傳遞給 `include` 來完成預先加載:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // 或通過提供關聯的模型名稱:
console.log((await Ship.findAll({ include: 'Captain' })).toJSON()); // 同樣,實例獲得用於延遲加載的 `getCaptain()` 方法:
const ship = Ship.findOne(); console.log((await ship.getCaptain()).toJSON());

3、定義別名

  定義別名比簡單指定外鍵的自定義名稱更強大. 通過一個示例可以更好地理解這一點:

Ship.belongsTo(Captain, { as: 'leader' }); // 這將在 Ship 中創建 `leaderId` 外鍵. // 通過將模型傳遞給 `include` 不能再觸發預先加載:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // 引發錯誤 // 相反,你必須傳遞別名:
console.log((await Ship.findAll({ include: 'leader' })).toJSON()); // 或者,你可以傳遞一個指定模型和別名的對象:
console.log((await Ship.findAll({ include: { model: Captain, as: 'leader' } })).toJSON()); // 同樣,實例獲得用於延遲加載的 `getLeader()`方法:
const ship = Ship.findOne(); console.log((await ship.getLeader()).toJSON());

  當你需要在同一模型之間定義兩個不同的關聯時,別名特別有用. 例如,如果我們有MailPerson 模型,則可能需要將它們關聯兩次,以表示郵件的 senderreceiver. 在這種情況下,我們必須為每個關聯使用別名,因為否則,諸如 mail.getPerson() 之類的調用將是模棱兩可的. 使用 senderreceiver 別名,我們將有兩種可用的可用方法:mail.getSender()mail.getReceiver(),它們都返回一個Promise<Person>.

  在為 hasOnebelongsTo 關聯定義別名時,應使用單詞的單數形式(例如上例中的 leader). 另一方面,在為 hasManybelongsToMany 定義別名時,應使用復數形式. 高級多對多關聯指南中介紹了定義多對多關系(帶有belongsToMany)的別名.

4、兩者都做

  我們可以定義別名,也可以直接定義外鍵:

Ship.belongsTo(Captain, { as: 'leader', foreignKey: 'bossId' }); // 這將在 Ship 中創建 `bossId` 外鍵. // 由於定義了別名,因此僅通過將模型傳遞給 `include`,預先加載將不起作用:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // 引發錯誤 // 相反,你必須傳遞別名:
console.log((await Ship.findAll({ include: 'leader' })).toJSON()); // 或者,你可以傳遞一個指定模型和別名的對象:
console.log((await Ship.findAll({ include: { model: Captain, as: 'leader' } })).toJSON()); // 同樣,實例獲得用於延遲加載的 `getLeader()` 方法:
const ship = Ship.findOne(); console.log((await ship.getLeader()).toJSON());

九、添加到實例的特殊方法

  當兩個模型之間定義了關聯時,這些模型的實例將獲得特殊的方法來與其關聯的另一方進行交互.

  例如,如果我們有兩個模型 FooBar,並且它們是關聯的,則它們的實例將具有以下可用的方法,具體取決於關聯類型:

1、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

2、Foo.belongsTo(Bar)

  來自 Foo.hasOne(Bar) 的相同內容:

  • fooInstance.getBar()
  • fooInstance.setBar()
  • fooInstance.createBar()

3、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([]); // 取消關聯所有先前關聯的 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);

4、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()

5、注意: 方法名稱

  如上面的示例所示,Sequelize 賦予這些特殊方法的名稱是由前綴(例如,get,add,set)和模型名稱(首字母大寫)組成的. 必要時,可以使用復數形式,例如在 fooInstance.setBars() 中. 同樣,不規則復數也由 Sequelize 自動處理. 例如,Person 變成 People 或者 Hypothesis 變成 Hypotheses.

  如果定義了別名,則將使用別名代替模型名稱來形成方法名稱. 例如:

Task.hasOne(User, { as: 'Author' });
  • taskInstance.getAuthor()
  • taskInstance.setAuthor()
  • taskInstance.createAuthor()

十、為什么關聯是成對定義的?

  如前所述,就像上面大多數示例中展示的,Sequelize 中的關聯通常成對定義:

  • 創建一個 一對一 關系, hasOnebelongsTo 關聯一起使用;
  • 創建一個 一對多 關系, hasMany he 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 的作用,我們通常成對設置關系,以便兩個模型都 互相知曉.

1、如果我們未定義關聯對,則僅調用 Foo.hasOne(Bar):

// 這有效...
await Foo.findOne({ include: Bar }); // 但這會引發錯誤:
await Bar.findOne({ include: Foo }); // SequelizeEagerLoadingError: foo is not associated to bar!

2、如果我們按照建議定義關聯對, 即, Foo.hasOne(Bar)Bar.belongsTo(Foo):

// 這有效
await Foo.findOne({ include: Bar }); // 這也有效!
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 允許你定義一個關聯,該關聯使用另一個字段而不是主鍵字段來建立關聯.

  此其他字段必須對此具有唯一的約束(否則,這將沒有意義).

1、對於 belongsTo 關系

  首先,回想一下 A.belongsTo(B) 關聯將外鍵放在 源模型 中(即,在 A 中).

  讓我們再次使用"船和船長"的示例. 此外,我們將假定船長姓名是唯一的:

const Ship = sequelize.define('ship', { name: DataTypes.TEXT }, { timestamps: false }); const Captain = sequelize.define('captain', { name: { type: DataTypes.TEXT, unique: true } }, { timestamps: false });

  這樣,我們不用在 Ship 上保留 captainId,而是可以保留 captainName 並將其用作關聯跟蹤器.

  換句話說,我們的關系將引用目標模型上的另一列:name 列,而不是從目標模型(Captain)中引用 id. 為了說明這一點,我們必須定義一個 目標鍵. 我們還必須為外鍵本身指定一個名稱:

Ship.belongsTo(Captain, { targetKey: 'name', foreignKey: 'captainName' }); // 這將在源模型(Ship)中創建一個名為 `captainName` 的外鍵, // 該外鍵引用目標模型(Captain)中的 `name` 字段.

  現在我們可以做類似的事情:

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"

2、對於 hasOnehasMany 關系

  可以將完全相同的想法應用於 hasOnehasMany 關聯,但是在定義關聯時,我們提供了 sourceKey,而不是提供 targetKey. 這是因為與 belongsTo 不同,hasOnehasMany關聯將外鍵保留在目標模型上:

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");

3、對於 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 });

  有四種情況需要考慮:

(1)我們可能希望使用默認的主鍵為 FooBar 進行多對多關系:

Foo.belongsToMany(Bar, { through: 'foo_bar' }); // 這將創建具有字段 `fooId` 和 `barID` 的聯結表 `foo_bar`.

(2)我們可能希望使用默認主鍵 Foo 的多對多關系,但使用 Bar 的不同字段:

Foo.belongsToMany(Bar, { through: 'foo_bar', targetKey: 'title' }); // 這將創建具有字段 `fooId` 和 `barTitle` 的聯結表 `foo_bar`.

(3)我們可能希望使用 Foo 的不同字段和 Bar 的默認主鍵進行多對多關系:

Foo.belongsToMany(Bar, { through: 'foo_bar', sourceKey: 'name' }); // 這將創建具有字段 `fooName` 和 `barId` 的聯結表 `foo_bar`.

(4)我們可能希望使用不同的字段為 FooBar 使用多對多關系:

Foo.belongsToMany(Bar, { through: 'foo_bar', sourceKey: 'name', targetKey: 'title' }); // 這將創建帶有字段 `fooName` 和 `barTitle` 的聯結表 `foo_bar`.

4、注意

  不要忘記關聯中引用的字段必須具有唯一性約束. 否則,將引發錯誤(對於 SQLite 有時還會發出詭異的錯誤消息,例如 SequelizeDatabaseError: SQLITE_ERROR: foreign key mismatch - "ships" referencing "captains").

  在 sourceKeytargetKey 之間做出決定的技巧只是記住每個關系在何處放置其外鍵. 如本指南開頭所述:

  • A.belongsTo(B) 將外鍵保留在源模型中(A),因此引用的鍵在目標模型中,因此使用了 targetKey.

  • A.hasOne(B)A.hasMany(B) 將外鍵保留在目標模型(B)中,因此引用的鍵在源模型中,因此使用了 sourceKey.

  • A.belongsToMany(B) 包含一個額外的表(聯結表),因此 sourceKeytargetKey 均可用,其中 sourceKey 對應於A(源)中的某個字段而 targetKey 對應於 B(目標)中的某個字段.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM