Skip to main content
Version: v6 - stable

关联

Sequelize 支持标准关联:一对一一对多多对多

¥Sequelize supports the standard associations: One-To-One, One-To-Many and Many-To-Many.

为此,Sequelize 提供了四种类型的关联,应将它们组合起来创建它们:

¥To do this, Sequelize provides four types of associations that should be combined to create them:

  • HasOne 关联

    ¥The HasOne association

  • BelongsTo 关联

    ¥The BelongsTo association

  • HasMany 关联

    ¥The HasMany association

  • BelongsToMany 关联

    ¥The BelongsToMany association

本指南将首先解释如何定义这四种关联类型,然后解释如何将它们组合起来定义三种标准关联类型(一对一一对多多对多)。

¥The guide will start explaining how to define these four types of associations, and then will follow up to explain how to combine those to define the three standard association types (One-To-One, One-To-Many and Many-To-Many).

定义 Sequelize 关联

¥Defining the Sequelize associations

四种关联类型的定义方式非常相似。假设我们有两个型号:AB。告诉 Sequelize 你想要两者之间的关联只需要一个函数调用:

¥The four association types are defined in a very similar way. Let's say we have two models, A and B. Telling Sequelize that you want an association between the two needs just a function call:

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

它们都接受选项对象作为第二个参数(前三个参数是可选的,对于至少包含 through 属性的 belongsToMany 是强制的):

¥They all accept an options object as a second parameter (optional for the first three, mandatory for belongsToMany containing at least the through property):

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

定义关联的顺序是相关的。换句话说,对于这四种情况,顺序很重要。在上面的所有示例中,A 称为源模型,B 称为目标模型。这个术语很重要。

¥The order in which the association is defined is relevant. In other words, the order matters, for the four cases. In all examples above, A is called the source model and B is called the target model. This terminology is important.

A.hasOne(B) 关联意味着 AB 之间存在一对一关系,外键在目标模型 (B) 中定义。

¥The A.hasOne(B) association means that a One-To-One relationship exists between A and B, with the foreign key being defined in the target model (B).

A.belongsTo(B) 关联意味着 AB 之间存在一对一关系,外键在源模型 (A) 中定义。

¥The A.belongsTo(B) association means that a One-To-One relationship exists between A and B, with the foreign key being defined in the source model (A).

A.hasMany(B) 关联意味着 AB 之间存在一对多关系,外键在目标模型 (B) 中定义。

¥The A.hasMany(B) association means that a One-To-Many relationship exists between A and B, with the foreign key being defined in the target model (B).

这三个调用将导致 Sequelize 自动将外键添加到适当的模型中(除非它们已经存在)。

¥These three calls will cause Sequelize to automatically add foreign keys to the appropriate models (unless they are already present).

A.belongsToMany(B, { through: 'C' }) 关联意味着 AB 之间存在多对多关系,使用表 C 作为 连接表,该表将具有外键(例如 aIdbId)。Sequelize 将自动创建此模型 C(除非它已经存在)并在其上定义适当的外键。

¥The A.belongsToMany(B, { through: 'C' }) association means that a Many-To-Many relationship exists between A and B, using table C as junction table, which will have the foreign keys (aId and bId, for example). Sequelize will automatically create this model C (unless it already exists) and define the appropriate foreign keys on it.

注意:在上面的 belongsToMany 示例中,字符串 ('C') 被传递给 through 选项。在这种情况下,Sequelize 会自动生成具有此名称的模型。但是,如果你已经定义了模型,也可以直接传递模型。

¥Note: In the examples above for belongsToMany, a string ('C') was passed to the through option. In this case, Sequelize automatically generates a model with this name. However, you can also pass a model directly, if you have already defined it.

这些是每种类型的关联所涉及的主要思想。然而,这些关系通常成对使用,以便更好地使用 Sequelize。这一点稍后会看到。

¥These are the main ideas involved in each type of association. However, these relationships are often used in pairs, in order to enable better usage with Sequelize. This will be seen later on.

创建标准关系

¥Creating the standard relationships

如前所述,Sequelize 关联通常是成对定义的。总之:

