Effect 里怎么把重复插入映射成领域错误?
我想把「重复插入」映射成领域错误 OwnerTaken,先在 PRIMARY KEY 列上测试,匹配 UniqueViolation 没命中,错误最终以 defect 冒了出来,打印出来 reason 是 ConstraintError。
答案是:用 catchTag("SqlError") 检查 e.reason._tag,但匹配哪个 tag 要看驱动和约束类型。 在 SQLite 上,主键冲突是 ConstraintError,UNIQUE 约束列才是 UniqueViolation。写映射之前,先用真实驱动写一条重复插入的测试,看它实际落到哪个 reason。
SqlError 的 reason 是一个带 _tag 的联合,包括 ConnectionError、UniqueViolation、ConstraintError、DeadlockError、SerializationError 等,并且每个都有 isRetryable。这比解析错误消息字符串可靠得多,重试策略可以直接 while: (e) => e.isRetryable。下面是完整的例子,以及它的代价。TaggedError 与 catchTag 的基本用法见 Effect 类型化错误:TaggedError、catchTag 与 failure/defect 之分。
映射代码怎么写?
// file: uniq.ts
import { SqliteClient } from "@effect/sql-sqlite-node"
import { Effect, Schema } from "effect"
import { SqlClient } from "effect/sql"
export class OwnerTaken extends Schema.TaggedError<OwnerTaken>()("OwnerTaken", { owner: Schema.String }) {}
const addOwner = (owner: string) =>
Effect.gen(function*() {
const sql = yield* SqlClient.SqlClient
yield* sql`INSERT INTO owners (name) VALUES (${owner})`
}).pipe(
Effect.catchTag("SqlError", (e) =>
e.reason._tag === "UniqueViolation" || e.reason._tag === "ConstraintError"
? Effect.fail(new OwnerTaken({ owner }))
: Effect.die(e))
)
Effect.gen(function*() {
const sql = yield* SqlClient.SqlClient
yield* sql`CREATE TABLE owners (name TEXT UNIQUE)`
yield* addOwner("alice")
const e = yield* addOwner("alice").pipe(Effect.flip)
console.log(e._tag, e.owner)
}).pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:" })), Effect.runPromise)其他原因的 SqlError 走 Effect.die,因为调用方对它们做不了业务恢复。
为什么两个 tag 都放行?
因为在 SQLite 上,主键冲突和唯一约束分别落在 ConstraintError 和 UniqueViolation,只匹配一个会漏掉另一个。
代价是:非唯一性的约束失败(比如 NOT NULL)也会被当成「owner 已被占用」。这个粗糙是我明知的,我的取舍是:这张表上只有这一个唯一约束,且别的约束由 Schema 在入口拦掉了。
换成 PostgreSQL 该匹配谁?
如果在 PostgreSQL 上,应该只匹配 UniqueViolation,不要放行 ConstraintError。不过我没有在 PostgreSQL 上验证过它的分类,所以更稳妥的做法是:先写一条重复插入的测试,看它实际落到哪个 reason,再决定匹配谁。这个测试比任何文档都可靠。
版本:驱动包@effect/sql-sqlite-node用的是 4.0.5。主键冲突落在ConstraintError、UNIQUE列落在UniqueViolation,以及上面脚本的输出,都是在内存 SQLite 上看到的。PostgreSQL、MySQL 的驱动包同样有 4.0.5 版本,但我没有数据库可连,它们的分类没有验证过;「PostgreSQL 上只匹配 UniqueViolation」只是推断,建议用测试确认。