ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Effect SQL D1 客户端:用 D1Client.batch 以单个原子批次执行一组 SQL 语句

2026/9/14 17:23:02 拓冰建站 浏览量
Effect SQL D1 客户端:用 D1Client.batch 以单个原子批次执行一组 SQL 语句 Effect SQL D1 客户端用 D1Client.batch 以单个原子批次执行一组 SQL 语句【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect.changeset/pre/d1-batch-statements.md是 Effect monorepo 中为effect/sql-d1包规划的一次 minor 版本变更公告其核心内容是为D1Client新增batch方法允许将一组 SQL 语句作为一个原子的 D1 批次一次性执行任意一条语句失败时整批回滚。读完本文你将了解D1Client.batch的 API 形态、类型推导机制、底层实现预处理语句缓存、OpenTelemetry span 埋点、错误分类、与 D1 事务模型的边界以及在 Cloudflare Workers 环境中如何验证其原子性。变更背景与发布计划该变更以 changeset 形式声明原始内容非常简短但指向明确--- effect/sql-d1: minor --- Add D1Client.batch for executing a collection of SQL statements as a single atomic D1 batch.变更级别为minor说明这是向后兼容的新能力见 .changeset/pre/d1-batch-statements.md。当前工作区中 packages/sql/d1/package.json 的版本为4.0.0-rc.115且D1Client的全部 API 均标注since 4.0.0因此batch属于 Effect 4.0 稳定版发布前、随 RC 系列逐步合入的 D1 驱动增强能力。该能力落在 packages/sql/d1 包内包本身定位为“基于 WorkersD1Databasebinding 的 Cloudflare D1 客户端同时适配为 D1 专属服务D1Client和通用 EffectSqlClient服务”。API 形态一个类型安全的批次执行器D1Client服务定义在 packages/sql/d1/src/D1Client.ts。它在继承通用SqlClient的基础上扩展了config该客户端的配置快照和一个新的batch成员export interface D1Client extends Client.SqlClient { readonly [TypeId]: TypeId readonly config: D1ClientConfig readonly batch: const Statements extends ReadonlyArrayStatement.Statementany( statements: Statements ) Effect.Effect { readonly [K in keyof Statements]: Effect.SuccessStatements[K] }, SqlError readonly updateValues: never }从签名可以读出三个关键设计点const修饰符 元组映射入参用as const传入语句元组后返回类型是一个与原元组等长的映射元组——第i个元素的类型是第i条Statement的成功类型即该语句的查询结果行类型。批次结果与输入语句按序一一对应无需手工拆包或猜测哪个结果来自哪条语句。错误通道统一为SqlError批次执行的失败被归一化为 Effect SQL 的SqlError类型体系调用方按 Effect 的常规错误处理Effect.flip、catchTags等即可。updateValues: neverD1 驱动不支持updateValues接口层面直接以never声明不可用避免误用。batch的 JSDoc 同时给出了官方使用指引与陷阱提示D1Client.ts L62-L77适用场景当你拥有一批固定语句希望它们在一个请求内运行、失败时一起回滚时陷阱每条语句使用的是创建它的客户端上的查询/结果名变换transformQueryNames/transformResultNames在同一批次里混用不同客户端时可能得到形状不同的行结果。客户端创建与配置参数D1Client的构造入口是makeD1Client.ts L198-L372以及两个 Layer 构造器。配置接口D1ClientConfigL110-L118的参数说明如下默认值取自make内部实现参数类型默认值说明dbD1Database必填—Cloudflare Workers 的 D1 binding所有执行最终落到它之上prepareCacheSizenumber200预处理语句缓存Cache容量上限prepareCacheTTLDuration.InputDuration.minutes(10)预处理语句缓存的存活时间spanAttributesRecordstring, unknown—附加到每个 SQL span 上的自定义属性transformQueryNames(name: string) string—编译 SQL 时对标识符的改写如驼峰转下划线transformResultNames(name: string) string—对结果列名的改写生成行结果的transformRows三种提供方式import { D1Client } from effect/sql-d1 // 1. 直接 make需放在有 Scope 与 Reactivity 服务的 Effect 中 const sql yield* D1Client.make({ db }) // 2. layer从具体配置创建同时提供 D1Client 与通用 SqlClient const layer D1Client.layer({ db, prepareCacheSize: 500, prepareCacheTTL: 10 minutes }) // 3. layerConfig从 Config.WrapD1ClientConfig 创建支持环境变量注入 const layerConfig D1Client.layerConfig(Config.wrap({ db, /* ... */ }))layer与layerConfig最终都Context.add了通用SqlClient因此依赖SqlClient的下游代码withTransaction等 API 形态在 D1 客户端上同样可用——但注意下一节的事务限制。使用示例一次请求内执行多条语句仓库中的测试用例packages/sql/d1/test/Client.test.ts L65-L87展示了batch的标准用法基于 Miniflare 提供的本地 D1 模拟test/utils.ts 中的D1Miniflare服务it.effect(should execute statements in a batch, () Effect.gen(function*() { const sql yield* D1Client.D1Client yield* sqlCREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT) const results: readonly [ ReadonlyArray{ id: number; name: string }, ReadonlyArray{ id: number; name: string }, ReadonlyArray{ count: number } ] yield* sql.batch( [ sql{ id: number; name: string }INSERT INTO test (name) VALUES (${hello}) RETURNING *, sql{ id: number; name: string }INSERT INTO test (name) VALUES (${world}) RETURNING *, sql{ count: number }SELECT COUNT(*) AS count FROM test ] as const ) assert.deepStrictEqual(results, [ [{ id: 1, name: hello }], [{ id: 2, name: world }], [{ count: 2 }] ]) }).pipe(Effect.provide(D1Miniflare.layerClient)))要点as const使 TypeScript 把入参推断为固定元组results的类型因此是逐条语句结果组成的元组而不是ReadonlyArrayany每条语句仍按单条语句的方式用模板标签sqlRow书写参数绑定${hello}照常工作RETURNING *的结果按语句顺序返回证明批次结果与输入语句严格一一对应。实现原理从编译到执行的完整链路batch的具体实现是makeBatchD1Client.ts L130-L190在make中与客户端一起组装L337-L342。其执行链路可分为五步1. 空批次短路。statements.length 0时直接Effect.succeed([])不产生 span、不触碰 D1L139-L141。测试should support an empty batchClient.test.ts L170-L175验证了空批次返回空元组。2. 逐条编译并复用预处理缓存。对每条语句const statement transformer undefined ? original : yield* transformer(original, options.getClient(), fiber, span) const [sql, params] statement.compile() queryTexts.push(sql) transforms.push((statement as StatementWithTransformRows).transformRows) prepared.push((yield* Cache.get(options.prepareCache, sql)).bind(...params))先读取 FiberRef 中的Statement.CurrentTransformer支持在批次外层通过Effect.provideService注入语句级变换测试should apply statement transformers in a batch用该机制把两条SELECT都改写为SELECT 3 AS value见 Client.test.ts L177-L194使用 SQLite 方言编译器Statement.makeCompilerSqlitepackages/effect/src/unstable/sql/Statement.ts编译出[sql, params]并以编译后的 SQL 文本为 key 从prepareCache取D1PreparedStatement随后bind(...params)缓存容量与 TTL 即上文配置表中的prepareCacheSize/prepareCacheTTLlookup 失败会包装为SqlErroroperation: prepare。3. Span 埋点。整个批次包在一个名为sql.execute的 client span 内并写入db.operation.name batch区别于单条语句的执行db.query.text 所有语句 SQL 以; 拼接后的文本db.system.name sqlite以及用户经spanAttributes传入的自定义属性L206-L209。这使链路追踪中一个批次呈现为单个 span便于观测批处理开销。4. 调用 D1 原生 batch 并归一化错误。核心执行L169-L180// D1 batches execute on the binding directly and intentionally // cannot participate in SqlClient transactions. const responses yield* Effect.tryPromise({ try: () options.db.batchRecordstring, unknown(prepared).then((responses) { for (const response of responses) { if (response.error) { throw response.error } } return responses }), catch: (cause) new SqlError({ reason: classifyError(cause, Failed to execute batch, execute) }) })直接调用 Workers binding 的db.batch(prepared)——这是 D1 平台级的事务性批量执行接口源码注释明确指出批次“有意不参与 SqlClient 事务”任一response.error都会抛出经tryPromise捕获后统一包装为SqlError其reason为UnknownErroroperation: executemessage: Failed to execute batch。5. 按语句施加结果变换。最后把每条语句编译期登记的transformRows由transformResultNames经Statement.defaultTransforms(...).array生成按索引逐条应用到对应结果行上L182-L187。测试should apply result transforms to batch resultsClient.test.ts L89-L125还验证了一个细节批次中每条语句应用的是其所属客户端的变换因此混合两个客户端的语句可以分别得到驼峰与下划线两种形状的列名——这正是 JSDoc 中提到的 “Gotchas” 的实测体现。原子性语义失败即整批回滚batch的“原子”语义由 D1 平台的 batch 机制保证仓库测试给出了直接证据。should roll back a failed batchClient.test.ts L154-L168it.effect(should roll back a failed batch, () Effect.gen(function*() { const sql yield* D1Client.D1Client yield* sqlCREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT UNIQUE) const error yield* sql.batch([ sqlINSERT INTO test (name) VALUES (${duplicate}), sqlINSERT INTO test (name) VALUES (${duplicate}) ]).pipe(Effect.flip) assert.strictEqual(error.reason._tag, UnknownError) assert.strictEqual(error.reason.operation, execute) const rows yield* sqlSELECT * FROM test assert.deepStrictEqual(rows, []) }).pipe(Effect.provide(D1Miniflare.layerClient)))第一条INSERT本应写入成功但同批第二条命中UNIQUE约束失败最终表中SELECT *返回空——先前已“成功”的语句也被回滚且错误按预期分类为UnknownError / execute。这与 changeset 中 “single atomic D1 batch” 的承诺一致。与 D1 事务模型的边界D1 驱动的模块头注释与make实现共同划定了边界transactionAcquirer Effect.die(transactions are not supported in D1)L323即 D1 不支持驱动层事务executeStream未实现Stream.die流式查询不可用因此batch是 D1 场景下实现“一组语句同生共死”的推荐且唯一的原子手段批次直接执行在 binding 上天然绕开 SqlClient 事务层。对应的行为验证should defect on transactionsClient.test.ts L196-L208sql.withTransaction在 D1 上以 defectdies终结查询不受影响should defect when batching in a transactionClient.test.ts L210-L224把batch包进withTransaction同样以 defect 终结明确禁止“批次套事务”的用法。实践含义在 D1 上不要依赖withTransaction需要多语句原子性时直接用batch需要更强控制如跨批次编排时应改选支持事务的驱动。本地验证方式Miniflare 模拟 D1如果你想在本地复现上述行为可参照仓库测试的做法test/utils.ts用miniflare起一个带 D1 binding 的 Workers 环境miniflare.getD1Database(DB)取出D1Database再交给D1Client.layer({ db })。测试中的 Worker 配置compatibilityDate、env.DB.type: d1等可直接照搬。运行方式即标准 pnpm workspace 测试流程在仓库根目录执行 vitest 运行packages/sql/d1下的用例即可无需真实 Cloudflare 账号。小结D1Client.batch以一行 changeset 公告.changeset/pre/d1-batch-statements.md宣告却落地为一套完整的工程实现API 层const元组入参 按序映射结果类型批次结果与语句一一对应且全类型安全执行层编译后 SQL 走预处理语句缓存默认容量 200、TTL 10 分钟经db.batch()平台级原子执行错误统一归一为SqlError可观测层单 span 覆盖整批db.operation.namebatch与拼接后的db.query.text便于追踪语义层失败整批回滚且有意与 SqlClient 事务隔离D1 不支持驱动事务测试用例对回滚、空批次、语句变换器、变换混用等边界均有覆盖。在 Effect 生态中使用 Cloudflare D1 时batch应当成为“一次请求内多条写操作”的默认模式完整实现可继续阅读 packages/sql/d1/src/D1Client.ts 与 packages/sql/d1/test/Client.test.ts。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考