¥As mentioned, usually the Sequelize associations are defined in pairs. In summary:

  • 要创建一对一关系,需要同时使用 hasOnebelongsTo 关联;

    ¥To create a One-To-One relationship, the hasOne and belongsTo associations are used together;

  • 要创建一对多关系,需要同时使用 hasManybelongsTo 关联;

    ¥To create a One-To-Many relationship, the hasMany and belongsTo associations are used together;

  • 要创建多对多关系,需要一起使用两个 belongsToMany 调用。

    ¥To create a Many-To-Many relationship, two belongsToMany calls are used together.

这一切都将在接下来详细地看到。使用这些对而不是单个关联的优点将在本章末尾讨论。

¥This will all be seen in detail next. The advantages of using these pairs instead of one single association will be discussed in the end of this chapter.

一对一的关系

¥One-To-One relationships

哲学

¥Philosophy

在深入研究使用 Sequelize 的各个方面之前,退后一步考虑一下一对一关系会发生什么是很有用的。

¥Before digging into the aspects of using Sequelize, it is useful to take a step back to consider what happens with a One-To-One relationship.

假设我们有两个型号:FooBar。我们希望在 Foo 和 Bar 之间建立一对一的关系。我们知道,在关系数据库中,这将通过在其中一个表中建立外键来完成。所以在这种情况下,一个非常相关的问题是:我们希望这个外键位于哪个表中?换句话说,我们是否希望 FoobarId 列,或者 Bar 应该有 fooId 列?

¥Let's say we have two models, Foo and Bar. We want to establish a One-To-One relationship between Foo and Bar. We know that in a relational database, this will be done by establishing a foreign key in one of the tables. So in this case, a very relevant question is: in which table do we want this foreign key to be? In other words, do we want Foo to have a barId column, or should Bar have a fooId column instead?

原则上,这两个选项都是在 Foo 和 Bar 之间建立一对一关系的有效方法。然而,当我们说像 "Foo 和 Bar 之间存在一对一的关系" 这样的东西时,并不清楚这种关系是强制性的还是可选的。换句话说,Foo 可以在没有 Bar 的情况下存在吗?Bar 可以在没有 Foo 的情况下存在吗?这些问题的答案有助于确定我们想要外键列的位置。

¥In principle, both options are a valid way to establish a One-To-One relationship between Foo and Bar. However, when we say something like "there is a One-To-One relationship between Foo and Bar", it is unclear whether or not the relationship is mandatory or optional. In other words, can a Foo exist without a Bar? Can a Bar exist without a Foo? The answers to these questions help figuring out where we want the foreign key column to be.

目标

¥Goal

对于本示例的其余部分,我们假设有两个模型:FooBar。我们希望在它们之间建立一对一的关系,以便 Bar 获得 fooId 列。

¥For the rest of this example, let's assume that we have two models, Foo and Bar. We want to setup a One-To-One relationship between them such that Bar gets a fooId column.

执行

¥Implementation

实现该目标的主要设置如下:

¥The main setup to achieve the goal is as follows:

Foo.hasOne(Bar);
Bar.belongsTo(Foo);

由于没有传递任何选项,Sequelize 将从模型的名称推断要做什么。在这种情况下,Sequelize 知道必须将 fooId 列添加到 Bar

¥Since no option was passed, Sequelize will infer what to do from the names of the models. In this case, Sequelize knows that a fooId column must be added to Bar.

这样,在上述之后调用 Bar.sync() 将产生以下 SQL(例如在 PostgreSQL 上):

¥This way, calling Bar.sync() after the above will yield the following SQL (on PostgreSQL, for example):

CREATE TABLE IF NOT EXISTS "foos" (
/* ... */
);
CREATE TABLE IF NOT EXISTS "bars" (
/* ... */
"fooId" INTEGER REFERENCES "foos" ("id") ON DELETE SET NULL ON UPDATE CASCADE
/* ... */
);

选项

¥Options

各种选项可以作为关联调用的第二个参数传递。

¥Various options can be passed as a second parameter of the association call.

onDeleteonUpdate

¥onDelete and onUpdate

例如,要配置 ON DELETEON UPDATE 行为,你可以执行以下操作:

¥For example, to configure the ON DELETE and ON UPDATE behaviors, you can do:

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

可能的选择是 RESTRICTCASCADENO ACTIONSET DEFAULTSET NULL

¥The possible choices are RESTRICT, CASCADE, NO ACTION, SET DEFAULT and SET NULL.

