Drizzle ORM 与 Kysely 集成指南:用 Kyselify 把 Drizzle 表定义无缝接入 Kysely 查询构建器
2026/9/19 12:41:25 网站建设 项目流程

Drizzle ORM 与 Kysely 集成指南:用 Kyselify 把 Drizzle 表定义无缝接入 Kysely 查询构建器

【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm

本篇技术指南基于当前仓库(drizzle-orm v0.45.3)中 drizzle-orm/src/kysely/README.md 编写,讲解如何在同一项目中同时使用 Drizzle 定义数据库 Schema、生成自动化迁移,并用 Kysely 作为日常查询构建器。读完本文,你将掌握Kyselify类型工具的核心用法、它如何把 Drizzle 表结构映射为 Kysely 的ColumnType三态类型(select / insert / update),以及如何从仓库源码与类型测试中验证映射结果的正确性。

为什么要把 Drizzle 和 Kysely 组合使用

Drizzle 与 Kysely 是两种定位不同的 TypeScript 数据库工具。本文所讲的集成方式,核心思路是"各取所长":

  • 用 Drizzle 负责 Schema 定义与迁移:通过pgTablemysqlTable等 API 声明表结构,配合 drizzle-kit 生成并执行 SQL 迁移,保证数据库结构与代码定义始终一致;
  • 用 Kysely 负责查询构建:如果你已有 Kysely 项目,或者更习惯 Kysely 的类型化链式查询 API(如selectFrom(...).selectAll()),可以继续用 Kysely 写查询。

这种组合特别适合"已有 Kysely 项目、希望引入 Drizzle 的表定义与自动化迁移能力"的存量场景。它让 Drizzle 的 Schema 定义和自动化迁移成为项目事实的唯一来源(single source of truth),而查询层保留你熟悉的 Kysely 写法。

快速上手:最小可用示例

以下示例完整引自关联文档,展示了 Drizzle 定义表 + Kysely 执行查询的完整链路:

import { Kysely, PostgresDialect } from 'kysely'; import { Pool } from 'pg'; import { Kyselify } from 'drizzle-orm/kysely'; import { pgTable, serial, text } from 'drizzle-orm/pg-core'; const test = pgTable('test', { id: serial('id').primaryKey(), name: text('name').notNull(), }); interface Database { test: Kyselify<typeof test>; } const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool(), }), }); const result/*: { id: number, name: string }[] */ = db.selectFrom('test').selectAll().execute();

关键步骤拆解:

  1. 用 Drizzle 声明表pgTable('test', {...})定义表结构与列类型,serial(...).primaryKey()text(...).notNull()同时携带 DDL 约束信息;
  2. Kyselify桥接类型Kyselify<typeof test>把 Drizzle 表类型转换为 Kysely 数据库接口(Databaseinterface)中可识别的表类型;
  3. 构造 Kysely 实例new Kysely<Database>({...})按 Kysely 惯用法配置方言与连接池(此处为 PostgreSQL 的pg.Pool);
  4. 执行类型安全的查询selectFrom('test').selectAll()返回的结果数组类型即为 Drizzle 表推断出的 select 行类型({ id: number, name: string }[])。

Kyselify 类型工具的实现原理

Kyselify是本次集成的核心,其完整实现位于 drizzle-orm/src/kysely/index.ts。它并非运行时工具,而是一个纯类型层面的映射器,源码总共只有一个导出类型:

