drizzle-orm 0.24.1 版本解析:onConflict 目标列修复与条件表达式 JSDoc 文档化
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
导读
本文围绕 drizzle-orm 0.24.1 版本的两项核心变更展开:一是修复onConflict(冲突目标列)处理逻辑,解决 upsert 场景下冲突目标识别不准确的问题;二是为 SQL 条件表达式(eq、ne、and、or等)补齐 JSDoc 文档,正式开启 drizzle-orm 的源码文档化之路。读完本文,你将理解 drizzle-orm 中onConflictDoNothing/onConflictDoUpdate的完整配置方式、各数据库方言的差异,以及条件表达式 JSDoc 的代码示例如何反哺开发体验。
一、0.24.1 版本变更总览
根据 changelogs/drizzle-orm/0.24.1.md 的官方记录,该版本包含两类变更:
| 类别 | 内容 | 贡献者 |
|---|---|---|
| Bug 修复 | 修复onConflict的目标(target)列处理(PR #475) | @wkunert |
| 文档改进 | 为 SQL 条件表达式补充 JSDoc 文档(PR #467) | @tmcw |
其中 JSDoc 变更在 changelog 中被特别标注:> Thanks to @tmcw we have started our way to get JSDoc documentation,即 drizzle-orm 从 0.24.1 开始,正式将「为公开 API 编写 JSDoc」纳入项目维护流程。
二、Bug 修复:onConflict 目标列处理
2.1 什么是 onConflict / upsert
on conflict是 PostgreSQL 与 SQLite 原生的「插入冲突处理」语法,即通常所说的 upsert:当插入的行与表中已有数据产生唯一约束/主键冲突时,执行备选动作(忽略或更新)。drizzle-orm 将这一能力封装为链式 API:
onConflictDoNothing():冲突时什么都不做,放弃本次插入;onConflictDoUpdate({ target, set, where }):冲突时更新已有行;- MySQL / SingleStore 方言则对应
onDuplicateKeyUpdate({ set })。
0.24.1 修复的正是这些方法在目标列(target)解析与生成上存在的问题,例如当传入多个冲突目标列、或目标列需要经过标识符转义时,生成的 SQL 不够准确。
2.2 SQLite 实现:onConflict 目标列如何生成 SQL
SQLite 侧的实现在 drizzle-orm/src/sqlite-core/query-builders/insert.ts 中。
onConflictDoNothing(insert.ts#L306-L317)的完整逻辑:
onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this { if (!this.config.onConflict) this.config.onConflict = []; if (config.target === undefined) { this.config.onConflict.push(sql` on conflict do nothing`); } else { const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`; const whereSql = config.where ? sql` where ${config.where}` : sql``; this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`); } return this; }关键点:
- 不传 target时生成
on conflict do nothing,即对所有唯一约束冲突都静默跳过; - 传入 target时生成
on conflict (<列>) do nothing,只针对指定列上的冲突触发; - target 既可以是单个
IndexColumn,也可以是数组,源码通过Array.isArray统一包装; - 可选
where子句会被追加为where <条件>,实现「仅当满足某条件时才忽略冲突」的精细化控制。
onConflictDoUpdate(insert.ts#L348-L366)的配置更丰富,它校验了where与targetWhere/setWhere不能混用:
onConflictDoUpdate(config: SQLiteInsertOnConflictDoUpdateConfig<this>): this { if (config.where && (config.targetWhere || config.setWhere)) { throw new Error( 'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.', ); } // ... const whereSql = config.where ? sql` where ${config.where}` : undefined; const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined; const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined; const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`; const setSql = this.dialect.buildUpdateSet(this.config.table, mapUpdateSet(this.config.table, config.set)); this.config.onConflict.push( sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`, ); return this; }这里对 target 的统一处理(单列与多列数组归一化)正是 0.24.1 修复的核心区域之一,修复后多目标列的 SQL 生成保持一致。
最终这些onConflict片段由方言层拼装进完整的 INSERT 语句。在 drizzle-orm/src/sqlite-core/dialect.ts#L513、dialect.ts#L587-L593 可以看到:
const onConflictSql = onConflict?.length ? sql.join(onConflict) : undefined; // ... return sql`${withSql}insert into ${table} ${insertOrder} ${valuesSql}${onConflictSql}${returningSql}`;即多个 onConflict 片段通过sql.join顺序拼接,紧随 values 之后、returning 之前,与原生 SQL 语法位置完全对应。
2.3 PostgreSQL 实现:target 列的标识符转义
PG 侧的实现在 drizzle-orm/src/pg-core/query-builders/insert.ts。onConflictDoNothing(insert.ts#L323-L338)在指定 target 时对列名做了显式转义处理:
targetColumn = Array.isArray(config.target) ? config.target.map((it) => this.dialect.escapeName(this.dialect.casing.getColumnCasing(it))).join(',') : this.dialect.escapeName(this.dialect.casing.getColumnCasing(config.target)); // ... this.config.onConflict = sql`(${sql.raw(targetColumn)})${whereSql} do nothing`;escapeName保证列名按 PG 规则加引号转义,而getColumnCasing则负责应用列命名策略(snake_case 等 casing 配置),这在启用自定义命名映射的项目中至关重要——这也是 onConflict 目标列容易出错的典型场景之一,0.24.1 修复后该路径被统一收敛。
onConflictDoUpdate(insert.ts#L369-L387)同样内置了where(已弃用)与targetWhere/setWhere互斥的运行时校验,并在参数注释中标注了targetWhere/setWhere的替代关系,从类型与运行时双层约束保证 API 正确使用。
2.4 MySQL / SingleStore:没有 on conflict 的替代方案
MySQL 与 SingleStore 并不支持on conflict语法,drizzle-orm 为它们提供的是onDuplicateKeyUpdate。在 drizzle-orm/src/mysql-core/query-builders/insert.ts#L273-L279:
onDuplicateKeyUpdate( config: MySqlInsertOnDuplicateKeyUpdateConfig<this>, ): MySqlInsertWithout<this, TDynamic, 'onDuplicateKeyUpdate'> { const setSql = this.dialect.buildUpdateSet(this.config.table, mapUpdateSet(this.config.table, config.set)); this.config.onConflict = sql`update ${setSql}`; return this as any; }由于 MySQL 没有「冲突时 do nothing」的原生写法,官方 JSDoc(insert.ts#L263-L271)给出了一个巧妙的等价实现:将任意一列设为自身值,例如onDuplicateKeyUpdate({ set: { id: sqlid} }),即可实现「冲突时不做任何修改」的 no-op 效果。SingleStore 侧的实现(drizzle-orm/src/singlestore-core/query-builders/insert.ts#L246)与之完全对称。
2.5 使用示例:修复后的 onConflict 实战
import { sql } from 'drizzle-orm'; // 冲突时静默忽略(不指定 target:任一唯一约束冲突均跳过) await db.insert(cars) .values({ id: 1, brand: 'BMW' }) .onConflictDoNothing(); // 精确指定冲突目标列 + 条件 await db.insert(cars) .values({ id: 1, brand: 'BMW' }) .onConflictDoNothing({ target: cars.id, where: sql`${cars.deletedAt} is null` }); // 冲突时更新(多目标列数组) await db.insert(cars) .values({ id: 1, brand: 'BMW' }) .onConflictDoUpdate({ target: [cars.id, cars.plateNo], set: { brand: 'Porsche' }, }); // MySQL 场景:无冲突则插入,冲突则更新 await db.insert(cars) .values({ id: 1, brand: 'BMW' }) .onDuplicateKeyUpdate({ set: { brand: 'Porsche' } });三、文档改进:条件表达式的 JSDoc 体系
3.1 起点:conditions.ts 的条件算子
0.24.1 的第二个变更来自 @tmcw 的 PR #467——为drizzle-orm/src/sql/expressions/conditions.ts中的条件表达式补齐 JSDoc。该文件集中定义了 drizzle-orm 最常用的过滤原语,且文档风格与正文保持统一:先一句功能说明,再给出真实可运行的 TypeScript 示例,最后用@see关联相关算子。
以eq为例(conditions.ts#L44-L64):
/** * Test that two values are equal. * * Remember that the SQL standard dictates that * two NULL values are not equal, so if you want to test * whether a value is null, you may want to use * `isNull` instead. * * ## Examples * * ```ts * // Select cars made by Ford * db.select().from(cars) * .where(eq(cars.make, 'Ford')) * ``` * * @see isNull for a way to test equality to NULL. */ export const eq: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => { return sql`${left} = ${bindIfParam(right, left)}`; };这份 JSDoc 不只是形式化的注释,它承担了三层职责:
- 语义澄清:明确指出 SQL 标准中两个 NULL 不相等,提醒用户判空应改用
isNull; - 示例即测试:
eq(cars.make, 'Ford')这样可直接粘贴运行的片段,降低了 API 理解成本; - 算子互链:通过
@see把eq/ne/isNull/isNotNull等易混淆的算子串联成一张知识网络。
同样的模式覆盖了ne(conditions.ts#L66-L86,示例为ne(cars.make, 'Ford'))、以及组合算子and/or(conditions.ts#L88-L125、conditions.ts#L127-L143)。其中and/or的文档还特别说明了「值为undefined的条件会被自动忽略」这一行为——这是 drizzle-orm 允许动态拼接过滤条件的底层保证。
3.2 扩散:query-builder 各方法的 JSDoc
条件表达式只是起点。在 0.24.1 之后,这套 JSDoc 风格快速扩散到各方言的 query-builder 中。以 SQLite 的 select builder 为例,drizzle-orm/src/sqlite-core/query-builders/select.ts 中几乎所有公开方法都带有结构化的 JSDoc:
leftJoin(select.ts#L289-L316):说明 left join 对无匹配行的处理(关联表列置为 null),并给出带类型标注的返回示例{ user: User; pets: Pet | null; }[];union(select.ts#L995-L1020):说明去重语义,并同时展示「函数式调用」与「链式调用」两种等价写法;- insert builder 中的
onConflictDoNothing/onConflictDoUpdate(drizzle-orm/src/sqlite-core/query-builders/insert.ts#L284-L347)则携带指向官方文档的See docs:链接,将 IDE 内的智能提示与完整文档打通。
这些 JSDoc 的实际价值在于:开发者在 IDE 中悬停即可看到方法签名、语义说明与可运行的示例,无需跳转文档站点;同时为drizzle-kit的 introspection、类型提示测试等下游工具提供了稳定的元信息基础。
3.3 对后续版本的启示
从源码结构看,JSDoc 工作贯穿了后续版本:PG 的PgInsertOnConflictDoUpdateConfig中targetWhere/setWhere的弃用标注(drizzle-orm/src/pg-core/query-builders/insert.ts#L174-L178)即是该文档化进程的延续。而 gel-core 中 onConflict 方法处于注释状态(drizzle-orm/src/gel-core/query-builders/insert.ts#L304-L367),说明 Gel 方言的 upsert 支持仍在演进中,这也是读者在选用方言时可以留意的差异点。
四、结语
drizzle-orm 0.24.1 是一个「小而关键」的版本:onConflict目标列修复让 PostgreSQL 与 SQLite 的 upsert 在标识符转义、多列目标场景下更加可靠;而条件表达式的 JSDoc 化则开启了项目长期的文档体系建设。对于使用 drizzle-orm 的开发者,理解onConflictDoNothing/onConflictDoUpdate/onDuplicateKeyUpdate三套 API 的方言差异,并善用 IDE 中逐步完善的 JSDoc 提示,是写出健壮写入逻辑的基础。
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考