一对一关联的默认值是 SET NULL 对应 ON DELETECASCADE 对应 ON UPDATE

¥The defaults for the One-To-One associations is SET NULL for ON DELETE and CASCADE for ON UPDATE.

自定义外键

¥Customizing the foreign key

上面显示的 hasOnebelongsTo 调用都将推断要创建的外键应称为 fooId。要使用不同的名称,例如 myFooId

¥Both the hasOne and belongsTo calls shown above will infer that the foreign key to be created should be called fooId. To use a different name, such as 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 调用中一样。因此,指定诸如 typeallowNulldefaultValue 等选项即可。

¥As shown above, the foreignKey option accepts a string or an object. When receiving an object, this object will be used as the definition for the column just like it would do in a standard sequelize.define call. Therefore, specifying options such as type, allowNull, defaultValue, etc, just work.

例如,要使用 UUID 作为外键数据类型而不是默认的 (INTEGER),你可以简单地执行以下操作:

¥For example, to use UUID as the foreign key data type instead of the default (INTEGER), you can simply do:

const { DataTypes } = require('Sequelize');

Foo.hasOne(Bar, {
foreignKey: {
// name: 'myFooId'
type: DataTypes.UUID,
},
});
Bar.belongsTo(Foo);

强制关联与可选关联

¥Mandatory versus optional associations

默认情况下,关联被认为是可选的。换句话说,在我们的示例中,fooId 允许为空,这意味着一个 Bar 可以在没有 Foo 的情况下存在。更改此设置只需在外键选项中指定 allowNull: false 即可:

¥By default, the association is considered optional. In other words, in our example, the fooId is allowed to be null, meaning that one Bar can exist without a Foo. Changing this is just a matter of specifying allowNull: false in the foreign key options:

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

一对多关系

¥One-To-Many relationships

哲学

¥Philosophy

一对多关联将一个源与多个目标连接,而所有这些目标仅与该单个源连接。

¥One-To-Many associations are connecting one source with multiple targets, while all these targets are connected only with this single source.

这意味着,与一对一关联不同,在一对一关联中我们必须选择外键的放置位置,而一对多关联中只有一个选项。例如,如果一个 Foo 有许多 Bar(这样每个 Bar 都属于一个 Foo),那么唯一明智的实现是在 Bar 表中拥有 fooId 列。相反的情况是不可能的,因为一个 Foo 有许多 Bar。

¥This means that, unlike the One-To-One association, in which we had to choose where the foreign key would be placed, there is only one option in One-To-Many associations. For example, if one Foo has many Bars (and this way each Bar belongs to one Foo), then the only sensible implementation is to have a fooId column in the Bar table. The opposite is impossible, since one Foo has many Bars.

目标

¥Goal

在此示例中,我们有型号 TeamPlayer。我们想要告诉 Sequelize,它们之间存在一对多的关系,这意味着一个 Team 有许多 Player,而每个 Player 属于一个 Team。

¥In this example, we have the models Team and Player. We want to tell Sequelize that there is a One-To-Many relationship between them, meaning that one Team has many Players, while each Player belongs to a single Team.

执行

¥Implementation

实现这一点的主要方法如下:

¥The main way to do this is as follows:

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

同样,如前所述,主要方法是使用一对 Sequelize 关联(hasManybelongsTo)。

¥Again, as mentioned, the main way to do it used a pair of Sequelize associations (hasMany and belongsTo).

例如,在 PostgreSQL 中,上述设置将在 sync() 上生成以下 SQL:

¥For example, in PostgreSQL, the above setup will yield the following SQL upon sync():

CREATE TABLE IF NOT EXISTS "Teams" (
/* ... */
);
CREATE TABLE IF NOT EXISTS "Players" (
/* ... */
"TeamId" INTEGER REFERENCES "Teams" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
/* ... */
);

选项

¥Options

在这种情况下要应用的选项与一对一情况相同。例如,要更改外键的名称并确保该关系是强制性的,我们可以这样做:

¥The options to be applied in this case are the same from the One-To-One case. For example, to change the name of the foreign key and make sure that the relationship is mandatory, we can do:

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

与一对一关系一样,ON DELETE 默认为 SET NULLON UPDATE 默认为 CASCADE

¥Like One-To-One relationships, ON DELETE defaults to SET NULL and ON UPDATE defaults to CASCADE.