export type Kyselify<T extends Table> = Simplify< { [Key in keyof T['_']['columns'] & string as MapColumnName<Key, T['_']['columns'][Key], true>]: ColumnType< // select InferSelectModel<T, { dbColumnNames: true }>[MapColumnName<Key, T['_']['columns'][Key], true>], // insert MapColumnName<Key, T['_']['columns'][Key], true> extends keyof InferInsertModel< T, { dbColumnNames: true } > ? InferInsertModel<T, { dbColumnNames: true }>[MapColumnName<Key, T['_']['columns'][Key], true>] : never, // update MapColumnName<Key, T['_']['columns'][Key], true> extends keyof InferInsertModel< T, { dbColumnNames: true } > ? InferInsertModel<T, { dbColumnNames: true }>[MapColumnName<Key, T['_']['columns'][Key], true>] : never >; } >;

从源码结构看,它做了三件事:

  • 遍历列集合keyof T['_']['columns']取出 Drizzle 表内部的列配置(即Table类中_品牌类型所携带的columns,见 drizzle-orm/src/table.ts);
  • 映射列名:通过MapColumnName<Key, T['_']['columns'][Key], true>将 Drizzle 的列属性键(如fileName)转换为实际的数据库列名(如file_name)。第三个泛型参数固定为true,表示始终使用dbColumnNames模式,即优先取列定义中的数据库存储名(TColumn['_']['name'],参见 drizzle-orm/src/table.ts);
  • 生成 Kysely 的 ColumnType 三态:Kysely 的ColumnType<SelectType, InsertType, UpdateType>区分"查询返回类型 / 插入类型 / 更新类型"。其中 select 类型来自InferSelectModel<T, { dbColumnNames: true }>,insert 与 update 类型来自InferInsertModel,且当某列不出现在 insert 模型(例如带默认值或自增的列)时映射为never

select / insert / update 三态映射的意义

InferInsertModel基于RequiredKeyOnly/OptionalKeyOnly等工具类型区分必填列与可选列(见 drizzle-orm/src/table.ts)。映射到 Kysely 后:

  • insert 为never的列,意味着 Kysely 在insertInto(...).values(...)时不会要求你提供该值——这正是serial自增主键、defaultNow()等带默认值列的行为;
  • update 类型同样取 insert 模型,保证更新时不会把自增/默认列当作可写字段。

因此,Kysely 的写入约束与 Drizzle 表定义中的默认值、自增、notNull 等语义天然对齐。

列名映射:camelCase 属性名与 snake_case 数据库列名

Kysely 的数据库接口默认以数据库列名作为键,而 Drizzle 允许你使用任意 TS 属性名对应到不同数据库列名。Kyselify通过MapColumnName<..., true>保证了这种差异在桥接后依然成立。

仓库中的类型测试 drizzle-orm/type-tests/kysely/index.ts 给出了真实场景:一个uploads表使用 camelCase 属性名(fileNamefileSize)映射到 snake_case 数据库列名(file_namefile_size),在 Kysely 侧插入时必须以数据库列名书写:

const uploads = pgTable('uploads', { id: varchar('id', { length: 100 }).primaryKey(), state: uploadStateEnum('state').notNull().default('uploading'), type: uploadTypeEnum('type').notNull(), fileName: varchar('file_name', { length: 100 }).notNull(), fileType: varchar('file_type', { length: 100 }).notNull(), fileSize: integer('file_size').notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), uploadedAt: timestamp('uploaded_at'), }); interface Database { uploads: Kyselify<typeof uploads>; } await db .insertInto('uploads') .values({ id: '1', file_name: 'fileName', file_type: 'contentType', type: 'image', file_size: 1, }) .returning('id') .executeTakeFirst();

注意state列带default('uploading'),插入时被Kyselify映射为可省略字段,所以上例未提供;而uploadedAt(数据库列uploaded_at)可为空,同样无需提供。

进阶场景:PostgreSQL 枚举与 MySQL 表

Kyselify作用于任意符合Table约束的 Drizzle 表,因此不限于pg-core,也支持mysql-core,并且能正确处理 Drizzle 的 PostgreSQL 枚举类型。仓库类型测试 drizzle-orm/type-tests/kysely/index.ts 中的pgEnum示例表明:

