Drizzle ORM 0.29.0 版本指南:动态查询构建、读副本、集合操作与 Proxy 驱动全解析
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
Drizzle ORM0.29.0是一次功能密集的版本更新:它引入了查询构建器的"动态模式"($dynamic())、支持为复合主键与外键自定义名称、新增withReplicas读写分离方案、补齐了UNION/INTERSECT/EXCEPT等集合操作符、发布 MySQL/PostgreSQL Proxy 驱动,并为 Cloudflare D1 增加了 Batch API。本文以官方更新日志为主线,结合本仓库源码逐一剖析这些新特性的用法、设计意图与底层实现,帮助你快速评估并完成升级。
升级前请务必注意版本耦合:Drizzle ORM
0.29.0要求最低 Drizzle Kit0.20.0,反之亦然。升级 ORM 时必须同步升级 Kit;如果你的 ORM 版本低于<0.28.0,跨版本升级过程可能伴随破坏性变更,请参照各版本更新日志逐一处理。
1. 新特性总览
本次更新包含以下核心特性:
| 特性 | 适用方言 | 说明 |
|---|---|---|
MySQLbigint unsigned | MySQL | 新增无符号 bigint 支持 |
查询构建器类型强化 +$dynamic() | 全部 | 默认限制方法单次调用,动态构建需显式开启 |
primaryKey/foreignKey自定义名称 | PostgreSQL/MySQL 等 | 规避数据库 64 字符约束名截断问题 |
withReplicas读副本支持 | 全部 | 读写分离,支持自定义副本选择逻辑 |
| 集合操作符 | 全部 | UNION、INTERSECT、EXCEPT及ALL变体,支持 import 与 builder 两种用法 |
| MySQL Proxy 驱动 | MySQL | 自定义 HTTP 驱动,可对接任意服务端实现 |
| PostgreSQL Proxy 驱动 | PostgreSQL | 同上,面向 PostgreSQL |
| D1 Batch API | Cloudflare D1 | 一次请求批量执行多条语句,返回强类型结果元组 |
2. MySQLbigint unsigned:无符号大整数列
此前 MySQL 的bigint列无法在类型层面声明unsigned属性。0.29.0起可以直接通过第二个参数开启:
const table = mysqlTable('table', { id: bigint('id', { mode: 'number', unsigned: true }), });mode依旧支持'number'(JS number)与'bigint'(JS bigint)两种映射模式,unsigned在两种模式下均可使用。从 bigint 列实现 可以看到,构建器在构造时读取unsigned配置(默认false),并在生成列定义 SQL 时拼接后缀:
return `bigint${this.config.unsigned ? ' unsigned' : ''}`;也就是说,unsigned: true最终会生成BIGINT UNSIGNED列定义,这在存储无负数的 ID、计数等场景下可以扩大取值范围。该能力同时被 Drizzle Kit 0.20.0 支持,可以正确地在 introspection 与迁移生成中处理无符号 bigint 列。
3. 查询构建器类型强化与$dynamic()动态模式
3.1 为什么默认限制方法只能调用一次?
从0.29.0开始,Drizzle 的查询构建器在类型层面尽可能向 SQL 语义对齐:SQL 中一条 SELECT 只能有一个 WHERE 子句,因此.where()也只能调用一次,重复调用会直接报类型错误:
const query = db .select() .from(users) .where(eq(users.id, 1)) .where(eq(users.name, 'John')); // ❌ 类型错误:where() 只能调用一次这种约束在"一次性写完整个查询"的常规场景下是有益的——它能尽早暴露逻辑错误,也让 IDE 提示更贴近 SQL 直觉。但当你需要分步、动态地拼装查询(例如抽出一个公共函数来增强查询构建器)时,这种限制就成了障碍。
3.2 用$dynamic()开启动态模式
解决方法是调用构建器上的$dynamic()方法,它会返回一个解除"单次调用"限制的动态版本。下面是一个分页公共函数的经典示例:
function withPagination<T extends PgSelect>( qb: T, page: number, pageSize: number = 10, ) { return qb.limit(pageSize).offset(page * pageSize); } const query = db.select().from(users).where(eq(users.id, 1)); withPagination(query, 1); // ❌ 类型错误:查询构建器未处于动态模式 const dynamicQuery = query.$dynamic(); withPagination(dynamicQuery, 1); // ✅ 正常关键点在于withPagination是泛型函数:T extends PgSelect意味着返回类型会随传入构建器的类型自动收窄,因此你可以在函数内部自由地继续链式调用,甚至改变结果类型——比如追加一个join:
function withFriends<T extends PgSelect>(qb: T) { return qb.leftJoin(friends, eq(friends.userId, users.id)); } let query = db.select().from(users).where(eq(users.id, 1)).$dynamic(); query = withFriends(query); // ✅ 返回类型自动携带 join 后的新字段3.3 底层实现
以 PostgreSQL 为例,在 PgSelect 查询构建器 中,$dynamic()的实现非常轻量——它直接返回this,真正的魔法在类型层:
$dynamic(): PgSelectDynamic<this> { return this; }它通过PgSelectDynamic<T>这一包装类型,将PgSelectBase中的TDynamic extends boolean泛型参数置为true,从而在类型层面放开方法单次调用的约束;运行时行为(查询构建、SQL 生成)不受任何影响。同理,delete、update、insert等构建器在 pg-core、mysql-core、sqlite-core、singlestore-core 与 gel-core 下均实现了$dynamic(),所有方言行为一致。
实践建议:默认(非动态)模式适合静态、确定的查询,能在编译期获得最严格的校验;只有当你需要把查询构建器作为参数传递、在函数内部继续增强时才调用
$dynamic()。
4. 复合主键与外键的自定义名称
4.1 背景:64 字符约束名的隐患
当primaryKey()或foreignKey()自动生成的约束名超过数据库的 64 字符上限时,数据库引擎会对名称进行截断,可能造成约束名冲突或难以定位的问题。0.29.0允许为这两类约束显式指定名称:
const table = pgTable('table', { id: integer('id'), name: text('name'), }, (table) => ({ cpk: primaryKey({ name: 'composite_key', columns: [table.id, table.name] }), cfk: foreignKey({ name: 'fkName', columns: [table.id], foreignColumns: [table.name], }), }));primaryKey({ name, columns }):name指定复合主键约束名,columns为参与主键的列数组;foreignKey({ name, columns, foreignColumns }):name指定外键约束名,columns为本地列,foreignColumns为被引用的远端列。
同时,旧的primaryKey()调用语法已被标记为弃用(deprecated),在未来的版本中会被移除,建议新代码一律使用带name的新语法。
4.2 为什么自定义名称有意义?
- 可预测性:迁移 SQL 中的约束名稳定可读,便于数据库运维与审查;
- 规避截断:彻底解决超长自动名称被数据库截断、进而引发冲突的问题;
- 可迁移性:配合 Drizzle Kit 0.20.0 对自定义约束名的支持,introspection 与 diff 生成的迁移脚本可以保持一致。
5. 读副本支持:withReplicas读写分离
5.1 基本用法
withReplicas允许你把一个主连接(负责写)与一组只读副本连接组合起来:读操作默认随机挑选一个副本,写操作与事务一律走主实例:
const primaryDb = drizzle(client); const read1 = drizzle(client); const read2 = drizzle(client); const db = withReplicas(primaryDb, [read1, read2]); // 显式读主库 db.$primary.select().from(usersTable); // 读操作:随机选择 read1 或 read2 db.select().from(usersTable); // 写操作:始终使用主库 db.delete(usersTable).where(eq(usersTable.id, 1));withReplicas在所有方言下都可用(PostgreSQL、MySQL、SQLite、SingleStore、Gel 等)。返回的对象额外暴露了$primary(主实例)与$replicas(副本数组)两个属性,方便显式控制路由。
5.2 自定义副本选择逻辑
默认策略是Math.random()均匀随机。你完全可以注入自己的策略,例如下面这个"第一个副本 70%、第二个副本 30%"的加权随机实现:
const db = withReplicas(primaryDb, [read1, read2], (replicas) => { const weight = [0.7, 0.3]; let cumulativeProbability = 0; const rand = Math.random(); for (const [i, replica] of replicas.entries()) { cumulativeProbability += weight[i]!; if (rand < cumulativeProbability) return replica; } return replicas[0]!; });第三个参数是一个(replicas: Q[]) => Q的选择函数,你可以实现任意策略——加权轮询、基于请求特征的亲和性路由、健康检查淘汰等都不受限制。
5.3 底层实现
以 PostgreSQL 的 withReplicas 实现 为例,其核心思路是返回一个代理对象,按操作类型拆分流:
- 读操作(
select、selectDistinct、selectDistinctOn、$count、with、$with、query关系查询):调用getReplica(replicas)从副本中选取一个执行; - 写操作(
insert、update、delete、execute、transaction、refreshMaterializedView):固定走primary; - 默认选择函数即为均匀随机:
() => replicas[Math.floor(Math.random() * replicas.length)]!。
值得注意的是:事务(transaction)被强制路由到主实例,这保证了事务内读写的一致性;而$primary与$replicas属性分别持有主库与副本引用,供需要显式控制的场景使用。其余方言(mysql-core/db.ts、sqlite-core/db.ts 等)的实现模式一致。
6. 集合操作符:UNION / INTERSECT / EXCEPT
6.1 两种调用方式
0.29.0带来了完整的集合操作支持:UNION、UNION ALL、INTERSECT、INTERSECT ALL、EXCEPT、EXCEPT ALL,并提供两种等价用法。
Import 方式——把多条查询作为参数传入:
import { union } from 'drizzle-orm/pg-core'; const allUsersQuery = db.select().from(users); const allCustomersQuery = db.select().from(customers); const result = await union(allUsersQuery, allCustomersQuery);Builder 方式——在查询构建器上直接链式调用:
const result = await db.select().from(users).union(db.select().from(customers));两种方式的结果类型都基于各子查询的选择列做了严格推导,UNION ALL/INTERSECT ALL/EXCEPT ALL与不带ALL的版本一一对应(unionAll、intersectAll、exceptAll)。
6.2 底层实现
在 PostgreSQL SELECT 构建器 中,六个操作符由统一的createSetOperator(type, isAll)工厂函数生成:构建器方法(union、unionAll、intersect、intersectAll、except、exceptAll)定义在第 558–723 行,同名导出函数(供 import 方式使用)定义在第 1181–1346 行。类型层在 select.types.ts 中为每种操作符声明了PgCreateSetOperatorFn类型的属性。MySQL、SQLite 等其他方言同样提供这套 API,用法完全一致。
6.3 使用注意
- 集合操作要求各子查询的列数、列顺序与类型兼容,类型系统会在可推断的范围内给出提示;
EXCEPT(差集)返回左侧查询有而右侧没有的行;INTERSECT返回两侧共有的行;UNION默认去重,需要保留重复行时使用ALL变体;- 集合操作的结果可以继续参与分页、排序等后续处理。
7. 全新 MySQL Proxy 驱动
7.1 设计思想
MySQL Proxy 驱动让你完全自定义 HTTP 驱动实现:驱动端只负责把 SQL 文本、参数与执行方法打包发送到你的服务端,服务端可以是任何技术栈——你可以在中间层做自定义映射、审计日志、权限控制、流量治理等任意逻辑,不受任何框架限制。仓库中驱动源码位于 drizzle-orm/src/mysql-proxy,包含driver.ts、session.ts、migrator.ts与index.ts。
7.2 你需要实现的两个端点
- 查询端点(必选):接收
{ sql, params, method },执行后返回{ rows }; - 迁移端点(可选,仅在需要使用 Drizzle 迁移时):接收
{ queries },逐条执行迁移语句。
7.3 使用示例
import axios from 'axios'; import { eq } from 'drizzle-orm/expressions'; import { drizzle } from 'drizzle-orm/mysql-proxy'; import { migrate } from 'drizzle-orm/mysql-proxy/migrator'; import { cities, users } from './schema'; async function main() { const db = drizzle(async (sql, params, method) => { try { const rows = await axios.post(`${process.env.REMOTE_DRIVER}/query`, { sql, params, method, }); return { rows: rows.data }; } catch (e: any) { console.error('Error from pg proxy server:', e.response.data); return { rows: [] }; } }); await migrate(db, async (queries) => { try { await axios.post(`${process.env.REMOTE_DRIVER}/migrate`, { queries }); } catch (e) { console.log(e); throw new Error('Proxy server cannot run migrations'); } }, { migrationsFolder: 'drizzle' }); await db.insert(cities).values({ id: 1, name: 'name' }); await db.insert(users).values({ id: 1, name: 'name', email: 'email', cityId: 1, }); const usersToCityResponse = await db.select().from(users).leftJoin( cities, eq(users.cityId, cities.id), ); }从 驱动入口实现 可以看到回调签名被定义为:
export type RemoteCallback = ( sql: string, params: any[], method: 'all' | 'execute', ) => Promise<{ rows: any[]; insertId?: number; affectedRows?: number }>;也就是说你的回调必须返回{ rows },并可附带insertId(自增主键)与affectedRows(受影响行数)供 Drizzle 内部使用;method用于区分all(取多行)与execute(执行写操作)。创建连接时还可以传入第二个参数config(如{ logger, casing, schema })来启用日志、配置大小写策略或关系模式。
注意:服务端与驱动端实现均由你掌控,示例中仅以 axios 演示 HTTP 通信;你完全可以用 fetch、gRPC、WebSocket 等任何传输方式。
8. 全新 PostgreSQL Proxy 驱动
与 MySQL Proxy 对称,PostgreSQL 也迎来了自己的 Proxy 驱动,源码位于 drizzle-orm/src/pg-proxy。同样的设计:实现查询与迁移两个端点,其余自由发挥。
import axios from 'axios'; import { eq } from 'drizzle-orm/expressions'; import { drizzle } from 'drizzle-orm/pg-proxy'; import { migrate } from 'drizzle-orm/pg-proxy/migrator'; import { cities, users } from './schema'; async function main() { const db = drizzle(async (sql, params, method) => { try { const rows = await axios.post(`${process.env.REMOTE_DRIVER}/query`, { sql, params, method }); return { rows: rows.data }; } catch (e: any) { console.error('Error from pg proxy server:', e.response.data); return { rows: [] }; } }); await migrate(db, async (queries) => { try { await axios.post(`${process.env.REMOTE_DRIVER}/query`, { queries }); } catch (e) { console.log(e); throw new Error('Proxy server cannot run migrations'); } }, { migrationsFolder: 'drizzle' }); const insertedCity = await db.insert(cities).values({ id: 1, name: 'name' }).returning(); const insertedUser = await db.insert(users).values({ id: 1, name: 'name', email: 'email', cityId: 1 }); const usersToCityResponse = await db.select().from(users).leftJoin(cities, eq(users.cityId, cities.id)); }与 MySQL 版的主要区别在于 PostgreSQL 天然支持RETURNING子句,因此示例中插入后可以直接取回新行。迁移端点的回调同样接收queries数组;示例中复用/query端点逐条执行迁移语句,实际实现可以根据需要拆分独立端点。
适用场景:当你的数据库不可直接暴露给应用(如数据库在专有网络、由网关统一鉴权)、需要集中式 SQL 审计与拦截、或者想基于既有 HTTP 服务封装一层统一数据入口时,Proxy 驱动都是轻量而灵活的选择。
9. Cloudflare D1 Batch API
9.1 基本用法
针对 Cloudflare D1,0.29.0引入了db.batch():把多条操作打包成一次请求发送给 D1 执行,显著降低远程调用的往返延迟。
const batchResponse = await db.batch([ db.insert(usersTable).values({ id: 1, name: 'John' }).returning({ id: usersTable.id, }), db.update(usersTable).set({ name: 'Dan' }).where(eq(usersTable.id, 1)), db.query.usersTable.findMany({}), db.select().from(usersTable).where(eq(usersTable.id, 1)), db.select({ id: usersTable.id, invitedBy: usersTable.invitedBy }).from( usersTable, ), ]);batchResponse的类型是按顺序对应的强类型元组,例如上例推导为:
type BatchResponse = [ { id: number }[], D1Result, { id: number; name: string; verified: number; invitedBy: number | null }[], { id: number; name: string; verified: number; invitedBy: number | null }[], { id: number; invitedBy: number | null }[], ];9.2 支持放入 batch 的构建器
官方说明中,以下构建器均可作为 batch 项:
db.all(), db.get(), db.values(), db.run(), db.query.<table>.findMany(), db.query.<table>.findFirst(), db.select()..., db.update()..., db.delete()..., db.insert()...,从 D1 驱动实现 可以看到,batch的签名要求传入非空元组Readonly<[U, ...U[]]>,并返回BatchResponse<T>类型的 Promise,其中BatchItem与BatchResponse定义在 batch.ts。D1 迁移器(d1/migrator.ts)内部同样借助session.batch来一次性执行迁移语句,说明 Batch 路径在真实迁移流程中已被实际使用。
参考:Batch 语义对应 Cloudflare D1 官方客户端 API 中的
db.batch(),即在单个请求内按顺序执行多条语句。
10. 配套 Drizzle Kit 0.20.0 更新要点
由于版本强耦合,升级 ORM 0.29.0 的同时必须升级 Kit 到 0.20.0。Kit 侧的变化包括:
- 使用
defineConfig函数定义drizzle.config的新方式; - 可通过
wrangler.toml让 Drizzle Studio 访问 Cloudflare D1; - Drizzle Studio 迁移到本地地址
https://local.drizzle.studio/; - 支持
bigint unsigned; primaryKeys与foreignKeys支持自定义名称;- 环境变量自动读取;
- 若干缺陷修复与改进。
这些变化与上文第 2、4 节介绍的特性一一对应,保证 schema 定义、introspection 与迁移生成在 ORM 与 Kit 两侧保持一致。
11. 升级建议与小结
- 同步升级:ORM 0.29.0 与 Kit 0.20.0 必须成对升级;若当前 ORM 版本低于 0.28.0,请逐版本阅读更新日志处理破坏性变更;
- 静态查询优先:默认的单次调用约束更贴近 SQL 语义,能获得最强的编译期保障;仅在需要动态增强查询构建器时使用
$dynamic(); - 长约束名问题:新代码请使用带
name的primaryKey/foreignKey新语法,规避 64 字符截断风险; - 读写分离:
withReplicas开箱即用(均匀随机读副本、写走主库),需要更精细的流量分配时注入自定义选择函数即可; - 集合查询:
UNION/INTERSECT/EXCEPT及ALL变体支持 import 与 builder 两种风格,所有方言一致; - Proxy 驱动:MySQL 与 PostgreSQL 均可通过自定义回调对接任意 HTTP/远程实现,迁移端点按需实现;
- D1 批量操作:
db.batch()一次请求执行多条语句,返回强类型元组,是优化 D1 远程调用延迟的利器。
你可以结合本仓库的集成测试(如 integration-tests 下各方言测试目录)进一步观察这些 API 在真实数据库上的行为;源码级参考入口包括 pg-core/db.ts、pg-core/query-builders/select.ts、mysql-proxy/driver.ts、pg-proxy/driver.ts 与 d1/driver.ts。
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考