多对多关系

¥Many-To-Many relationships

哲学

¥Philosophy

多对多关联将一个源与多个目标连接起来,而所有这些目标又可以连接到第一个源之外的其他源。

¥Many-To-Many associations connect one source with multiple targets, while all these targets can in turn be connected to other sources beyond the first.

这不能像其他关系那样通过向其中一个表添加一个外键来表示。相反,使用了 结点模型 的概念。这将是一个额外的模型(以及数据库中的额外表),它将有两个外键列并将跟踪关联。连接表有时也称为连接表或直通表。

¥This cannot be represented by adding one foreign key to one of the tables, like the other relationships did. Instead, the concept of a Junction Model is used. This will be an extra model (and extra table in the database) which will have two foreign key columns and will keep track of the associations. The junction table is also sometimes called join table or through table.

目标

¥Goal

对于本例,我们将考虑型号 MovieActor。一名演员可能参演多部电影,一部电影可能有多名演员参与制作。跟踪关联的联结表称为 ActorMovies,其中包含外键 movieIdactorId

¥For this example, we will consider the models Movie and Actor. One actor may have participated in many movies, and one movie had many actors involved with its production. The junction table that will keep track of the associations will be called ActorMovies, which will contain the foreign keys movieId and actorId.

执行

¥Implementation

在 Sequelize 中实现这一点的主要方法如下:

¥The main way to do this in Sequelize is as follows:

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 中:

¥Since a string was given in the through option of the belongsToMany call, Sequelize will automatically create the ActorMovies model which will act as the junction model. For example, in 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")
);

除了字符串之外,还支持直接传递模型,在这种情况下,给定的模型将用作连接模型(并且不会自动创建模型)。例如:

¥Instead of a string, passing a model directly is also supported, and in that case the given model will be used as the junction model (and no model will be created automatically). For example:

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 等效:

¥The above yields the following SQL in PostgreSQL, which is equivalent to the one shown above:

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

选项

¥Options

与一对一和一对多关系不同,对于多对多关系,ON UPDATEON DELETE 的默认值都是 CASCADE

¥Unlike One-To-One and One-To-Many relationships, the defaults for both ON UPDATE and ON DELETE are CASCADE for Many-To-Many relationships.

Belongs-To-Many 通过模型创建唯一的键。可以使用 uniqueKey 选项覆盖此唯一键名称。要防止创建此唯一密钥,请使用唯一的:错误的选项。

¥Belongs-To-Many creates a unique key on through model. This unique key name can be overridden using uniqueKey option. To prevent creating this unique key, use the unique: false option.

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

涉及关联的查询基础知识

¥Basics of queries involving associations

了解了定义关联的基础知识后,我们可以查看涉及关联的查询。关于此问题最常见的查询是读取查询(即 SELECT)。稍后,将显示其他类型的查询。

¥With the basics of defining associations covered, we can look at queries involving associations. The most common queries on this matter are the read queries (i.e. SELECTs). Later on, other types of queries will be shown.

为了研究这个问题,我们将考虑一个例子,其中有船舶和船长,以及它们之间的一对一关系。我们将允许外键为空(默认),这意味着一艘船可以在没有船长的情况下存在,反之亦然。

¥In order to study this, we will consider an example in which we have Ships and Captains, and a one-to-one relationship between them. We will allow null on foreign keys (the default), meaning that a Ship can exist without a Captain and vice-versa.

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

获取关联 - 预加载与延迟加载

¥Fetching associations - Eager Loading vs Lazy Loading

预加载和延迟加载的概念是理解 Sequelize 中获取关联如何工作的基础。延迟加载是指仅在你真正需要时才获取关联数据的技术;另一方面,预加载是指从一开始就通过更大的查询一次性获取所有内容的技术。

¥The concepts of Eager Loading and Lazy Loading are fundamental to understand how fetching associations work in Sequelize. Lazy Loading refers to the technique of fetching the associated data only when you really want it; Eager Loading, on the other hand, refers to the technique of fetching everything at once, since the beginning, with a larger query.

延迟加载示例

¥Lazy Loading example

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

请注意,在上面的示例中,我们进行了两个查询,仅在我们想要使用相关船舶时才获取它。如果我们可能需要也可能不需要这艘船,也许我们只想有条件地获取它(仅在少数情况下),这可能特别有用;这样我们可以通过仅在必要时获取它来节省时间和内存。

