Drizzle ORM 0.29.0 版本指南:动态查询构建、读副本、集合操作与 Proxy 驱动全解析
2026/9/19 19:33:11 网站建设 项目流程

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 ORM0.29.0要求最低 Drizzle Kit0.20.0,反之亦然。升级 ORM 时必须同步升级 Kit;如果你的 ORM 版本低于<0.28.0,跨版本升级过程可能伴随破坏性变更,请参照各版本更新日志逐一处理。


1. 新特性总览

本次更新包含以下核心特性:

特性适用方言说明
MySQLbigint unsignedMySQL新增无符号 bigint 支持
查询构建器类型强化 +$dynamic()全部默认限制方法单次调用,动态构建需显式开启
primaryKey/foreignKey自定义名称PostgreSQL/MySQL 等规避数据库 64 字符约束名截断问题
withReplicas读副本支持全部读写分离,支持自定义副本选择逻辑
集合操作符全部UNIONINTERSECTEXCEPTALL变体,支持 import 与 builder 两种用法
MySQL Proxy 驱动MySQL自定义 HTTP 驱动,可对接任意服务端实现
PostgreSQL Proxy 驱动PostgreSQL同上,面向 PostgreSQL
D1 Batch APICloudflare 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 生成)不受任何影响。同理,deleteupdateinsert等构建器在 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 实现 为例,其核心思路是返回一个代理对象,按操作类型拆分流:

  • 读操作selectselectDistinctselectDistinctOn$countwith$withquery关系查询):调用getReplica(replicas)从副本中选取一个执行;
  • 写操作insertupdatedeleteexecutetransactionrefreshMaterializedView):固定走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带来了完整的集合操作支持:UNIONUNION ALLINTERSECTINTERSECT ALLEXCEPTEXCEPT 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的版本一一对应(unionAllintersectAllexceptAll)。

6.2 底层实现

在 PostgreSQL SELECT 构建器 中,六个操作符由统一的createSetOperator(type, isAll)工厂函数生成:构建器方法(unionunionAllintersectintersectAllexceptexceptAll)定义在第 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.tssession.tsmigrator.tsindex.ts

7.2 你需要实现的两个端点

  1. 查询端点(必选):接收{ sql, params, method },执行后返回{ rows }
  2. 迁移端点(可选,仅在需要使用 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,其中BatchItemBatchResponse定义在 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 侧的变化包括:

  1. 使用defineConfig函数定义drizzle.config的新方式;
  2. 可通过wrangler.toml让 Drizzle Studio 访问 Cloudflare D1;
  3. Drizzle Studio 迁移到本地地址https://local.drizzle.studio/
  4. 支持bigint unsigned
  5. primaryKeysforeignKeys支持自定义名称;
  6. 环境变量自动读取;
  7. 若干缺陷修复与改进。

这些变化与上文第 2、4 节介绍的特性一一对应,保证 schema 定义、introspection 与迁移生成在 ORM 与 Kit 两侧保持一致。


11. 升级建议与小结

  • 同步升级:ORM 0.29.0 与 Kit 0.20.0 必须成对升级;若当前 ORM 版本低于 0.28.0,请逐版本阅读更新日志处理破坏性变更;
  • 静态查询优先:默认的单次调用约束更贴近 SQL 语义,能获得最强的编译期保障;仅在需要动态增强查询构建器时使用$dynamic()
  • 长约束名问题:新代码请使用带nameprimaryKey/foreignKey新语法,规避 64 字符截断风险;
  • 读写分离withReplicas开箱即用(均匀随机读副本、写走主库),需要更精细的流量分配时注入自定义选择函数即可;
  • 集合查询UNION/INTERSECT/EXCEPTALL变体支持 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询