- 文档
- 教程
- 知识库
【免费下载链接】til
:memo: Today I Learned
在使用 Drizzle ORM 操作 PostgreSQL 时,如何为表定义一个既具备bigint数据类型、又使用现代 identity column 机制自动生成的主键?本文基于 drizzle/create-bigint-identity-column-for-primary-key.md 展开,给出可直接运行的 TypeScript 表定义、drizzle-kit generate生成的真实迁移 SQL,并结合本仓库中关于迁移日志与插入返回值的相关笔记,帮你彻底掌握这一常见建模场景。
完整示例:定义一张带 bigint 自增主键的表
假设我们要创建一张users表,主键id使用 PostgreSQL 的bigint类型,并通过 identity column 让数据库自动生成主键值。在 Drizzle 中,完整定义如下:
import { pgTable, bigint, text, timestamp, } from "drizzle-orm/pg-core"; // Users table export const users = pgTable("users", { id: bigint({ mode: 'bigint' }).primaryKey().generatedAlwaysAsIdentity(), email: text("email").unique().notNull(), name: text("name").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), });这段代码的核心在于id这一列的链式调用:
- 导入
bigint:从drizzle-orm/pg-core中导入bigint,用于声明这一列的数据类型为 PostgreSQL 的bigint; - 指定主键:通过
.primaryKey()将id声明为表的主键; - 声明自增语义:通过
.generatedAlwaysAsIdentity()将默认值语义声明为generated always as identity,即主键值由数据库侧的 identity 机制自动生成。
为什么bigint必须指定mode
这是本示例中最容易踩坑的一点:bigint必须显式传入mode配置,否则运行时会抛出如下错误:
TypeError: Cannot read properties of undefined (reading 'mode')原因是 Drizzle 中的bigint()需要依赖mode来决定该列在 JavaScript / TypeScript 侧的序列化方式:
mode: 'bigint':该列的值在查询结果中会被还原为 JavaScript 的BigInt类型,适合表示超出Number.MAX_SAFE_INTEGER(9007199254740991)范围的整数;mode: 'number':该列的值会被还原为 JavaScript 的Number类型,适合范围在安全整数之内的场景。
由于 PostgreSQL 的bigint是 8 字节有符号整数,取值范围为-9223372036854775808到9223372036854775807(参见仓库笔记 postgres/integers-in-postgres.md),上限远超过 JSNumber的安全范围。因此当主键可能增长到较大数值时,mode: 'bigint'是更稳妥的选择。选择number模式虽然代码上可行,但需要你自行确保取值不会溢出安全整数范围。
生成的迁移 SQL:drizzle-kit generate 的产物
当表结构定义好后,运行迁移生成命令:
npx drizzle-kit generateDrizzle 会根据 schema 生成对应的 SQL 迁移文件,其中会包含类似如下的建表语句:
--> statement-breakpoint CREATE TABLE IF NOT EXISTS "users" ( "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), "email" text NOT NULL, "name" text NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, CONSTRAINT "users_email_unique" UNIQUE("email") );这份 SQL 可以逐项与上面的 TypeScript 定义对上号:
| TypeScript 声明 | 生成的 SQL 片段 | 说明 |
|---|---|---|
bigint({ mode: 'bigint' }) | "id" bigint | 列数据类型为bigint(8 字节有符号整数) |
.primaryKey() | PRIMARY KEY | 列被声明为表的主键 |
.generatedAlwaysAsIdentity() | GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1) | 由数据库自动生成主键值,并自动创建配套的序列users_id_seq |
text("email").unique().notNull() | "email" text NOT NULL+CONSTRAINT "users_email_unique" UNIQUE("email") | 非空且唯一 |
text("name").notNull() | "name" text NOT NULL | 非空 |
timestamp("created_at").defaultNow().notNull() | "created_at" timestamp DEFAULT now() NOT NULL | 默认取当前时间且非空 |
注意 identity 子句中的参数:INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1。这里的MAXVALUE 9223372036854775807恰好就是bigint类型的最大值上限,说明 PostgreSQL 为该 identity 列自动生成的序列直接对齐了bigint的完整取值范围。
为什么用 identity column 而不是 serial
主键自增在历史上最流行的写法是serial(或bigserial)。本仓库的 postgres/generate-modern-primary-key-columns.md 专门讨论了这一问题:PostgreSQL 官方 wiki 明确建议新应用不要使用serial,而应使用 identity columns,原因是 serial 类型在 schema、依赖和权限管理上存在一些绕不开的怪异行为。
因此,.generatedAlwaysAsIdentity()映射到的GENERATED ALWAYS AS IDENTITY正是当前推荐的现代做法。与之相对,Drizzle 也提供了.generatedByDefaultAsIdentity(),对应 SQL 中的GENERATED BY DEFAULT AS IDENTITY,二者的区别在于:
GENERATED ALWAYS AS IDENTITY:应用无法显式写入该列的主键值(除非使用OVERRIDING SYSTEM VALUE),保证值完全由数据库生成;GENERATED BY DEFAULT AS IDENTITY:允许应用在插入时显式提供该列的值,仅在未提供时由数据库生成。
对于不希望业务代码干预主键生成的场景,本示例采用的ALWAYS语义是更严格、更安全的选择。
结合迁移与插入流程的完整实战闭环
定义好 identity 主键只是第一步,把整个流程串起来还需要理解两件事:迁移如何被跟踪,以及插入后如何拿回数据库自动生成的id。
迁移文件如何被记录
运行npx drizzle-kit generate生成 SQL 迁移文件后,还需要运行npx drizzle-kit migrate将其应用到数据库。仓库笔记 drizzle/drizzle-tracks-migrations-in-a-log-table.md 说明:Drizzle 会像其他 SQL 迁移工具一样,在数据库中使用一张日志表(默认名为__drizzle_migrations,位于drizzleschema 下)记录每个迁移文件的 SHA256 哈希和运行时间戳,从而判断哪些迁移已经执行、哪些还没有执行。所以上述建表 SQL 一旦被migrate应用,就会被登记在这张日志表中,后续重复运行不会再次执行。
插入后获取自动生成的主键值
由于id由数据库侧的 identity 机制生成,普通的insert返回值是QueryResult<never>,拿不到任何有用的数据。仓库笔记 drizzle/get-fields-for-inserted-row.md 给出了标准解法:在 insert 语句后追加.returning(),让 PostgreSQL 返回插入行的全部字段;如果只需要新行的id,还可以做部分返回:
await db .insert(users) .values({ email, name, }) .returning({ id: users.id })这两篇仓库内的相关笔记分别对应 drizzle/drizzle-tracks-migrations-in-a-log-table.md 与 drizzle/get-fields-for-inserted-row.md,与本文主题共同构成"定义 identity 主键 → 生成并应用迁移 → 插入并回读主键"的完整实战链路。
小结
本文围绕 Drizzle ORM + PostgreSQL 的bigintidentity 主键,覆盖了从 TypeScript 表定义、mode参数的必要性、drizzle-kit generate生成的迁移 SQL 解读,到现代 identity column 相对serial的优势,以及迁移跟踪与插入返回值两个相邻环节。掌握这一组合,你就可以放心地为新表设计大规模、数据库自主生成的主键,并让 Drizzle 的 schema 定义、迁移与运行时查询保持一致。
更多相关主题可继续阅读仓库中的 Drizzle 分类 与 PostgreSQL 分类 笔记。
- 文档
- 教程
- 知识库
【免费下载链接】til
:memo: Today I Learned
相关推荐
drizzle-orm-pg 0.15.1:PostgreSQL Schema(模式)完整支持与使用指南
drizzle orm pg 0.15.1:PostgreSQL Schema(模式)完整支持与使用指南 导读 本文以 drizzle orm pg 0.15.
后端数据库ORMRufus 制作U盘启动盘指南:3 步做出能开机的U盘,老电脑也适用
Rufus 制作U盘启动盘指南:3 步做出能开机的U盘,老电脑也适用 插上U盘、选好镜像、点了开始,进度条走到一半报错,或者做完电脑根本不认盘。Rufus 是一
桌面应用开发工具Drizzle ORM 0.27.1 新增 Neon HTTP 驱动支持:在 Serverless 环境中使用 `drizzle-orm/neon-http`
Drizzle ORM 0.27.1 新增 Neon HTTP 驱动支持:在 Serverless 环境中使用 drizzle orm/neon http dr
后端数据库ORM
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考