¥Observe that in the example above, we made two queries, only fetching the associated ship when we wanted to use it. This can be especially useful if we may or may not need the ship, perhaps we want to fetch it conditionally, only in a few cases; this way we can save time and memory by only fetching it when necessary.

注意:上面使用的 getShip() 实例方法是 Sequelize 自动添加到 Captain 实例的方法之一。还有其他的。你将在本指南的后面部分了解有关它们的更多信息。

¥Note: the getShip() instance method used above is one of the methods Sequelize automatically adds to Captain instances. There are others. You will learn more about them later in this guide.

预加载示例

¥Eager Loading Example

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 选项执行预加载。请注意,此处仅对数据库执行了一次查询(它带来了关联的数据以及实例)。

¥As shown above, Eager Loading is performed in Sequelize by using the include option. Observe that here only one query was performed to the database (which brings the associated data along with the instance).

这只是 Sequelize 中预加载的快速介绍。还有很多东西,你可以在 预加载专用指南 学到。

¥This was just a quick introduction to Eager Loading in Sequelize. There is a lot more to it, which you can learn at the dedicated guide on Eager Loading.

创建、更新和删除

¥Creating, updating and deleting

上面展示了获取涉及关联的数据的查询的基础知识。对于创建、更新和删除,你可以:

¥The above showed the basics on queries for fetching data involving associations. For creating, updating and deleting, you can either:

  • 直接使用标准模型查询:

    ¥Use the standard model queries directly:

    // 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.
  • 或者使用适用于相关型号的 特殊方法/混合,本页后面将对此进行说明。

    ¥Or use the special methods/mixins available for associated models, which are explained later on this page.

注意:save() 实例方法 不知道关联。换句话说,如果你更改沿着父对象预先加载的子对象的值,则在父对象上调用 save() 将完全忽略子对象上发生的更改。

¥Note: The save() instance method is not aware of associations. In other words, if you change a value from a child object that was eager loaded along a parent object, calling save() on the parent will completely ignore the change that happened on the child.

关联别名和自定义外键

¥Association Aliases & Custom Foreign Keys

在上述所有示例中,Sequelize 自动定义外键名称。例如,在 Ship and Captain 示例中,Sequelize 自动在 Ship 模型上定义了 captainId 字段。但是,指定自定义外键很容易。

¥In all the above examples, Sequelize automatically defined the foreign key names. For example, in the Ship and Captain example, Sequelize automatically defined a captainId field on the Ship model. However, it is easy to specify a custom foreign key.

让我们以简化的形式考虑模型 Ship 和 Captain,只是为了关注当前主题,如下所示(较少字段):

¥Let's consider the models Ship and Captain in a simplified form, just to focus on the current topic, as shown below (less fields):

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

可以通过三种方式为外键指定不同的名称:

¥There are three ways to specify a different name for the foreign key:

  • 通过直接提供外键名称

    ¥By providing the foreign key name directly

  • 通过定义别名

    ¥By defining an Alias

  • 通过做这两件事

    ¥By doing both things

回顾:默认设置

¥Recap: the default setup

通过简单地使用 Ship.belongsTo(Captain),sequelize 将自动生成外键名称:

¥By using simply Ship.belongsTo(Captain), sequelize will generate the foreign key name automatically:

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

直接提供外键名称

¥Providing the foreign key name directly

外键名称可以直接通过关联定义中的选项提供,如下所示:

¥The foreign key name can be provided directly with an option in the association definition, as follows:

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

定义别名

¥Defining an Alias

定义别名比简单地为外键指定自定义名称更强大。通过一个例子可以更好地理解这一点:

¥Defining an Alias is more powerful than simply specifying a custom name for the foreign key. This is better understood with an example:

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

当你需要在同一模型之间定义两个不同的关联时,别名特别有用。例如,如果我们有型号 MailPerson,我们可能希望将它们关联两次,以代表邮件的 senderreceiver。在这种情况下,我们必须为每个关联使用一个别名,否则像 mail.getPerson() 这样的调用将是不明确的。使用 senderreceiver 别名,我们将拥有两种可用且有效的方法:mail.getSender()mail.getReceiver(),两者都返回 Promise<Person>

