ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Drizzle ORM 0.29.0 版本指南:动态查询构建、读副本、集合操作与 Proxy 驱动全解析

Drizzle ORM 0.29.0 版本指南:动态查询构建、读副本、集合操作与 Proxy 驱动全解析 Drizzle ORM 0.29.0 版本指南动态查询构建、读副本、集合操作与 Proxy 驱动全解析【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-ormDrizzle 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读副本支持全部读写分离支持自定义副本选择逻辑集合操作符全部UNION、INTERSECT、EXCEPT及ALL变体支持 import 与 builder 两种用法MySQL Proxy 驱动MySQL自定义 HTTP 驱动可对接任意服务端实现PostgreSQL Proxy 驱动PostgreSQL同上面向 PostgreSQLD1 Batch APICloudflare D1一次请求批量执行多条语句返回强类型结果元组2. MySQLbigint unsigned无符号大整数列此前 MySQL 的bigint列无法在类型层面声明unsigned属性。0.29.0起可以直接通过第二个参数开启const table mysqlTable(table, { id: bigint(id, { mode: number, unsigned: true }), });mode依旧支持numberJS number与bigintJS 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 withPaginationT 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意味着返回类型会随传入构建器的类型自动收窄因此你可以在函数内部自由地继续链式调用甚至改变结果类型——比如追加一个joinfunction withFriendsT 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(): PgSelectDynamicthis { return this; }它通过PgSelectDynamicT这一包装类型将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 / EXCEPT6.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 API9.1 基本用法针对 Cloudflare D10.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[]]并返回BatchResponseT类型的 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 D1Drizzle Studio 迁移到本地地址https://local.drizzle.studio/支持bigint unsignedprimaryKeys与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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进