drizzle-orm-sqlite 0.15.0 复合主键支持:在表结构定义中声明多列联合主键
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
drizzle-orm-sqlite0.15.0 正式在 SQLite 表结构(table schema)定义中引入**复合主键(composite primary key)**支持。在此之前,主键只能通过单列上的.primaryKey()声明,而复合主键(多列联合作为主键)往往需要退回到原始 SQL 或绕过 ORM 手动处理。本篇将基于该版本的发布说明(changelogs/drizzle-orm-sqlite/0.15.0.md)与仓库源码,完整讲解复合主键的声明语法、primaryKey()构建器的实现原理、命名规则及实际工程中的使用注意事项,帮助你直接在本仓库的drizzle-orm代码中落地这一能力。
版本变更核心:表级复合主键
drizzle-orm-sqlite0.15.0 的发布说明核心只有一条能力变更:
Add composite PK's on table schema definition
即在表结构定义阶段,通过第三个参数(extraConfig)内的primaryKey(...)构建器,将多个列组合声明为一个主键,并由 Drizzle 在生成 SQL 时自动产出对应的表级约束。这与 SQLite 原生 DDL 中的PRIMARY KEY (col_a, col_b)表约束一一对应。
从发布说明继承的最小示例
发布说明给出了如下最小示例,它也是理解该特性最直接的入口:
import { sqliteTable, integer, text, primaryKey } from 'drizzle-orm/sqlite-core'; const pkExample = sqliteTable('pk_example', { id: integer('id'), name: text('name').notNull(), email: text('email').notNull(), }, (table) => ({ compositePk: primaryKey(table.id, table.name) }));要点拆解:
sqliteTable的第三个参数是一个回调函数,接收table(即已构建好的列集合,类型为BuildColumns<TTableName, TColumnsMap, 'sqlite'>),返回一组"表级额外配置";- 在返回的对象中,
compositePk只是给这个约束起的语义化字段名,真正起作用的是右侧的primaryKey(table.id, table.name); - 参与联合主键的列在列定义中不要再单独调用
.primaryKey(),否则会同时产生单列主键,与复合主键冲突。
源码级解析:primaryKey()构建器与PrimaryKey实体
primaryKey函数定义在 drizzle-orm/src/sqlite-core/primary-keys.ts。当前版本提供两种重载形态:
// 推荐形态:对象配置,支持自定义约束名 export function primaryKey< TTableName extends string, TColumn extends AnySQLiteColumn<{ tableName: TTableName }>, TColumns extends AnySQLiteColumn<{ tableName: TTableName }>[], >(config: { name?: string; columns: [TColumn, ...TColumns] }): PrimaryKeyBuilder; // 已废弃形态:直接把列作为可变参数传入 /** * @deprecated: Please use primaryKey({ columns: [] }) instead of this function */ export function primaryKey< TTableName extends string, TColumns extends AnySQLiteColumn<{ tableName: TTableName }>[], >(...columns: TColumns): PrimaryKeyBuilder;从源码可以看到,运行时逻辑会先判断首个参数是否含有columns属性:
- 传入
{ columns: [col1, col2], name: 'xxx' }对象 → 走new PrimaryKeyBuilder(config[0].columns, config[0].name); - 直接传列(如发布说明示例
primaryKey(table.id, table.name))→ 走new PrimaryKeyBuilder(config),此时name为undefined。
PrimaryKeyBuilder内部仅持有columns: SQLiteColumn[]与可选的name?: string,其build(table)方法(primary-keys.ts)将其转换为最终的PrimaryKey实体。PrimaryKey类中值得关注的是默认命名规则getName():
getName(): string { return this.name ?? `${this.table[SQLiteTable.Symbol.Name]}_${this.columns.map((column) => column.name).join('_')}_pk`; }也就是说,若不显式指定name,生成的约束名默认为${表名}_${列名1}_${列名2}_pk的形式。例如上文的pk_example表、id与name两列,默认主键约束名即为pk_example_id_name_pk。若希望约束名可控(例如用于迁移脚本对比、数据库约束管理),推荐使用对象形态:
const pkExample = sqliteTable('pk_example', { id: integer('id'), name: text('name').notNull(), email: text('email').notNull(), }, (table) => ({ compositePk: primaryKey({ columns: [table.id, table.name], name: 'pk_example_id_name' }), }));该推荐写法在仓库的集成测试中大量使用,例如 integration-tests/tests/sqlite/d1-batch.test.ts 与 integration-tests/tests/sqlite/durable-objects/index.ts 中均采用primaryKey({ columns: [t.userId, t.groupId] })的对象形态。
extraConfig的两种返回形态:对象与数组
sqliteTable的第三个参数(extraConfig)在 drizzle-orm/src/sqlite-core/table.ts 中被类型化为返回SQLiteTableExtraConfigValue[]或(已废弃的)SQLiteTableExtraConfig对象:
- 数组形态(推荐):直接返回一组构建器,例如
[primaryKey(...), index(...), foreignKey(...)]; - 对象形态(deprecated):为每个构建器命名,如上文示例中的
{ compositePk: primaryKey(...) }。
对象形态在SQLiteTableFn的重载声明中标注了@deprecated,未来的 API 方向是统一改为数组。两种形态最终都会进入getTableConfig(drizzle-orm/src/sqlite-core/utils.ts)统一处理:
const extraValues = Array.isArray(extraConfig) ? extraConfig.flat(1) as any[] : Object.values(extraConfig); for (const builder of Object.values(extraValues)) { // ... if (is(builder, PrimaryKeyBuilder)) { primaryKeys.push(builder.build(table)); } }可见,无论采用哪种书写形态,PrimaryKeyBuilder都会在表配置收集阶段被识别并构建为PrimaryKey实体,随后参与后续 SQL 生成与类型推导。getTableConfig返回结构中的primaryKeys字段正是表级主键约束的最终载体。
与单列主键的对比及联合主键的业务场景
在 0.15.0 之前,SQLite 表声明主键的唯一方式是列内修饰符.primaryKey():
const users = sqliteTable('users', { id: integer('id').primaryKey(), // 单列主键 name: text('name'), });primaryKey()在列构建器上的实现(参见 drizzle-orm/src/sqlite-core/columns/integer.ts)会自动将列标记为NOT NULL且具备自增默认语义(IsPrimaryKey<HasDefault<NotNull<this>>>)。而复合主键属于表级约束,参与联合的列需要在定义时显式声明约束条件(如示例中的.notNull()),因为 SQLite 规范要求主键列不可为空。
复合主键典型适用于以下业务建模场景:
- 关联表:如多对多中间表,以
(userId, groupId)或(orderId, productId)联合唯一标识一条关系记录; - 分区 / 分片键:以业务键 + 时间戳等组合定位数据;
- 无自增 ID 的业务实体:自然键本身由多个字段构成,如
(countryCode, postalCode)。
从 SQL 层面看,上述pk_example声明最终等价于:
CREATE TABLE `pk_example` ( `id` integer, `name` text NOT NULL, `email` text NOT NULL, PRIMARY KEY (`id`, `name`) );这一能力在 drizzle-kit 的迁移生成与db.push流程中同样被完整支持,声明了复合主键的表可以被稳定地 introspection、diff 与生成迁移语句,保证 schema 与数据库两端一致。
完整可运行的工程示例
结合本仓库的 SQLite 驱动(如better-sqlite3),下面是一个完整可运行的复合主键示例,覆盖建表与插入写入:
import { sqliteTable, integer, text, primaryKey } from 'drizzle-orm/sqlite-core'; import { drizzle } from 'drizzle-orm/better-sqlite3'; import Database from 'better-sqlite3'; // 1. 定义带复合主键的表 const groupMembers = sqliteTable('group_members', { groupId: integer('group_id').notNull(), userId: integer('user_id').notNull(), role: text('role').notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.groupId, table.userId] }), })); // 2. 实例化驱动(内存数据库,便于本地验证) const sqlite = new Database(':memory:'); const db = drizzle(sqlite); // 3. 建表并写入数据 db.run(sql`CREATE TABLE group_members ( group_id integer NOT NULL, user_id integer NOT NULL, role text NOT NULL, PRIMARY KEY (group_id, user_id) )`); db.insert(groupMembers).values({ groupId: 1, userId: 10, role: 'admin' });说明:
db.run(sql\...`)仅为演示底层 DDL 效果;实际工程中建议直接通过 drizzle-kit 的generate/push` 从 schema 定义生成表结构,避免手工维护 SQL。
注意事项小结
- 不要叠加单列主键:复合主键涉及的列不应再调用
.primaryKey(),否则生成重复/冲突的主键约束; - 参与列尽量显式
.notNull():SQLite 要求主键列非空,复合主键不会像单列.primaryKey()那样自动追加NOT NULL修饰(参考列构建器实现 columns/integer.ts); - 优先使用对象形态:
primaryKey({ columns: [...] })是当前推荐 API,变参数形态已标记 deprecated,且对象形态支持通过name指定约束名,便于迁移脚本的稳定对比; extraConfig建议返回数组:sqliteTable第三参数的数组形态是未来方向,对象形态为兼容旧代码而保留(详见 table.ts 中的 deprecation 注释);- 默认约束名可预期:未显式命名时,约束名遵循
${表名}_${列名(下划线连接)}_pk规则,可在getName()(primary-keys.ts)中确认。
至此,drizzle-orm-sqlite0.15.0 引入的复合主键能力已覆盖"声明语法 → 构建器实现 → 命名规则 → 工程落地"全链路。从本仓库的 sqlite-core/README.md 到 integration-tests/tests/sqlite 下的各类驱动测试,复合主键均已得到广泛使用与验证,你可以放心在生产 schema 中采用该特性来建模多列联合唯一实体。
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考