¥Aliases are especially useful when you need to define two different associations between the same models. For example, if we have the models Mail and Person, we may want to associate them twice, to represent the sender and receiver of the Mail. In this case we must use an alias for each association, since otherwise a call like mail.getPerson() would be ambiguous. With the sender and receiver aliases, we would have the two methods available and working: mail.getSender() and mail.getReceiver(), both of them returning a Promise<Person>.

hasOnebelongsTo 关联定义别名时,应使用单词的单数形式(例如上例中的 leader)。另一方面,在为 hasManybelongsToMany 定义别名时,应使用复数形式。高级多对多关联指南 中介绍了为多对多关系(使用 belongsToMany)定义别名。

¥When defining an alias for a hasOne or belongsTo association, you should use the singular form of a word (such as leader, in the example above). On the other hand, when defining an alias for hasMany and belongsToMany, you should use the plural form. Defining aliases for Many-to-Many relationships (with belongsToMany) is covered in the Advanced Many-to-Many Associations guide.

两件事都做

¥Doing both things

我们可以定义别名,也可以直接定义外键:

¥We can define and alias and also directly define the foreign key:

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

添加到实例的特殊方法/混合

¥Special methods/mixins added to instances

当两个模型之间定义关联时,这些模型的实例将获得特殊的方法来与其关联的对应模型进行交互。

¥When an association is defined between two models, the instances of those models gain special methods to interact with their associated counterparts.

例如,如果我们有两个模型 FooBar,并且它们是关联的,则它们的实例将具有以下可用方法/混合,具体取决于关联类型:

¥For example, if we have two models, Foo and Bar, and they are associated, their instances will have the following methods/mixins available, depending on the association type:

Foo.hasOne(Bar)

  • fooInstance.getBar()

  • fooInstance.setBar()

  • fooInstance.createBar()

示例:

¥Example:

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) 相同:

¥The same ones from 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()

示例:

¥Example:

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):

¥The getter method accepts options just like the usual finder methods (such as 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) 相同:

¥The same ones from 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 类似。作为一个例子,给定 FooBelongsToManyBar,以下将输出没有连接表字段的结果:

¥For belongsToMany relationships, by default getBars() will return all fields from the join table. Note that any include options will apply to the target Bar object, so trying to set options for the join table as you would when eager loading with find methods is not possible. To choose what attributes of the join table to include, getBars() supports a joinTableAttributes option that can be used similarly to setting through.attributes in an include. As an example, given Foo belongsToMany Bar, the following will both output results without join table fields:

const foo = Foo.findByPk(id, {
include: [
{
model: Bar,
through: { attributes: [] },
},
],
});
console.log(foo.bars);

const foo = Foo.findByPk(id);
console.log(foo.getBars({ joinTableAttributes: [] }));

注意:方法名称

¥Note: Method names

如上面的示例所示,Sequelize 赋予这些特殊方法的名称由前缀(例如 getaddset)与模型名称(第一个字母大写)连接而成。必要时使用复数,例如 fooInstance.setBars()。同样,Sequelize 也会自动处理不规则复数。例如,Person 变为 PeopleHypothesis 变为 Hypotheses

¥As shown in the examples above, the names Sequelize gives to these special methods are formed by a prefix (e.g. get, add, set) concatenated with the model name (with the first letter in uppercase). When necessary, the plural is used, such as in fooInstance.setBars(). Again, irregular plurals are also handled automatically by Sequelize. For example, Person becomes People and Hypothesis becomes Hypotheses.

如果定义了别名,则将使用别名代替模型名称来构成方法名称。例如:

¥If an alias was defined, it will be used instead of the model name to form the method names. For example:

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

  • taskInstance.setAuthor()

  • taskInstance.createAuthor()

为什么关联是成对定义的?

¥Why associations are defined in pairs?

正如前面提到的以及上面大多数示例所示,Sequelize 中的关联通常是成对定义的:

¥As mentioned earlier and shown in most examples above, usually associations in Sequelize are defined in pairs:

  • 要创建一对一关系,需要同时使用 hasOnebelongsTo 关联;

    ¥To create a One-To-One relationship, the hasOne and belongsTo associations are used together;

  • 要创建一对多关系,需要同时使用 hasManybelongsTo 关联;

    ¥To create a One-To-Many relationship, the hasMany and belongsTo associations are used together;

  • 要创建多对多关系,需要一起使用两个 belongsToMany 调用。

    ¥To create a Many-To-Many relationship, two belongsToMany calls are used together.

