创新方案
在 TypeScript 中用 Kysely 访问 SQL…
适用于任何需要在 TypeScript 项目中以类型安全、可自动补全的方式构建 SQL 查询和迁移的开发者。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 TypeScript 中用 Kyse通用
解决思路
- 按 Kysely 核心原则开发:把它当作 SQL 的薄抽象层,显式构建查询以保持可预测的 1:1 SQL 编译;利用表、列、结果集的完整类型安全与自动补全;不依赖魔法或隐藏行为;确保代码可运行于 Node.js、Deno、Bun、Cloudflare Workers 和浏览器。
- 定义数据库 schema:用 interface 定义每张表的行类型,例如 UserTable 和 PostTable;定义 Database interface 将表名映射到表接口;用 Selectable<UserTable>、Insertable<UserTable>、Updateable<UserTable> 导出 User、NewUser、UserUpdate 等辅助类型。
- 区分 Generated<T> 与 ColumnType:用 Generated<T> 标注数据库自动生成的列(如自增、默认值);用 ColumnType<SelectType, InsertType, UpdateType> 标注在不同操作中类型不同的列。
- PostgreSQL 连接:import { Kysely, PostgresDialect } from 'kysely';import { Pool } from 'pg';创建 new Kysely<Database>,配置 dialect 为 new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),并 export db。
- MySQL 连接:import { Kysely, MysqlDialect } from 'kysely';import { createPool } from 'mysql2';创建 new Kysely<Database>,配置 dialect 为 new MysqlDialect({ pool: createPool({ uri: process.env.DATABASE_URL }) })。
- SQLite 连接:import { Kysely, SqliteDialect } from 'kysely';import Database from 'better-sqlite3';创建 new Kysely<Database>,配置 dialect 为 new SqliteDialect({ database: new Database('database.db') })。
- Select 查询:用 db.selectFrom('users').selectAll().execute() 查询所有列;用 .select(['id', 'email', 'name']).execute() 查询指定列;用 .where('is_active', '=', true).execute() 加 WHERE 条件;用多个 .where 如 .where('is_active', '=', true).where('email', 'like', '%@example.com') 表示 AND;用 .where((eb) => eb.or([eb('name', '=', 'John'), eb('name', '=', 'Jane')])).execute() 表示 OR 条件。
- 列别名:在 select 数组中使用如 'name as userName' 的别名,Kysely 会自动解析并推断别名类型;executeTakeFirst() 后可直接使用 result.userName。
- Joins:用 .innerJoin('users', 'users.id', 'posts.author_id') 做内连接并选择 posts.id、posts.title、users.name as authorName;用 .leftJoin('posts', 'posts.author_id', 'users.id') 做左连接并选择 users.id、users.name、posts.title as postTitle。
- 子查询:在 select 中用回调 (eb) => eb.selectFrom('posts').select(eb.fn.count<number>('posts.id').as('count')).whereRef('posts.author_id', '=', 'users.id').as('postCount');在 where 中用 .where('id', 'in', (eb) => eb.selectFrom('posts').select('author_id').distinct())。
- Insert 操作:单条插入用 .insertInto('users').values({ email: 'user@example.com', name: 'John Doe', is_active: true }).returning(['id', 'email', 'created_at']).executeTakeFirstOrThrow();批量插入用 .values([{ email: 'user1@example.com', name: 'User 1', is_active: true }, { email: 'user2@example.com', name: 'User 2', is_active: true }]).execute();upsert 用 .onConflict((oc) => oc.column('email').doUpdateSet({ name: 'John Updated', updated_at: new Date() })).execute()。
- Update 操作:用 db.updateTable('users').set({ name: 'Jane Doe', updated_at: new Date() }).where('id', '=', 1).returning(['id', 'name', 'updated_at']).executeTakeFirst()。
- Delete 操作:用 db.deleteFrom('users').where('id', '=', 1).returning(['id', 'email']).executeTakeFirst()。
- 事务:用 db.transaction().execute(async (trx) => { ... });在事务内用 trx.insertInto('users')...returning(['id']).executeTakeFirstOrThrow() 获取用户 id,再用 trx.insertInto('posts')... 插入关联数据。
- 用 kysely-codegen 生成类型:执行 npm install -D kysely-codegen;执行 npx kysely-codegen(从 DATABASE_URL 环境变量读取);执行 npx kysely-codegen --out-file src/db/types.ts 指定输出文件;数据库 schema 变化后重新生成类型。
- 使用 CamelCase 插件:在 new Kysely<Database> 配置 plugins: [new CamelCasePlugin()],把 snake_case 列名转换为 camelCase。
- 编写自定义插件:实现 KyselyPlugin,在 transformQuery(args) 中记录 args.node 并返回 args.node;在 transformResult(args) 中记录 args.result 并返回 args.result。
- 动态查询构建:从 let query = db.selectFrom('users').selectAll() 开始;根据 filters.email 追加 .where('email', '=', filters.email);根据 filters.isActive !== undefined 追加 .where('is_active', '=', filters.isActive);根据 filters.name 追加 .where('name', 'like', `%${filters.name}%`);最后 query.execute()。
- Raw SQL:import { sql } from 'kysely';在 select 中用 sql<string>`CONCAT(first_name, ' ', last_name)`.as('fullName');在 where 中用 .where(sql`LOWER(email)`, '=', 'user@example.com')。
- CTE:用 db.with('active_users', (db) => db.selectFrom('users').selectAll().where('is_active', '=', true)).selectFrom('active_users').selectAll().execute() 定义并使用公共表表达式。
- TypeScript 配置:在 tsconfig.json 中启用 strict、strictNullChecks,target 使用 ES2020;使用 TypeScript 5.4 或更高版本以获得最佳类型推断。
- 性能:只选择需要的列,避免仅需要特定字段时使用 selectAll()。
- 性能:为 WHERE 和 JOIN 中的列确保存在数据库索引。
- 性能:批量操作使用 bulk inserts 插入多条记录。
- 性能:生产环境总是使用连接池。
- 性能:Kysely 会自动使用 prepared statements,无需手动处理。
- 错误处理:import { NoResultError } from 'kysely';使用 executeTakeFirstOrThrow() 获取单条结果时,用 try/catch 捕获 error instanceof NoResultError 处理未找到,否则重新抛出。
- 查询可组合:创建可复用查询片段函数,例如 withActiveUsers(db: Kysely<Database>) 返回 db.selectFrom('users').where('is_active', '=', true);在查询中使用 withActiveUsers(db).selectAll().execute()。
- 迁移管理:import { Migrator, FileMigrationProvider } from 'kysely';import path from 'path';import { promises as fs } from 'fs';创建 new Migrator({ db, provider: new FileMigrationProvider({ fs, path, migrationFolder: path.join(__dirname, 'migrations') }) });执行 await migrator.migrateToLatest()。
- 编写迁移文件:在 up(db: Kysely<any>) 中用 db.schema.createTable('users').addColumn('id', 'serial', (col) => col.primaryKey()).addColumn('email', 'varchar(255)', (col) => col.notNull().unique()).addColumn('name', 'varchar(255)').addColumn('is_active', 'boolean', (col) => col.defaultTo(true)).addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql`now()`).notNull()).execute();在 down(db: Kysely<any>) 中用 db.schema.dropTable('users').execute()。
适用边界 · 注意事项
- 需要 ORM 的关系管理、懒加载、模型钩子、级联等高级功能时,Kysely 只是类型安全 SQL 查询构建器,不提供这些抽象。
- 项目不使用 TypeScript,或团队不愿意维护与数据库 schema 对应的类型定义时。
- 必须使用 Kysely 尚未支持且又不接受 sql 模板写原始 SQL 的数据库特有语法时。