这没有关系,因为它也是 PRIMARY KEY
与一对一和一对多关系不同,对于多对多关系,ON UPDATE
和 ON 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());
当你需要在同一模型之间定义两个不同的关联时,别名特别有用. 例如,如果我们有Mail
和 Person
模型,则可能需要将它们关联两次,以表示邮件的 sender
和 receiver
. 在这种情况下,我们必须为每个关联使用别名,因为否则,诸如 mail.getPerson()
之类的调用将是模棱两可的. 使用 sender
和 receiver
别名,我们将有两种可用的可用方法:mail.getSender()
和 mail.getReceiver()
,它们都返回一个Promise<Person>
.
在为 hasOne
或 belongsTo
关联定义别名时,应使用单词的单数形式(例如上例中的 leader
). 另一方面,在为 hasMany
和 belongsToMany
定义别名时,应使用复数形式. 高级多对多关联指南中介绍了定义多对多关系(带有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());
九、添加到实例的特殊方法
当两个模型之间定义了关联时,这些模型的实例将获得特殊的方法来与其关联的另一方进行交互.
例如,如果我们有两个模型 Foo
和 Bar
,并且它们是关联的,则它们的实例将具有以下可用的方法,具体取决于关联类型:
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 中的关联通常成对定义:
创建一个 一对一 关系, hasOne
和 belongsTo
关联一起使用;
创建一个 一对多 关系, 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、对于 hasOne
和 hasMany
关系
可以将完全相同的想法应用于 hasOne
和 hasMany
关联,但是在定义关联时,我们提供了 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");
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)我们可能希望使用默认的主键为 Foo
和 Bar
进行多对多关系:
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)我们可能希望使用不同的字段为 Foo
和 Bar
使用多对多关系:
Foo.belongsToMany(Bar, { through: 'foo_bar', sourceKey: 'name', targetKey: 'title' });
// 这将创建带有字段 `fooName` 和 `barTitle` 的联结表 `foo_bar`.
不要忘记关联中引用的字段必须具有唯一性约束. 否则,将引发错误(对于 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
(目标)中的某个字段.