当两个模型之间定义 Sequelize 关联时,只有源模型知道它。因此,例如,当使用 Foo.hasOne(Bar) 时(因此 Foo 是源模型,Bar 是目标模型),只有 Foo 知道此关联的存在。这就是为什么在这种情况下,如上所示,Foo 实例获得方法 getBar()setBar()createBar(),而另一方面 Bar 实例却什么也得不到。

¥When a Sequelize association is defined between two models, only the source model knows about it. So, for example, when using Foo.hasOne(Bar) (so Foo is the source model and Bar is the target model), only Foo knows about the existence of this association. This is why in this case, as shown above, Foo instances gain the methods getBar(), setBar() and createBar(), while on the other hand Bar instances get nothing.

同样,对于 Foo.hasOne(Bar),由于 Foo 知道这种关系,所以我们可以像 Foo.findOne({ include: Bar }) 一样执行预加载,但不能执行 Bar.findOne({ include: Foo })

¥Similarly, for Foo.hasOne(Bar), since Foo knows about the relationship, we can perform eager loading as in Foo.findOne({ include: Bar }), but we can't do Bar.findOne({ include: Foo }).

因此,为了充分利用 Sequelize 的功能,我们通常成对设置关系,以便两个模型都了解它。

¥Therefore, to bring full power to Sequelize usage, we usually setup the relationship in pairs, so that both models get to know about it.

实际演示:

¥Practical demonstration:

  • 如果我们不定义关联对,则仅调用 Foo.hasOne(Bar)

    ¥If we do not define the pair of associations, calling for example just 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)

    ¥If we define the pair as recommended, i.e., both Foo.hasOne(Bar) and Bar.belongsTo(Foo):

    // This works!
    await Foo.findOne({ include: Bar });

    // This also works!
    await Bar.findOne({ include: Foo });

涉及相同模型的多个关联

¥Multiple associations involving the same models

在 Sequelize 中,可以在相同模型之间定义多个关联。你只需为它们定义不同的别名:

¥In Sequelize, it is possible to define multiple associations between the same models. You just have to define different aliases for them:

Team.hasOne(Game, { as: 'HomeTeam', foreignKey: 'homeTeamId' });
Team.hasOne(Game, { as: 'AwayTeam', foreignKey: 'awayTeamId' });
Game.belongsTo(Team);

创建引用非主键字段的关联

¥Creating associations referencing a field which is not the primary key

在上面的所有示例中,关联是通过引用所涉及模型的主键(在我们的例子中是它们的 ID)来定义的。但是,Sequelize 允许你定义使用另一个字段而不是主键字段来建立关联的关联。

¥In all the examples above, the associations were defined by referencing the primary keys of the involved models (in our case, their IDs). However, Sequelize allows you to define an association that uses another field, instead of the primary key field, to establish the association.

这个其他字段必须有一个唯一的约束(否则,它就没有意义)。

¥This other field must have a unique constraint on it (otherwise, it wouldn't make sense).

对于 belongsTo 关系

¥For belongsTo relationships

首先,回想一下 A.belongsTo(B) 关联将外键放置在源模型中(即,在 A 中)。

¥First, recall that the A.belongsTo(B) association places the foreign key in the source model (i.e., in A).

让我们再次使用船舶和船长的例子。此外,我们假设队长的名字是唯一的:

¥Let's again use the example of Ships and Captains. Additionally, we will assume that Captain names are unique:

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 保留在我们的船上。换句话说,我们的关系将引用目标模型上的另一列,而不是引用目标模型(Captain)中的 idname 列。为了指定这一点,我们必须定义一个目标键。我们还必须为外键本身指定一个名称:

¥This way, instead of keeping the captainId on our Ships, we could keep a captainName instead and use it as our association tracker. In other words, instead of referencing the id from the target model (Captain), our relationship will reference another column on the target model: the name column. To specify this, we have to define a target key. We will also have to specify a name for the foreign key itself:

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).

现在我们可以做这样的事情:

¥Now we can do things like:

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"

对于 hasOnehasMany 关系

¥For hasOne and hasMany relationships

