Associations - 关联
Sequelize 支持标准关联关系: 一对一, 一对多 和 多对多.
为此,Sequelize 提供了 四种 关联类型,并将它们组合起来以创建关联:
HasOne
关联类型BelongsTo
关联类型HasMany
关联类型BelongsToMany
关联类型
该指南将讲解如何定义这四种类型的关联,然后讲解如何将它们组合来定义三种标准关联类型(一对一, 一对多 和 多对多).
定义 Sequelize 关联
四种关联类型的定义非常相似. 假设我们有两个模型 A
和 B
. 告诉 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)
关联意味着 A
和 B
之间存在一对一的关系,外键在目标模型(B
)中定义.
A.belongsTo(B)
关联意味着 A
和 B
之间存在一对一的关系,外键在源模型中定义(A
).
A.hasMany(B)
关联意味着 A
和 B
之间存在一对多关系,外键在目标模型(B
)中定义.
这三个调用将导致 Sequelize 自动将外键添加到适当的模型中(除非它们已经存在).
A.belongsToMany(B, { through: 'C' })
关联意味着将表 C
用作联结表,在 A
和 B
之间存在多对多关系. 具有外键(例如,aId
和 bId
). Sequelize 将自动创建此模型 C
(除非已经存在),并在其上定义适当的外键.
注意:在上面的 belongsToMany
示例中,字符串('C'
)被传递给 through
参数. 在这种情况下,Sequelize 会自动使用该名称生成模型. 但是,如果已经定义了模型,也可以直接传递模型.
这些是每种关联类型中涉及的主要思想. 但是,这些关系通常成对使用,以便 Sequelize 更好地使用. 这将在后文中看到.
创建标准关系
如前所述,Sequelize 关联通常成对定义. 综上所述:
- 创建一个 一对一 关系,
hasOne
和belongsTo
关联一起使用; - 创建一个 一对多 关系,
hasMany
hebelongsTo
关联一起使用; - 创建一个 多对多 关系, 两个
belongsToMany
调用一起使用.- 注意: 还有一个 超级多对多 关系,一次使用六个关联,将在高级多对多关系指南中进行讨论.
接下来将进行详细介绍. 本章末尾将讨论使用这些成对而不 是单个关联的优点.
一对一关系
哲理
在深入探讨使用 Sequelize 的各个方面之前,退后一步来考虑一对一关系会发生什么是很有用的.
假设我们有两个模型,Foo
和 Bar
.我们要在 Foo 和 Bar 之间建立一对一的关系.我们知道在关系数据库中,这将通过在其中一个表中建立外键来完成.因此,在这种情况下,一个非常关键的问题是:我们希望该外键在哪个表中?换句话说,我们是要 Foo
拥有 barId
列,还是 Bar
应当拥有 fooId
列?
原则上,这两个选择都是在 Foo 和 Bar 之间建立一对一关系的有效方法.但是,当我们说 "Foo 和 Bar 之间存在一对一关系" 时,尚不清楚该关系是 强制性 的还是可选的.换句话说,Foo 是否可以没有 Bar 而存在? Foo 的 Bar 可以存在吗?这些问题的答案有助于帮我们弄清楚外键列在哪里.
目标
对于本示例的其余部分,我们假设我们有两个模型,即 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
:
// 方法 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);
强制性与可选性关联
默认情况下,该关联被视为可选. 换句话说,在我们的示例中,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 有很多 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
.
多对多关系
原理
多对多关联将一个源与多个目标相连,而所有这些目标又可以与第一个目标之外的其他源相连.
不能像其他关系那样通过向其中一个表添加一个外键来表示这一点. 取而代之的是使用联结模型的概念. 这将是一个额外的模型(以及数据库中的额外表),它将具有两个外键列并跟踪关联. 联结表有时也称为 join table 或 through table.
目标
对于此示例,我们将考虑模型 Movie
和 Actor
. 一位 actor 可能参与了许多 movies,而一部 movie 中有许多 actors 参与了其制作. 跟踪关联的联结表将被称为 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' 也可以使用
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
);
参数
与一对一和一对多关系不同,对于多对多关系,ON UPDATE
和 ON DELETE
的默认值为 CASCADE
.
当模型中不存在主键时,Belongs-to-Many 将创建一个唯一键. 可以使用 uniqueKey 参数覆盖此唯一键名. 若不希望产生唯一键, 可以使用 unique: false 参数.
Project.belongsToMany(User, { through: UserProjects, uniqueKey: 'my_custom_unique' })
基本的涉及关联的查询
了解了定义关联的基础知识之后,我们可以查看涉及关联的查询. 最常见查询是 read 查询(即 SELECT). 稍后,将展示其他类型的查询.
为了研究这一点,我们将思考一个例子,其中有船和船长,以及它们之间的一对一关系. 我们将在外键上允许 null(默认值),这意味着船可以在没有船长的情况下存在,反之亦然.
// 这是我们用于以下示例的模型的设置
const Ship = sequelize.define('ship', {
name: DataTypes.STRING,
crewCapacity: DataTypes.INTEGER,
amountOfSails: DataTypes.INTEGER
}, { timestamps: false });
const Captain = sequelize.define('captain', {
name: DataTypes.STRING,
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"
}
});
// 用获取到的 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 实例的方法之一. 还有其他方法, 你将在本指南的后面部分进一步了解它们.
预先加载示例
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 中预先加载的简单介绍. 还有更多内容,你可以在预先加载的专用指南中学习
创建, 更新和删除
上面显示了查询有关关联 的数据的基础知识. 对于创建,更新和删除,你可以:
-
直接使用标准模型查询:
// 示例:使用标准方法创建关联的模型
Bar.create({
name: 'My Bar',
fooId: 5
});
// 这将创建一个属于 ID 5 的 Foo 的 Bar
// 这里没有什么特别的东西 -
或使用关联模型可用的 特殊方法/混合 ,这将在本文稍后进行解释.
注意: save()
实例方法 并不知道关联关系. 如果你修改了 父级 对象预先加载的 子级 的值,那么在父级上调用 save()
将会忽略子级上发生的修改.