关联
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
为空,这意味着一个 Bar 可以独立于 Foo 存在。要更改此设置,只需在外键选项中指定 allowNull: false
即可。
Foo.hasOne(Bar, {
foreignKey: {
allowNull: false,
},
});
// "fooId" INTEGER NOT NULL REFERENCES "foos" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT
一对多关系
理念
一对多关联将一个源连接到多个目标,而所有这些目标仅连接到此单个源。
这意味着,与我们必须选择放置外键位置的一对一关联不同,在一对多关联中只有一个选项。例如,如果一个 Foo 有多个 Bars(并且每个 Bar 属于一个 Foo),那么唯一合理的实现是在 Bar
表中添加一个 fooId
列。反之则不可能,因为一个 Foo 有多个 Bars。
目标
在这个例子中,我们有 Team
和 Player
模型。我们想告诉 Sequelize 它们之间存在一对多关系,这意味着一个 Team 有多个 Players,而每个 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 在中间模型上创建唯一键。可以使用 **uniqueKey** 选项覆盖此唯一键的名称。要防止创建此唯一键,请使用 **unique: false** 选项。
Project.belongsToMany(User, {
through: UserProjects,
uniqueKey: 'my_custom_unique',
});
涉及关联的基本查询
在涵盖了定义关联的基础知识之后,我们可以查看涉及关联的查询。关于此问题的最常见查询是读取查询(即 SELECT)。稍后将显示其他类型的查询。
为了研究这一点,我们将考虑一个示例,其中我们有 Ships 和 Captains,以及它们之间的一对一关系。我们将允许外键为空(默认值),这意味着 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);
获取关联 - 急切加载与延迟加载
急切加载和延迟加载的概念对于理解 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. -
或使用关联模型可用的特殊方法/mixin,这些方法将在本页后面进行解释。
注意: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 方法接受与普通查找方法(如 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
)中。
让我们再次使用船舶和船长的示例。此外,我们将假设船长姓名是唯一的
const Ship = sequelize.define('ship', { name: DataTypes.TEXT }, { timestamps: false });
const Captain = sequelize.define(
'captain',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
这样,我们可以将 captainName
而不是 captainId
保存在我们的船舶中,并将其用作关联跟踪器。换句话说,我们的关系将引用目标模型(船长)上的另一列:name
列,而不是引用目标模型(船长)的 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
关联,但与 belongsTo
不同的是,在定义关联时,我们提供 sourceKey
而不是 targetKey
。这是因为与 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
(目标)中的某个字段。