完全相同的想法可以应用于 hasOnehasMany 关联,但我们在定义关联时不提供 targetKey,而是提供 sourceKey。这是因为与 belongsTo 不同,hasOnehasMany 关联将外键保留在目标模型上:

¥The exact same idea can be applied to the hasOne and hasMany associations, but instead of providing a targetKey, we provide a sourceKey when defining the association. This is because unlike belongsTo, the hasOne and hasMany associations keep the foreign key on the target model:

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 关系

¥For belongsToMany relationships

同样的想法也可以应用于 belongsToMany 关系。但是,与仅涉及一个外键的其他情况不同,belongsToMany 关系涉及两个外键,它们保存在额外的表(联结表)上。

¥The same idea can also be applied to belongsToMany relationships. However, unlike the other situations, in which we have only one foreign key involved, the belongsToMany relationship involves two foreign keys which are kept on an extra table (the junction table).

考虑以下设置:

¥Consider the following setup:

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 },
);

有四种情况需要考虑:

¥There are four cases to consider:

  • 我们可能希望使用 FooBar 的默认主键建立多对多关系:

    ¥We might want a many-to-many relationship using the default primary keys for both Foo and Bar:

Foo.belongsToMany(Bar, { through: 'foo_bar' });
// This creates a junction table `foo_bar` with fields `fooId` and `barId`
  • 我们可能需要使用 Foo 的默认主键但 Bar 使用不同的字段来建立多对多关系:

    ¥We might want a many-to-many relationship using the default primary key for Foo but a different field for Bar:

Foo.belongsToMany(Bar, { through: 'foo_bar', targetKey: 'title' });
// This creates a junction table `foo_bar` with fields `fooId` and `barTitle`
  • 我们可能想要使用 Foo 的不同字段和 Bar 的默认主键建立多对多关系:

    ¥We might want a many-to-many relationship using the a different field for Foo and the default primary key for Bar:

Foo.belongsToMany(Bar, { through: 'foo_bar', sourceKey: 'name' });
// This creates a junction table `foo_bar` with fields `fooName` and `barId`
  • 我们可能希望对 FooBar 使用不同的字段来建立多对多关系:

    ¥We might want a many-to-many relationship using different fields for both Foo and Bar:

Foo.belongsToMany(Bar, {
through: 'foo_bar',
sourceKey: 'name',
targetKey: 'title',
});
// This creates a junction table `foo_bar` with fields `fooName` and `barTitle`

注意

¥Notes

不要忘记关联中引用的字段必须具有唯一的约束。否则,将会抛出错误(有时会带有神秘的错误消息 - 例如 SQLite 的 SequelizeDatabaseError: SQLITE_ERROR: foreign key mismatch - "ships" referencing "captains")。

¥Don't forget that the field referenced in the association must have a unique constraint placed on it. Otherwise, an error will be thrown (and sometimes with a mysterious error message - such as SequelizeDatabaseError: SQLITE_ERROR: foreign key mismatch - "ships" referencing "captains" for SQLite).

sourceKeytargetKey 之间做出决定的技巧就是记住每个关系将其外键放置在哪里。正如本指南开头所述:

¥The trick to deciding between sourceKey and targetKey is just to remember where each relationship places its foreign key. As mentioned in the beginning of this guide:

  • A.belongsTo(B) 将外键保留在源模型 (A) 中,因此引用的键位于目标模型中,因此使用 targetKey

    ¥A.belongsTo(B) keeps the foreign key in the source model (A), therefore the referenced key is in the target model, hence the usage of targetKey.

  • A.hasOne(B)A.hasMany(B) 将外键保留在目标模型 (B) 中,因此引用的键位于源模型中,因此使用 sourceKey

    ¥A.hasOne(B) and A.hasMany(B) keep the foreign key in the target model (B), therefore the referenced key is in the source model, hence the usage of sourceKey.

  • A.belongsToMany(B) 涉及一个额外的表(联结表),因此 sourceKeytargetKey 都可用,其中 sourceKey 对应 A(源)中的某个字段,targetKey 对应 B(目标)中的某个字段。

    ¥A.belongsToMany(B) involves an extra table (the junction table), therefore both sourceKey and targetKey are usable, with sourceKey corresponding to some field in A (the source) and targetKey corresponding to some field in B (the target).