关于 drizzle-orm
面向使用 Drizzle ORM 的应用仓库技能,限定其数据库为 PostgreSQL,并规范 Schema、迁移、类型化查询、驱动边界和类型处理。
主要功能
- 01保持 Schema 定义可静态检查
- 02结合快照审查迁移 SQL
- 03优先使用类型化查询构建器
- 04在边界校验外部输入
- 05为品牌类型选择无运行时转换的写法
适用场景
- 01新增或修改 PostgreSQL Schema 与迁移
- 02构建类型化应用查询
- 03选择 $type () 或 customType
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill drizzle-orm安装后在 Claude Code 中输入/drizzle-orm确认已加载。
详细介绍
Drizzle ORM 指南
参考仓库
- Drizzle ORM:类似 SQL 的查询构建器 TypeScript ORM
Drizzle 在这里使用的数据库只有 Postgres:apps/api/drizzle.config.ts 声明了
dialect: 'postgresql',且唯一导入的方言是 pg-core。仓库中的其他 SQLite 引擎都不是 Drizzle,因此关于 SQLite 的问题通常与它们有关。
基于上游资料
当 Drizzle 模式定义、迁移快照、查询构建器 API、列类型、自定义类型或驱动集成会影响正确性时,在依赖记忆前,先基于源代码或文档进行核实。如果有 DeepWiki MCP 可用,针对 drizzle-team/drizzle-orm 提出一个范围明确的问题。如果 DeepWiki 不可用或未收录该仓库,请直接查阅上游源代码或官方文档。将 DeepWiki 作为了解方向的参考,然后在修改代码前,对关键细节通过本地已安装的类型、生成的迁移、源代码、驱动版本或官方文档进行核实。
对于下文已记录的仓库本地模式命名和存储边界约定,无需查询 DeepWiki。
模式与迁移规则
- 从应用的数据库模块导出一个模式对象,并将同一个对象传递给
drizzle(...)和 Drizzle Kit 配置。 - 明确声明表定义、关系和模式导出。避免使用 Drizzle Kit 无法静态检查的动态模式构造。
- 将 Drizzle Kit 快照视为差异来源的事实依据。一起审查生成的 SQL 和快照变更。
- 每个数据库只选择一种命名大小写策略。除非由边界负责映射,否则不要混用应用层 camelCase 和临时 SQL 别名。
- 当外部输入在 IO 边界转换为行时,使用
drizzle-zod、drizzle-valibot或本地模式解析器。不要将推断出的插入类型视为运行时验证。
查询构建器规则
- 应用查询优先使用类型化查询构建器。仅在查询构建器无法简洁表达某个表达式时才使用原始 SQL。
- 将联接和选取的数据形状放在负责响应契约的调用方附近。
- 在模式定义中,为需要相应查询模式的表添加索引。
驱动边界
bun:sqlite和better-sqlite3是本地同步 SQLite 驱动。不要在 Cloudflare Workers 中使用它们。- D1 是具有 Worker 特定行为的 Cloudflare 绑定。将其限制在 Worker 代码和生成的绑定中。
对品牌字符串使用 $type(),而非 customType
需要使用品牌化 TypeScript 类型的列,但不需要实际转换数据时,请使用 $type<T>(),而不是 customType。
规则
如果 toDriver 和 fromDriver 都会是恒等函数 (x) => x,请改用 $type<T>()。
原因
即使函数是恒等函数,customType 仍会对每一行调用 mapFromDriverValue:
// drizzle-orm/src/utils.ts - runs for EVERY column of EVERY row
const rawValue = row[columnIndex]!;
const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
查询 1000 行、包含 3 个日期列,就会产生 3000 次无用的函数调用。
不好的模式
// Runtime overhead for identity functions
customType<{ data: DateTimeString; driverParam: DateTimeString }>({
dataType: () => 'text',
toDriver: (value) => value, // called on every write
fromDriver: (value) => value, // called on every read
});
好的模式
// Zero runtime overhead - pure type assertion
text().$type<DateTimeString>();
$type<T>() 只在编译时覆盖类型:
// drizzle-orm/src/column-builder.ts
$type<TType>(): $Type<this, TType> {
return this as $Type<this, TType>;
}
何时使用 customType
仅当数据在应用与数据库之间确实会发生转换时使用:
// JSON: object ↔ string - actual transformation
customType<{ data: UserPrefs; driverParam: string }>({
toDriver: (value) => JSON.stringify(value),
fromDriver: (value) => JSON.parse(value),
});
在中间表示中保留数据
优先让数据在系统中以序列化形式(字符串)流转,只在边缘(UI 组件)进行解析。
原则:如果数据以序列化形式进入并以序列化形式离开,就让它在中间过程保持序列化。在确实需要丰富表示的边缘进行解析。
示例:DateTimeString
不要在数据库层将 DateTimeString 解析为 Temporal.ZonedDateTime:
// Bad: parse on every read, re-serialize at API boundaries
customType<{ data: Temporal.ZonedDateTime; driverParam: string }>({
fromDriver: (value) => fromDateTimeString(value),
});
保留字符串格式,直到 UI 确实需要它:
// Good: string stays string, parse only in date-picker component
text().$type<DateTimeString>();
// In UI component:
const temporal = fromDateTimeString(row.createdAt);
// After edit:
const updated = toDateTimeString(temporal);