高级 M:N 关联
阅读本指南之前,请确保已阅读 关联指南.
让我们从 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.