const uploadStateEnum = pgEnum('upload_state', ['uploading', 'uploaded', 'failed']); const uploadTypeEnum = pgEnum('upload_type', ['image', 'video']); // 表内枚举列: state: uploadStateEnum('state').notNull().default('uploading'), type: uploadTypeEnum('type').notNull(),

Kyselify映射后,Kysely 侧的state/type字段类型即为枚举字面量联合('uploading' | 'uploaded' | 'failed'等),写入时同样受枚举值约束。

MySQL 场景同样有完整测试佐证(drizzle-orm/type-tests/kysely/index.ts),包括char定长字符串、varchar、带defaultNow()onUpdateNow()的时间戳列:

const units = mysqlTable('units', { id: char('id', { length: 16 }).primaryKey(), name: mysqlVarchar('name', { length: 255 }).notNull(), abbreviation: mysqlVarchar('abbreviation', { length: 10 }).notNull(), created_at: mysqlTimestamp('created_at').defaultNow().notNull(), updated_at: mysqlTimestamp('updated_at').defaultNow().notNull().onUpdateNow(), }); interface Database { units: Kyselify<UnitModel>; } await db .insertInto('units') .values({ id: 'my-unique-id', abbreviation: 'foo', name: 'bar', }) .execute();

带默认值的created_at/updated_at被映射为可省略字段,插入语句无需提供。

用类型测试验证映射结果与 Drizzle 推断一致

仓库不仅提供了功能示例,还通过类型断言确保桥接后类型与 Drizzle 原生推断完全等价。见 drizzle-orm/type-tests/kysely/index.ts:

const result = db.selectFrom('test').selectAll().execute(); Expect<Equal<PromiseOf<typeof result>, typeof test.$inferSelect[]>>();

它用Expect<Equal<...>>断言:Kysely 查询返回的Promise解包后的元素类型,与test.$inferSelect(Drizzle 表自带的 select 行推断类型)逐一相等。这从类型层面证明了Kyselify的映射没有丢失或改变任何列的类型信息。

安装与依赖说明

kysely在 drizzle-orm/package.json 中作为可选 peer dependency声明("kysely": "*",且peerDependenciesMeta.kysely.optional = true),因此:

  • 只有真正使用drizzle-orm/kysely子路径时,才需要安装kysely包;
  • 仓库开发环境使用的 Kysely 版本为^0.25.0(devDependencies),类型测试基于该版本编写,建议你的项目选用兼容版本;
  • 导入路径为drizzle-orm/kysely(如文档示例中的import { Kyselify } from 'drizzle-orm/kysely'),它是独立子路径导出,不会影响未使用此特性的项目体积。

在项目中使用时,直接安装所需驱动即可,例如 PostgreSQL 场景需要kyselypg@types/pg

适用边界与注意事项

  • Kyselify是纯类型工具:它只负责类型桥接,不参与任何运行时行为;实际的 SQL 生成与执行完全由 Kysely 及其方言(如PostgresDialect)负责,因此查询性能、方言能力取决于 Kysely 本身;
  • 数据库列名语义:由于Kyselify固定以数据库列名(dbColumnNames模式)组织类型键,Kysely 查询中一律使用 snake_case 数据库列名,与 Drizzle 侧使用 camelCase 属性名不冲突但需注意区分;
  • Schema 演进:表结构变更后,Drizzle 表类型、Kyselify桥接类型与 Kysely 查询会同步获得类型反馈,编译期即可发现字段增删导致的查询失效,这也是本方案相对纯 Kysely 手写类型的主要收益之一。

延伸阅读

  • 集成文档原文:本指南的源头文档;
  • Kyselify 源码实现:类型映射的完整定义;
  • Kysely 类型测试:覆盖 PostgreSQL、MySQL、枚举与列名映射的可运行示例;
  • Drizzle 表与推断类型基础:理解InferSelectModelInferInsertModelMapColumnName等底层类型工具;
  • drizzle-orm 包配置:查看kyselypeer dependency 声明与版本约束。

【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询