sequelize 高级 m:n 关联——迹忆客-ag捕鱼王app官网
阅读本篇内容之前,请确保已阅读 关联。
让我们从 user 和 profile 之间的多对多关系示例开始。
const user = sequelize.define('user', {
username: datatypes.string,
points: datatypes.integer
}, { timestamps: false });
const profile = sequelize.define('profile', {
name: datatypes.string
}, { timestamps: false });
定义多对多关系的最简单方法是:
user.belongstomany(profile, { through: 'user_profiles' });
profile.belongstomany(user, { through: 'user_profiles' });
通过将字符串传递给上面的 through,我们要求 sequelize 自动生成名为 user_profiles 的模型作为 联结表,该模型只有两列: userid
和 profileid
。 在这两个列上将建立一个复合唯一键。
我们还可以为自己定义一个模型,以用作联结表。
const user_profile = sequelize.define('user_profile', {}, { timestamps: false });
user.belongstomany(profile, { through: user_profile });
profile.belongstomany(user, { through: user_profile });
以上具有完全相同的效果。 注意:我们没有在 user_profile 模型上定义任何属性。 我们将其传递给 belongstomany
调用的事实告诉 sequelize
自动创建两个属性 userid 和 profileid,就像其他关联一样,也会导致 sequelize 自动向其中一个涉及的模型添加列。
然而,自己定义模型有几个优点。 例如,我们可以在联结表中定义更多列:
const user_profile = sequelize.define('user_profile', {
selfgranted: datatypes.boolean
}, { timestamps: false });
user.belongstomany(profile, { through: user_profile });
profile.belongstomany(user, { through: user_profile });
这样,我们现在可以在联结表中跟踪额外的信息,即 selfgranted 布尔值。 例如,当调用 user.addprofile()
时,我们可以使用 through 参数传递额外列的值。
示例:
const amidala = await user.create({ username: 'p4dm3', points: 1000 });
const queen = await profile.create({ name: 'queen' });
await amidala.addprofile(queen, { through: { selfgranted: false } });
const result = await user.findone({
where: { username: 'p4dm3' },
include: profile
});
console.log(result);
输出:
{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen",
"user_profile": {
"userid": 4,
"profileid": 6,
"selfgranted": false
}
}
]
}
我们也可以在单个 create 调用中创建所有关系。
示例:
const amidala = await user.create({
username: 'p4dm3',
points: 1000,
profiles: [{
name: 'queen',
user_profile: {
selfgranted: true
}
}]
}, {
include: profile
});
const result = await user.findone({
where: { username: 'p4dm3' },
include: profile
});
console.log(result);
输出:
{
"id": 1,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 1,
"name": "queen",
"user_profile": {
"selfgranted": true,
"userid": 1,
"profileid": 1
}
}
]
}
你可能已经注意到 user_profiles 表中没有 id 字段。 如上所述,它具有复合唯一键。 该复合唯一密钥的名称由 sequelize 自动选择,但可以使用 uniquekey 参数进行自定义:
user.belongstomany(profile, { through: user_profiles, uniquekey: 'my_custom_unique' });
如果需要的话,另一种可能是强制联结表像其他标准表一样具有主键。 为此,只需在模型中定义主键:
const user_profile = sequelize.define('user_profile', {
id: {
type: datatypes.integer,
primarykey: true,
autoincrement: true,
allownull: false
},
selfgranted: datatypes.boolean
}, { timestamps: false });
user.belongstomany(profile, { through: user_profile });
profile.belongstomany(user, { through: user_profile });
上面的代码当然仍然会创建两列 userid 和 profileid,但是模型不会在其上设置复合唯一键,而是将其 id 列用作主键。 其他一切仍然可以正常工作。
联结表与普通表以及"超级多对多关联"
现在,我们将比较上面显示的最后一个"多对多"设置与通常的"一对多"关系的用法,以便最后得出 超级多对多关系 的概念作为结论。
模型回顾 (有少量重命名)
为了使事情更容易理解,让我们将 user_profile 模型重命名为 grant。 请注意,所有操作均与以前相同。 我们的模型是:
const user = sequelize.define('user', {
username: datatypes.string,
points: datatypes.integer
}, { timestamps: false });
const profile = sequelize.define('profile', {
name: datatypes.string
}, { timestamps: false });
const grant = sequelize.define('grant', {
id: {
type: datatypes.integer,
primarykey: true,
autoincrement: true,
allownull: false
},
selfgranted: datatypes.boolean
}, { timestamps: false });
我们使用 grant 模型作为联结表在 user 和 profile 之间建立了多对多关系:
user.belongstomany(profile, { through: grant });
profile.belongstomany(user, { through: grant });
这会自动将 userid 和 profileid 列添加到 grant 模型中。
注意
: 如上所示,我们选择强制 grant 模型具有单个主键(通常称为 id)。 对于 超级多对多关系(即将定义),这是必需的。
改用一对多关系
除了建立上面定义的多对多关系之外,如果我们执行以下操作怎么办?
// 在 user 和 grant 之间设置一对多关系
user.hasmany(grant);
grant.belongsto(user);
// 在profile 和 grant 之间也设置一对多关系
profile.hasmany(grant);
grant.belongsto(profile);
结果基本相同! 这是因为 user.hasmany(grant)
和 profile.hasmany(grant)
会分别自动将 userid 和 profileid 列添加到 grant 中。
这表明一个多对多关系与两个一对多关系没有太大区别。 数据库中的表看起来相同。
唯一的区别是你尝试使用 sequelize 执行预先加载时。
// 使用多对多方法,你可以:
user.findall({ include: profile });
profile.findall({ include: user });
// however, you can't do:
user.findall({ include: grant });
profile.findall({ include: grant });
grant.findall({ include: user });
grant.findall({ include: profile });
// 另一方面,通过双重一对多方法,你可以:
user.findall({ include: grant });
profile.findall({ include: grant });
grant.findall({ include: user });
grant.findall({ include: profile });
// however, you can't do:
user.findall({ include: profile });
profile.findall({ include: user });
// 尽管你可以使用嵌套 include 来模拟那些,如下所示:
user.findall({
include: {
model: grant,
include: profile
}
}); // 这模拟了 `user.findall({ include: profile })`,
// 但是生成的对象结构有些不同.
// 原始结构的格式为 `user.profiles[].grant`,
// 而模拟结构的格式为 `user.grants[].profiles[]`.
两全其美:超级多对多关系
我们可以简单地组合上面显示的两种方法!
// 超级多对多关系
user.belongstomany(profile, { through: grant });
profile.belongstomany(user, { through: grant });
user.hasmany(grant);
grant.belongsto(user);
profile.hasmany(grant);
grant.belongsto(profile);
这样,我们可以进行各种预先加载:
// 全部可以使用:
user.findall({ include: profile });
profile.findall({ include: user });
user.findall({ include: grant });
profile.findall({ include: grant });
grant.findall({ include: user });
grant.findall({ include: profile });
我们甚至可以执行各种深层嵌套的 include:
user.findall({
include: [
{
model: grant,
include: [user, profile]
},
{
model: profile,
include: {
model: user,
include: {
model: grant,
include: [user, profile]
}
}
}
]
});
别名和自定义键名
与其他关系类似,可以为多对多关系定义别名。
在继续之前,请回顾关联指南上的 belongsto 别名示例。 请注意,在这种情况下,定义关联影响 include 完成方式(即传递关联名称)和 sequelize 为外键选择的名称(在该示例中,leaderid 是在 ship 模型上创建的) 。
为一个 belongstomany 关联定义一个别名也会影响 include 执行的方式:
product.belongstomany(category, { as: 'groups', through: 'product_categories' });
category.belongstomany(product, { as: 'items', through: 'product_categories' });
// [...]
await product.findall({ include: category }); // 这无法使用
await product.findall({ // 通过别名这可以使用
include: {
model: category,
as: 'groups'
}
});
await product.findall({ include: 'groups' }); // 这也可以使用
但是,在此处定义别名与外键名称无关。 联结表中创建的两个外键的名称仍由 sequelize 基于关联的模型的名称构造。 通过检查上面示例中的穿透表生成的 sql,可以很容易看出这一点:
create table if not exists `product_categories` (
`createdat` datetime not null,
`updatedat` datetime not null,
`productid` integer not null references `products` (`id`) on delete cascade on update cascade,
`categoryid` integer not null references `categories` (`id`) on delete cascade on update cascade,
primary key (`productid`, `categoryid`)
);
我们可以看到外键是 productid 和 categoryid。 要更改这些名称,sequelize 分别接受参数 foreignkey 和 otherkey(即,foreignkey 定义联结关系中源模型的 key,而 otherkey 定义目标模型中的 key):
product.belongstomany(category, {
through: 'product_categories',
foreignkey: 'objectid', // 替换 `productid`
otherkey: 'typeid' // 替换 `categoryid`
});
category.belongstomany(product, {
through: 'product_categories',
foreignkey: 'typeid', // 替换 `categoryid`
otherkey: 'objectid' // 替换 `productid`
});
生成 sql:
create table if not exists `product_categories` (
`createdat` datetime not null,
`updatedat` datetime not null,
`objectid` integer not null references `products` (`id`) on delete cascade on update cascade,
`typeid` integer not null references `categories` (`id`) on delete cascade on update cascade,
primary key (`objectid`, `typeid`)
);
如上所示,当使用两个 belongstomany 调用定义多对多关系时(这是标准方式),应在两个调用中适当地提供 foreignkey 和 otherkey 参数。 如果仅在一个调用中传递这些参数,那么 sequelize 行为将不可靠。
自参照
sequelize 直观地支持自参照多对多关系:
person.belongstomany(person, { as: 'children', through: 'personchildren' })
// 这将创建表 personchildren,该表存储对象的 id.
从联结表中指定属性
默认情况下,当预先加载多对多关系时,sequelize 将以以下结构返回数据(基于本指南中的第一个示例):
// user.findone({ include: profile })
{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen",
"grant": {
"userid": 4,
"profileid": 6,
"selfgranted": false
}
}
]
}
注意,外部对象是一个 user,它具有一个名为 profiles 的字段,该字段是 profile 数组,因此每个 profile 都带有一个名为 grant 的额外字段,这是一个 grant 实例。当从多对多关系预先加载时,这是 sequelize 创建的默认结构。
但是,如果只需要联结表的某些属性,则可以在 attributes 参数中为数组提供所需的属性。 例如,如果只需要穿透表中的 selfgranted 属性:
user.findone({
include: {
model: profile,
through: {
attributes: ['selfgranted']
}
}
});
输出:
{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen",
"grant": {
"selfgranted": false
}
}
]
}
如果你根本不想使用嵌套的 grant 字段,请使用 attributes: []:
user.findone({
include: {
model: profile,
through: {
attributes: []
}
}
});
输出:
{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen"
}
]
}
如果你使用 mixins(例如 user.getprofiles()
)而不是查找器方法(例如 user.findall()
),则必须使用 jointableattributes
参数:
someuser.getprofiles({ jointableattributes: ['selfgranted'] });
输出:
[
{
"id": 6,
"name": "queen",
"grant": {
"selfgranted": false
}
}
]
多对多对多关系及更多
思考你正在尝试为游戏锦标赛建模。 有玩家和团队。 团队玩游戏。 然而,玩家可以在锦标赛中(但不能在比赛中间)更换团队。 因此,给定一个特定的游戏,有某些团队参与该游戏,并且每个团队都有一组玩家(针对该游戏)。
因此,我们首先定义三个相关模型:
const player = sequelize.define('player', { username: datatypes.string });
const team = sequelize.define('team', { name: datatypes.string });
const game = sequelize.define('game', { name: datatypes.integer });
现在的问题是:如何关联它们?
首先,我们注意到:
- 一个游戏有许多与之相关的团队(正在玩该游戏的团队);
- 一个团队可能参加了许多比赛.
以上观察表明,我们需要在 game 和 team 之间建立多对多关系。 让我们使用本指南前面解释的超级多对多关系:
// game 与 team 之间的超级多对多关系
const gameteam = sequelize.define('gameteam', {
id: {
type: datatypes.integer,
primarykey: true,
autoincrement: true,
allownull: false
}
});
team.belongstomany(game, { through: gameteam });
game.belongstomany(team, { through: gameteam });
gameteam.belongsto(game);
gameteam.belongsto(team);
game.hasmany(gameteam);
team.hasmany(gameteam);
关于玩家的部分比较棘手。 我们注意到,组成一个团队的一组球员不仅取决于团队,还取决于正在考虑哪个游戏。 因此,我们不希望玩家与团队之间存在多对多关系。 我们也不希望玩家与游戏之间存在多对多关系。 除了将玩家与任何这些模型相关联之外,我们需要的是玩家与 团队-游戏约束 之类的关联,因为这是一对(团队加游戏)来定义哪些玩家属于那里。因此,我们正在寻找的正是联结模型gameteam本身!并且,我们注意到,由于给定的 游戏-团队 指定了许多玩家,而同一位玩家可以参与许多 游戏-团队,因此我们需要玩家之间的多对多关系和gameteam!
为了提供最大的灵活性,让我们在这里再次使用"超级多对多"关系构造:
// player 与 gameteam 之间的超级多对多关系
const playergameteam = sequelize.define('playergameteam', {
id: {
type: datatypes.integer,
primarykey: true,
autoincrement: true,
allownull: false
}
});
player.belongstomany(gameteam, { through: playergameteam });
gameteam.belongstomany(player, { through: playergameteam });
playergameteam.belongsto(player);
playergameteam.belongsto(gameteam);
player.hasmany(playergameteam);
gameteam.hasmany(playergameteam);
上面的关联正是我们想要的。 这是一个完整的可运行示例:
const { sequelize, op, model, datatypes } = require('sequelize');
const sequelize = new sequelize('sqlite::memory:', {
define: { timestamps: false } // 在这个例子中只是为了减少混乱
});
const player = sequelize.define('player', { username: datatypes.string });
const team = sequelize.define('team', { name: datatypes.string });
const game = sequelize.define('game', { name: datatypes.integer });
// 我们在 game 和 team 游戏和团队之间应用超级多对多关系
const gameteam = sequelize.define('gameteam', {
id: {
type: datatypes.integer,
primarykey: true,
autoincrement: true,
allownull: false
}
});
team.belongstomany(game, { through: gameteam });
game.belongstomany(team, { through: gameteam });
gameteam.belongsto(game);
gameteam.belongsto(team);
game.hasmany(gameteam);
team.hasmany(gameteam);
// 我们在 player 和 gameteam 游戏和团队之间应用超级多对多关系
const playergameteam = sequelize.define('playergameteam', {
id: {
type: datatypes.integer,
primarykey: true,
autoincrement: true,
allownull: false
}
});
player.belongstomany(gameteam, { through: playergameteam });
gameteam.belongstomany(player, { through: playergameteam });
playergameteam.belongsto(player);
playergameteam.belongsto(gameteam);
player.hasmany(playergameteam);
gameteam.hasmany(playergameteam);
(async () => {
await sequelize.sync();
await player.bulkcreate([
{ username: 's0me0ne' },
{ username: 'empty' },
{ username: 'greenhead' },
{ username: 'not_spock' },
{ username: 'bowl_of_petunias' }
]);
await game.bulkcreate([
{ name: 'the big clash' },
{ name: 'winter showdown' },
{ name: 'summer beatdown' }
]);
await team.bulkcreate([
{ name: 'the martians' },
{ name: 'the earthlings' },
{ name: 'the plutonians' }
]);
// 让我们开始定义哪些球队参加了哪些比赛.
// 这可以通过几种方式来完成,例如在每个游戏上调用`.setteams`.
// 但是,为简便起见,我们将直接使用 `create` 调用,
// 直接引用我们想要的 id. 我们知道 id 是从 1 开始的.
await gameteam.bulkcreate([
{ gameid: 1, teamid: 1 }, // 该 gameteam 将获得 id 1
{ gameid: 1, teamid: 2 }, // 该 gameteam 将获得 id 2
{ gameid: 2, teamid: 1 }, // 该 gameteam 将获得 id 3
{ gameid: 2, teamid: 3 }, // 该 gameteam 将获得 id 4
{ gameid: 3, teamid: 2 }, // 该 gameteam 将获得 id 5
{ gameid: 3, teamid: 3 } // 该 gameteam 将获得 id 6
]);
// 现在让我们指定玩家.
// 为简便起见,我们仅在第二场比赛(winter showdown)中这样做.
// 比方说,s0me0ne 和 greenhead 效力于 martians,
// 而 not_spock 和 bowl_of_petunias 效力于 plutonians:
await playergameteam.bulkcreate([
// 在 'winter showdown' (即 gameteamids 3 和 4)中:
{ playerid: 1, gameteamid: 3 }, // s0me0ne played for the martians
{ playerid: 3, gameteamid: 3 }, // greenhead played for the martians
{ playerid: 4, gameteamid: 4 }, // not_spock played for the plutonians
{ playerid: 5, gameteamid: 4 } // bowl_of_petunias played for the plutonians
]);
// 现在我们可以进行查询!
const game = await game.findone({
where: {
name: "winter showdown"
},
include: {
model: gameteam,
include: [
{
model: player,
through: { attributes: [] } // 隐藏结果中不需要的 `playergameteam` 嵌套对象
},
team
]
}
});
console.log(`found game: "${game.name}"`);
for (let i = 0; i < game.gameteams.length; i ) {
const team = game.gameteams[i].team;
const players = game.gameteams[i].players;
console.log(`- team "${team.name}" played game "${game.name}" with the following players:`);
console.log(players.map(p => `--- ${p.username}`).join('\n'));
}
})();
输出:
found game: "winter showdown"
- team "the martians" played game "winter showdown" with the following players:
--- s0me0ne
--- greenhead
- team "the plutonians" played game "winter showdown" with the following players:
--- not_spock
--- bowl_of_petunias
因此,这就是我们利用超级多对多关系技术在 sequelize 中实现三个模型之间的 多对多对多 关系的方式!
这个想法可以递归地应用于甚至更复杂的,多对多对......对多 关系(尽管有时查询可能会变慢)。