关于 sequelize-patterns
sequelize-patterns 是面向 Sequelize 开发者的技能。它为这款 Node.js SQL 数据库 ORM 提供源自文档的模型、查询、迁移及常见数据库任务指南。
主要功能
- 01模型与关联指南
- 02迁移和查询模式
- 03事务、验证与钩子
- 04连接池配置
- 05命名与 Query Interface 示例
适用场景
- 01设计 Sequelize 模型和关联
- 02编写 SQL 数据库迁移
- 03排查 Sequelize 查询与配置问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ joneqian/ claude-skills-suite --skill sequelize-patterns安装后在 Claude Code 中输入/sequelize-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced.mdassociations.mddata_types.mddatabase.mddeployment.mdgetting_started.mdindex.mdmigrations.mdmodels.mdother.mdquerying.mdtypescript.md详细介绍
Sequelize 技能
用于 SQL 数据库的 Sequelize Node.js ORM。适用于数据库模型、迁移、关联、查询、事务、验证、钩子,以及与 PostgreSQL、MySQL、MariaDB、SQLite、SQL Server 配合使用。根据官方文档生成。
何时使用此技能
在以下情况下应触发此技能:
- 使用 Sequelize
- 询问 Sequelize 的功能或 API
- 实现 Sequelize 解决方案
- 调试 Sequelize 代码
- 学习 Sequelize 最佳实践
快速参考
常见模式
模式 1: 连接池如果你在单个进程中连接数据库,则只应创建一个 Sequelize 实例。Sequelize 会在初始化时设置连接池。此连接池可通过构造函数的 options 参数(使用 options.pool)进行配置,如以下示例所示:const sequelize = new Sequelize(/_ ... _/, { // ... pool: { max: 5, min: 0, acquire: 30000, idle: 10000 }}); 请参阅 Sequelize 构造函数的 API 参考了解更多信息。如果你从多个进程连接数据库,则必须为每个进程创建一个实例,但应设置每个实例的最大连接池大小,以确保总最大连接数不超出限制。例如,如果你希望最大连接池大小为 90,且有三个进程,则每个进程的 Sequelize 实例都应将最大连接池大小设为 30。
options
**模式 2:*命名策略带下划线的选项 Sequelize 为模型提供了 underscored 选项。设为 true 时,此选项会将所有属性的 field 选项设为其名称对应的 snake_case 形式。此设置也会自动应用于关联自动生成的外键和其他自动生成的字段。示例: const User = sequelize.define( 'user', { username: Sequelize.STRING }, { underscored: true, },);const Task = sequelize.define( 'task', { title: Sequelize.STRING }, { underscored: true, },);User.hasMany(Task);Task.belongsTo(User); 上例中的模型 User 和 Task 都使用了 underscored 选项。它们之间还存在一对多关系。另外请记住,timestamps 默认值为 true,因此 createdAt 和 updatedAt 字段也会自动创建。 如果不使用 underscored 选项,Sequelize 会自动定义: 每个模型都有一个 createdAt 属性,对应每张表中名为 createdAt 的列 每个模型都有一个 updatedAt 属性,对应每张表中名为 updatedAt 的列 Task 模型中有一个 userId 属性,对应 task 表中名为 userId 的列 启用 underscored 选项后,Sequelize 则会定义: 每个模型都有一个 createdAt 属性,对应每张表中名为 created_at 的列 每个模型都有一个 updatedAt 属性,对应每张表中名为 updated_at 的列 Task 模型中有一个 userId 属性,对应 task 表中名为 user_id 的列 请注意,无论哪种情况,字段在 JavaScript 侧仍采用 camelCase;此选项只会更改这些字段映射到数据库的方式。每个属性的 field 选项都会设为其对应的 snake_case 形式,但属性本身仍保留 camelCase。这样,对上述代码调用 sync() 会生成以下内容: CREATE TABLE IF NOT EXISTS "users" ( "id" SERIAL, "username" VARCHAR(255), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id"));CREATE TABLE IF NOT EXISTS "tasks" ( "id" SERIAL, "title" VARCHAR(255), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, "user_id" INTEGER REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE, PRIMARY KEY ("id")); 单数与复数 初看之下,在 Sequelize 中使用名称的单数形式还是复数形式可能会令人困惑。本节将对此作些说明。请记住,Sequelize 底层使用名为 inflection 的库,因此可以正确计算不规则复数(例如 person -> people)。不过,如果你使用其他语言,可能希望直接定义名称的单数和复数形式;sequelize 提供了一些选项来实现这一点。 定义模型时 模型应使用单词的单数形式定义。示例: sequelize.define('foo', { name: DataTypes.STRING }); 上例中,模型名为 foo(单数),对应的表名为 foos,因为 Sequelize 会自动将表名转换为复数形式。 在模型中定义引用键时 sequelize.define('foo', { name: DataTypes.STRING, barId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'bars', key: 'id', }, onDelete: 'CASCADE', },}); 在上例中,我们手动定义了一个引用另一个模型的键。通常不会这样做,但如果确实需要,就应在此处使用表名。这是因为引用是根据被引用的表名创建的。上例使用了复数形式(bars),假设 bar 模型是按默认设置创建的(其底层表名会自动变为复数)。 通过预加载获取数据时 在查询中执行 include 时,被包含的数据会根据以下规则添加到返回对象中的一个额外字段: 当包含来自单一关联(hasOne 或 belongsTo)的数据时,字段名采用模型名的单数形式; 当包含来自多项关联(hasMany 或 belongsToMany)的数据时,字段名采用模型名的复数形式。 简而言之,字段名在每种情况下都会采用最合乎逻辑的形式。示例: // 假设 Foo.hasMany(Bar)const foo = Foo.findOne({ include: Bar });// foo.bars 将是一个数组// foo.bar 不会存在,因为这样没有意义// 假设 Foo.hasOne(Bar)const foo = Foo.findOne({ include: Bar });// foo.bar 将是一个对象(如果没有关联模型,则可能为 null)// foo.bars 不会存在,因为这样没有意义// 依此类推。 定义别名时覆盖单复数形式 为关联定义别名时,可以传入一个对象来指定单数和复数形式,而不只是使用 { as: 'myAlias' }: Project.belongsToMany(User, { as: { singular: 'líder', plural: 'líderes', },}); 如果你知道某个模型在关联中始终使用相同的别名,可以直接为模型提供单数和复数形式: const User = sequelize.define( 'user', { / ... _/ }, { name: { singular: 'líder', plural: 'líderes', }, },);Project.belongsToMany(User); 添加到 user 实例的 mixin 会采用正确的形式。例如,Sequelize 会提供 project.getLíder(),而不是 project.addUser()。此外,Sequelize 会提供 project.setLíderes(),而不是 project.setUsers()。 注意:请记住,使用 as 更改关联名称也会更改外键名称。因此,在这种情况下,建议直接指定涉及的外键。 // 可能出错的示例Invoice.belongsTo(Subscription, { as: 'TheSubscription' });Subscription.hasMany(Invoice); 上面的第一次调用会在 Invoice 上建立名为 theSubscriptionId 的外键。不过,第二次调用也会在 Invoice 上建立一个外键(因为我们知道,hasMany 调用会将外键放在目标模型中),但其名称会是 subscriptionId。这样一来,就会同时存在 subscriptionId 和 theSubscriptionId 两列。最佳做法是为外键选择一个名称,并在两次调用中显式指定。例如,若选择 subscription_id: // 修正后的示例Invoice.belongsTo(Subscription, { as: 'TheSubscription', foreignKey: 'subscription_id',});Subscription.hasMany(Invoice, { foreignKey: 'subscription_id' });
underscored
模式 3: 模型名称应使用单数形式。例如:
sequelize.define('foo', { name: DataTypes.STRING });
模式 4: 默认情况下,模型的每一列都允许使用 null 值。可以为某一列设置 allowNull: false 选项来禁用此行为,就像我们的代码示例中 username 字段所做的那样:
null
模式 5: 查询接口
Sequelize 实例使用名为 Query Interface 的接口,以与方言无关的方式与数据库通信。本手册中介绍的大多数方法都是借助查询接口中的若干方法实现的。因此,查询接口中的方法属于较低级别的方法;只有在找不到其他方式通过 Sequelize 的高级 API 实现目标时,才应使用这些方法。当然,它们仍然比直接执行原始查询(即手写 SQL)更高级。本指南展示了一些示例;有关它的完整功能列表以及每种方法的详细用法,请参阅 QueryInterface API。
获取查询接口
从现在起,我们将把 Sequelize 实例上可用的 QueryInterface 类的单例实例称为 queryInterface:
const { Sequelize, DataTypes } = require('sequelize');const sequelize = new Sequelize(/_ ... _/);const queryInterface = sequelize.getQueryInterface();
创建表
queryInterface.createTable('Person', { name: DataTypes.STRING, isBetaMember: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false, },});
生成的 SQL(使用 SQLite):
CREATE TABLE IF NOT EXISTS Person ( name VARCHAR(255), isBetaMember TINYINT(1) NOT NULL DEFAULT 0);
注意:可以考虑改为定义一个 Model 并调用 YourModel.sync(),这是更高级别的做法。
向表中添加列
queryInterface.addColumn('Person', 'petName', { type: DataTypes.STRING });
生成的 SQL(使用 SQLite):
ALTER TABLE Person ADD petName VARCHAR(255);
更改列的数据类型
queryInterface.changeColumn('Person', 'foo', { type: DataTypes.FLOAT, defaultValue: 3.14, allowNull: false,});
生成的 SQL(使用 MySQL):
ALTER TABLE Person CHANGE foo foo FLOAT NOT NULL DEFAULT 3.14;
删除列
queryInterface.removeColumn('Person', 'petName', { /_ query options _/});
生成的 SQL(使用 PostgreSQL):
ALTER TABLE "public"."Person" DROP COLUMN "petName";
在 SQLite 中更改和删除列
SQLite 不支持直接修改或删除列。不过,Sequelize 会尝试通过借助备份表重新创建整个表来解决这一问题,具体做法参考了这些说明。例如:
// Assuming we have a table in SQLite created as follows:queryInterface.createTable('Person', { name: DataTypes.STRING, isBetaMember: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false, }, petName: DataTypes.STRING, foo: DataTypes.INTEGER,});// And we change a column:queryInterface.changeColumn('Person', 'foo', { type: DataTypes.FLOAT, defaultValue: 3.14, allowNull: false,});
以下是为 SQLite 生成的 SQL 调用:
PRAGMA TABLE_INFO(Person);CREATE TABLE IF NOT EXISTS Person_backup ( name VARCHAR(255), isBetaMember TINYINT(1) NOT NULL DEFAULT 0, foo FLOAT NOT NULL DEFAULT '3.14', petName VARCHAR(255));INSERT INTO Person_backup SELECT name, isBetaMember, foo, petName FROM Person;DROP TABLE Person;CREATE TABLE IF NOT EXISTS Person ( name VARCHAR(255), isBetaMember TINYINT(1) NOT NULL DEFAULT 0, foo FLOAT NOT NULL DEFAULT '3.14', petName VARCHAR(255));INSERT INTO Person SELECT name, isBetaMember, foo, petName FROM Person_backup;DROP TABLE Person_backup;
其他
如本指南开头所述,Sequelize 中的 Query Interface 还有许多其他功能!有关可执行操作的完整列表,请参阅 QueryInterface API。
queryInterface
**模式 6:**高级 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 的模型作为 through 表(也称为联结表),其中只有两列:userId 和 profileId。Sequelize 会在这两列上建立一个复合唯一键。
我们也可以自行定义一个模型作为 through 表。
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 在其中一个关联模型上自动添加列一样。
不过,自行定义模型还有几个好处。例如,我们可以在 through 表中定义更多列:
const User_Profile = sequelize.define( 'User_Profile', { selfGranted: DataTypes.BOOLEAN, }, { timestamps: false }, ); User.belongsToMany(Profile, { through: User_Profile }); Profile.belongsToMany(User, { through: User_Profile });
这样,我们现在就可以在 through 表中跟踪额外信息,即 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', });
如果需要,也可以强制 through 表像其他标准表一样具有主键。只需在模型中定义主键即可:
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 列作为主键,而不会在这两列上设置复合唯一键。其他功能仍会正常工作。
through 表与普通表,以及“超级多对多关联”
现在我们将比较上面展示的最后一种多对多设置与常见的一对多关系,以便最终引出“超级多对多关系”的概念。
模型回顾(稍作重命名)
为了更便于理解,我们将 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 模型作为 through 表,在 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 });
// 但不能这样做: 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 });
// 但不能这样做: 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 为外键选择的名称(该示例中在 Ship 模型上创建了 leaderId)。
为 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' }); // 这样也可以
不过,在这里定义别名与外键名称无关。through 表中创建的两个外键名称仍由 Sequelize 根据关联模型的名称构造。查看上面示例中 through 表的生成 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 定义 through 关系中源模型对应的键,otherKey 则定义目标模型对应的键):
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' }); // 这会创建存储对象 ID 的 PersonChildren 表。
指定 through 表中的属性
默认情况下,预加载多对多关系时,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 从多对多关系预加载时生成的默认结构。
不过,如果你只需要 through 表中的部分属性,可以在 attributes 选项中提供一个数组,列出所需的属性。例如,如果你只需要 through 表中的 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" } ] }
如果使用的是 mixin(例如 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.STRING });
现在的问题是:如何将它们关联起来?首先,我们注意到:
一场比赛关联多支参赛队伍; 一支队伍可能参加过多场比赛。
以上观察表明,我们需要在 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);
玩家之间的关系更复杂一些。我们注意到,组成一支队伍的玩家集合不仅取决于队伍本身(显而易见),还取决于具体是哪场比赛。因此,我们不需要在 Player 和 Team 之间建立多对多关系,也不需要在 Player 和 Game 之间建立多对多关系。我们不应将 Player 关联到上述任一模型,而是需要在 Player 与某种“队伍-比赛对约束”之间建立关联,因为正是这个组合(队伍加比赛)决定了哪些玩家属于其中。因此,我们需要的恰好是联结模型 GameTeam 本身!另外,由于给定的比赛-队伍组合会对应多个玩家,而同一玩家又可以参加多个比赛-队伍组合,因此我们需要在 Player 和 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.STRING });
// 在 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 为 The Martians 效力,而 // not_spock 和 bowl_of_petunias 为 The Plutonians 效力: await PlayerGameTeam.bulkCreate([ // 在 'Winter Showdown' 中(即 GameTeamIds 3 和 4): { PlayerId: 1, GameTeamId: 3 }, // s0me0ne 为 The Martians 效力 { PlayerId: 3, GameTeamId: 3 }, // greenhead 为 The Martians 效力 { PlayerId: 4, GameTeamId: 4 }, // not_spock 为 The Plutonians 效力 { PlayerId: 5, GameTeamId: 4 }, // bowl_of_petunias 为 The Plutonians 效力 ]);
// 现在可以执行查询了!
const game = await Game.findOne({
where: {
name: 'Winter Showdown',
},
include: {
model: GameTeam,
include: [
{
model: Player,
through: {
attributes: [], // 从结果中隐藏不需要的嵌套 PlayerGameTeam 对象
},
},
Team,
],
},
});
console.log(找到比赛:"${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.name}" 参加了比赛 "${game.name}",队员如下:);
console.log(players.map(p => --- ${p.username}).join('\n'));
}
})();
输出:
找到比赛:"Winter Showdown"
- 队伍 "The Martians" 参加了比赛 "Winter Showdown",队员如下: --- s0me0ne --- greenhead
- 队伍 "The Plutonians" 参加了比赛 "Winter Showdown",队员如下: --- not_spock --- bowl_of_petunias
这就是如何通过利用超级多对多关系技术,在 Sequelize 中实现三个模型之间的多对多对多关系!这个思路可以递归应用于更复杂的多对多……对多关系(不过,查询最终可能会变慢)。
User
模式 7: 示例:
await User.sync({ force: true });console.log('The table for the User model was just (re)created!');
模式 8: 你可以使用 sequelize.sync() 自动同步所有模型。示例:
sequelize.sync()
示例代码模式
示例 1(css):
/* ... */ { username: { type: DataTypes.TEXT, allowNull: false, unique: true },} /* ... */
示例 2(javascript):
const jane = User.build({ name: 'Jane' });
console.log(jane instanceof User); // trueconsole.log(jane.name); // "Jane"
示例 3(sql):
const [results, metadata] = await sequelize.query('UPDATE users SET y = 42 WHERE x = 12');// Results will be an empty array and metadata will contain the number of affected rows.
示例 4(sql):
SELECT `id`, `status`, `createdAt`, `updatedAt`, `fooId`FROM `bars` AS `bar`WHERE `bar`.`status` = 'open' AND `bar`.`fooId` = 1;
示例 5(javascript):
const project = await Project.findByPk(123);if (project === null) { console.log('Not found!');} else { console.log(project instanceof Project); // true // Its primary key is 123}
参考文件
此技能在 references/ 中包含全面的文档:
- advanced.md - 高级文档
- associations.md - 关联文档
- data_types.md - 数据类型文档
- database.md - 数据库文档
- deployment.md - 部署文档
- getting_started.md - 入门文档
- migrations.md - 迁移文档
- models.md - 模型文档
- other.md - 其他文档
- querying.md - 查询文档
- typescript.md - TypeScript 文档
需要详细信息时,使用 view 阅读特定的参考文件。
使用此技能
初学者
从 getting_started 或 tutorials 参考文件开始,了解基础概念。
特定功能
使用相应类别的参考文件(api、guides 等)获取详细信息。
代码示例
上方的快速参考部分包含从官方文档中提取的常见模式。
资源
references/
从官方来源提取并整理的文档。这些文件包含:
- 详细说明
- 带语言标注的代码示例
- 指向原始文档的链接
- 便于快速导航的目录
scripts/
在此处添加用于常见自动化任务的辅助脚本。
assets/
在此处添加模板、样板代码或示例项目。
备注
- 此技能由官方文档自动生成
- 参考文件保留了源文档的结构和示例
- 代码示例包含语言检测,以便更好地进行语法高亮
- 快速参考模式摘自文档中的常见用法示例
更新
如需使用更新后的文档刷新此技能:
- 使用相同配置重新运行爬取程序
- 技能将使用最新信息重新构建