ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MikroORM 与 Kysely 深度集成指南:类型安全 SQL 查询构建器实战

MikroORM 与 Kysely 深度集成指南:类型安全 SQL 查询构建器实战 后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载MikroORM 为 Kysely 展开结合仓库源码SqlEntityManager.ts、plugin/index.ts、plugin/transformer.ts与测试用例带你掌握getKysely()的完整用法、事务绑定语义、五种插件选项以及从零到一的类型安全配置方案。为什么需要 Kysely 集成MikroORM 的 QueryBuilder 已经覆盖了绝大多数 CRUD 与复杂查询场景但当你需要书写更贴近 SQL 的低层语句、复用既有的 Kysely 查询逻辑或者希望绕过 ORM 抽象直接操作表时Kysely 是一个理想的补充。这套集成允许你写出低层 SQL 查询同时保持类型安全复用 MikroORM 中定义的实体关系与钩子兼容全部实体定义风格——装饰器、EntitySchema与defineEntity。从源码结构看这套能力被封装在mikro-orm/sql包内所有 SQL 驱动PostgreSQL、MySQL、MariaDB、SQLite、libSQL、MSSQL、Oracle 等通过SqlEntityManager共享同一实现。获取 Kysely 实例最基础的用法是通过em.getKysely()直接获取实例const kysely orm.em.getKysely();默认情况下getKysely()返回的是未挂载 MikroORM 插件的裸 Kysely 实例。要启用 MikroORM 感知的特性——实体/属性名映射、钩子处理、值转换——需要传入配置对象const kysely orm.em.getKysely({ tableNamingStrategy: entity, columnNamingStrategy: property, processOnCreateHooks: true, processOnUpdateHooks: true, convertValues: true, });从 SqlEntityManager.ts 的实现可以看到getKysely()的关键逻辑getKyselyTDB undefined, TOptions extends GetKyselyOptions GetKyselyOptions( options: TOptions {} as TOptions, ): Kysely... { const context this.getContext(false); const ctx context.getTransactionContextKyselyany(); let kysely: Kyselyany ctx ?? this.getConnection(options.type).getClient(); if ( options.columnNamingStrategy ! null || options.tableNamingStrategy ! null || options.processOnCreateHooks ! null || options.processOnUpdateHooks ! null || options.convertValues ! null ) { kysely kysely.withPlugin(new MikroKyselyPlugin(this, options)); } return kysely; }两个关键点连接来源优先使用当前事务上下文中的 Kysely 客户端ctx否则回退到getConnection(options.type).getClient()插件按需挂载只有当五个插件选项中有任意一个被显式设置时才会调用withPlugin(new MikroKyselyPlugin(this, options))——因此零配置调用不会产生任何转换开销这是一个刻意设计的性能细节。GetKyselyOptions在type之外扩展了MikroKyselyPluginOptions其中type?: ConnectionType表示连接类型read/write完整定义见 SqlEntityManager.ts 与 plugin/index.ts。事务上下文自动绑定与回滚getKysely()会自动使用 EntityManager 当前的事务上下文。当在em.transactional(...)内部或em.begin()之后调用时返回的 Kysely 实例会绑定到活动事务上任何通过 Kysely 自身的.execute()/.executeTakeFirst*()执行的查询都会参与该事务并随之一同回滚await orm.em.transactional(async em { const kysely em.getKysely({ tableNamingStrategy: entity, convertValues: true }); // 在事务内执行 await kysely.insertInto(Doc).values({ title: t, content: hello }).execute(); throw new Error(boom); // 上面的 insert 会随之回滚 });这条语义在源码中直接体现getKysely()首先查询context.getTransactionContextKyselyany()事务存在时优先使用事务绑定的客户端。若你需要一个不绑定当前事务的 Kysely 实例——例如在事务块内从连接池读取数据——先 fork 一个 EntityManager。被 fork 的 EM 没有事务上下文getKysely()会回退到连接池客户端await orm.em.transactional(async em { // 绑定当前事务 await em.getKysely().selectFrom(user).selectAll().execute(); // 绑定连接池在事务外运行 await em.fork().getKysely().selectFrom(audit_log).selectAll().execute(); });type选项read/write仅在事务外生效——事务内连接已被固定该选项会被忽略。测试用例 get-kysely-transaction-context.test.ts 验证了事务内写入的提交与回滚行为事务抛错后通过 fork 出的 EM 查询不到已回滚的数据事务提交时数据则持久可见。使用实体名与属性名书写查询插件最有价值的能力之一是让你用实体名和属性名书写 Kysely 查询而不是裸表名和裸列名。这一能力与实体定义方式无关——装饰器、EntitySchema、defineEntity均支持Entity() class UserProfile { [EntityName]?: UserProfile; PrimaryKey() id!: number; Property() firstName!: string; // 映射到 first_name 列 Property() lastName!: string; // 映射到 last_name 列 } const kysely orm.em.getKysely({ tableNamingStrategy: entity, columnNamingStrategy: property, }); const users await kysely .selectFrom(UserProfile) // 实体名而非表名 .select([firstName, lastName]) // 属性名而非列名 .where(firstName, , John) .execute(); // 生成的 SQL: select first_name, last_name from user_profile where first_name ? // 结果自动映射回属性名 console.log(users[0].firstName); // John插件在查询编译阶段将实体名翻译为表名、属性名翻译为列名因此你写出的查询与 TypeScript 代码保持一致而非与数据库 Schema 绑定。这一转换发生在 MikroTransformer 中。其核心机制值得了解transformIdentifier在tableNamingStrategy entity且父节点为SchemableIdentifierNode时通过findEntityMetadata找到元数据并把实体名替换为meta.tableName在columnNamingStrategy property时则把属性名替换为prop.fieldNames[0]见 transformer.tsfindEntityMetadata同时支持按类名和按表名查找元数据见 transformer.ts查询节点通过上下文栈context stack维护作用域内的表/别名到元数据的映射支持子查询、CTEWITH子句与关联子查询的列解析见 transformer.ts结果行转换时transformResult会建立“数据库字段名 → 属性对象”的全局映射并把外键列名迁移到关联属性名上见 transformer.ts。类型安全四种配置路径1.defineEntity的自动推断使用defineEntity定义实体时getKysely()会自动从实体元数据推断完整数据库类型——表名、列类型、可空性、关系全部源自属性定义import { MikroORM, defineEntity, p } from mikro-orm/core; const User defineEntity({ name: User, tableName: users, properties: { name: p.string().primary(), email: p.string().nullable(), }, }); const orm new MikroORM({ dbName: :memory:, entities: [User], }); // getKysely() 自动推断表结构 const kysely orm.em.getKysely(); // 完全类型安全TypeScript 自动补全 users 表及其列 const result await kysely .selectFrom(users) .selectAll() .where(email, is not, null) .execute();类型层面的实现位于 typings.tsInferKyselyDB将实体元数据映射为 Kysely 的数据库类型MapTableName、MapValueAsTable、InferKyselyTable等类型工具共同完成fieldName自定义列名、nullable、autoincrement、default、onCreate以及m:1/1:1外键列的table_column_id命名都会被精确推导见 typings.ts。测试 get-kysely.test.ts 对此做了严格验证例如lastName: p.string().fieldName(the_last_name)被推断为the_last_name: stringISBN被蛇形化为isbnuser2FAEnabled被推断为user2faenabledpost.authorm:1被推断为author_full_name: stringid自增主键被推断为Generatednumber。2. 装饰器实体的自动推断对于装饰器实体通过在类中声明[EntityName]symbol 属性即可启用自动类型推断。它让getKysely()在类型层面获知实体名从而从类属性推断表与列的类型import { Entity, PrimaryKey, Property } from mikro-orm/decorators/legacy; import { EntityName, MikroORM } from mikro-orm/sqlite; Entity() class UserProfile { [EntityName]?: UserProfile; PrimaryKey() id!: number; Property() firstName!: string; Property() lastName!: string; } const orm await MikroORM.init({ dbName: :memory:, // 类型推断要求显式传入实体类 entities: [UserProfile], }); // getKysely() 从类属性推断表结构 const kysely orm.em.getKysely({ columnNamingStrategy: property, }); // TypeScript 已知 user_profile 表及 id、firstName、lastName 列 const result await kysely .selectFrom(user_profile) .select([id, firstName, lastName]) .execute();[EntityName]symbol 属性是必需的——没有它getKysely()无法从类引用中推断出字符串字面量类型的实体名。这与[EntityRepositoryType]为自定义仓储类型提供类型信息的方式类似。该 symbol 定义于 core/src/typings.ts并配有InferEntityName提取类型。几个细节约束需要注意列类型直接从类实例属性推断表名遵循已配置的命名策略默认下划线风格使用tableNamingStrategy: entity时查询中直接使用实体名如UserProfile实体类必须显式传入entities数组而非文件夹路径才能启用自动类型推断。该方式直接使用类属性类型不支持精细的列名映射如自定义fieldName——需要这种控制力时请使用defineEntity。3. 手动类型声明对于装饰器或EntitySchema实体你也可以向getKysely()传入手写的数据库类型获得完整类型安全// 定义与实体匹配的数据库类型 interface Database { user_profile: { id: number; first_name: string; last_name: string; }; post: { id: number; title: string; author_id: number; }; } // 将类型传给 getKysely() 以获得完整类型安全 const kysely orm.em.getKyselyDatabase(); const user await kysely .selectFrom(user_profile) .select([id, first_name]) .executeTakeFirst();使用columnNamingStrategy: property时接口改用属性名定义interface Database { user_profile: { id: number; firstName: string; lastName: string; }; } const kysely orm.em.getKyselyDatabase({ columnNamingStrategy: property, });4. 混合推断类型与手动类型你可以将InferKyselyTable用于defineEntity实体与手动类型声明用于其他表或视图组合使用import { InferKyselyTable } from mikro-orm/postgresql; const pluginOptions { tableNamingStrategy: entity, convertValues: true, } as const; // 为没有 defineEntity 定义的数据库视图或表手写类型 interface ViewStatsTable { view_id: number; view_count: number; } interface Database { User: InferKyselyTabletypeof User, typeof pluginOptions; Post: InferKyselyTabletypeof Post, typeof pluginOptions; view_stats: ViewStatsTable; } const kysely orm.em.getKyselyDatabase(pluginOptions); const user await kysely.selectFrom(User).selectAll().executeTakeFirst(); const stats await kysely.selectFrom(view_stats).selectAll().executeTakeFirst();测试 mikro-kysely-plugin.test.ts 展示了同样的组合模式interface PersonTable extends InferKyselyTabletypeof Person, typeof options {}并将 SQLite 与 PostgreSQL 两个驱动实例化后交叉验证编译结果。使用 CLI 生成实体导出discovery:export当项目实体很多时在 ORM 配置中手工维护entities数组既繁琐又容易出错。discovery:exportCLI 命令会扫描实体源文件并生成一个 TypeScript barrel 文件导出entities数组——供 ORM 配置使用Database类型——实体元组可用于任何接受实体元组的场景如MikroORMDriver, EM, DatabaseEntityManager类型——驱动固定、实体感知的EntityManager别名适用于 DI 场景。npx mikro-orm discovery:export命令从 ORM 配置entitiesTs或entities中读取实体路径发现所有实体类与 Schema生成类似下面的文件// This file was generated by MikroORM CLI. Do not edit manually. // Re-run mikro-orm discovery:export to update. import { Author } from ./entities/Author.js; import { BookSchema } from ./entities/Book.js; import type { EntityManager as DriverEntityManager } from mikro-orm/postgresql; export const entities [ Author, BookSchema, ] as const; export type Database typeof entities; export type EntityManager DriverEntityManager { ~entities: Database };在 ORM 配置中使用生成的entities数组import { entities } from ./entities.generated; export default defineConfig({ entities });在 DI/框架场景如 NestJS中EntityManager 的实体元组通常会被擦除此时应导入生成的EntityManager。生成文件同时以类型通过幻影~entities挂载携带实体元组和const重导出驱动的实际 EM 类两种形态导出它——因此它既能充当 Nest 的 DI token也能作为参数的注解类型。驱动已由代码生成固定无需填充驱动泛型em.getKysely(opts)对数据库形状与调用点插件选项都保留完整类型推断import { EntityManager } from ./entities.generated; Injectable() export class ArticleService { constructor(private readonly em: EntityManager) {} list() { // 物理名/下划线名 return this.em.getKysely().selectFrom(article).selectAll().execute(); } listByEntityName() { // 实体名表 属性名列——根据运行时选项推断 return this.em .getKysely({ tableNamingStrategy: entity, columnNamingStrategy: property }) .selectFrom(Article) .selectAll() .execute(); } }从 DiscoveryExportCommand.ts 的实现可以看到entities数组、Database typeof entities、EntityManager的类型/值双导出DiscoveryExportCommand.ts都是按上述机制生成的驱动包通过driverPackageMap从驱动类名映射如PostgreSqlDriver→mikro-orm/postgresql见 DiscoveryExportCommand.ts。自定义EntityManager子类生成的EntityManager重导出的是驱动的原生 EM。如果你扩展了 EM参考 Extending EntityManager并希望子类也获得实体感知的类型请保持生成文件原样不动另写一个紧邻的小包装把生成的Database元组嫁接到你自己的类上import { entities, type Database } from ./entities.generated.js; import { MyEntityManager } from ./MyEntityManager.js; export { entities }; export type EntityManager MyEntityManager { ~entities: Database }; export const EntityManager MyEntityManager;随后在 DI 绑定与服务中从./custom-em导入EntityManager而不是./entities.generated。生成文件保持纯粹的机械产出这样重新运行discovery:export永远不会覆盖你的自定义 EM 胶水代码且em.getKysely(opts)通过子类保留完整类型推断。命令选项Flag类型说明-p, --pathstring[]实体源文件的 Glob 模式-o, --outstring输出文件路径默认紧邻 ORM 配置-d, --dumpboolean打印到 stdout 而非写文件未提供--path时命令从配置的entitiesTs优先或entities数组中提取文件夹/Glob 路径见 DiscoveryExportCommand.ts。一旦配置切换到生成的 barrel 文件其中是实体引用而非路径再次运行命令就需要显式传入--pathnpx mikro-orm discovery:export --path ./src/entities/*.ts插件选项详解当你向getKysely()传入配置对象时返回的实例会包含MikroKyselyPlugin。该插件拦截 Kysely 的查询编译与结果处理以支持 MikroORM 特有的功能plugin/index.ts。以下五个选项的默认值与语义定义见 plugin/index.ts。tableNamingStrategy控制你在 Kysely 查询中如何引用表。table默认使用数据库中的实际表名如user_profiles即 Kysely 的标准行为entity使用实体名如UserProfile插件在生成 SQL 前将其转换为对应表名。// 假设实体名为 User数据库表名为 users // 默认tableNamingStrategy: table await kysely.selectFrom(users).selectAll().execute(); // 实体名策略tableNamingStrategy: entity await kysely.selectFrom(User).selectAll().execute(); // 生成的 SQL: select * from userscolumnNamingStrategy控制查询中如何引用列以及结果如何映射。column默认使用数据库中的实际列名如first_nameproperty使用实体属性名如firstName。插件生成 SQL 时将属性名转换为列名并在返回结果中将列名映射回属性名。const kysely orm.em.getKysely({ columnNamingStrategy: property }); const users await kysely .selectFrom(user) .select([firstName, lastName]) // 属性名 .where(firstName, , John) .execute(); // 生成的 SQL: select first_name, last_name from user where first_name ? // 结果自动映射回属性名 console.log(users[0].firstName); // John在结果转换层面transformResult仅当columnNamingStrategy property或convertValues开启时才会执行plugin/index.ts并且通过#queryNodeCache以 queryId 为键的 WeakMap缓存查询节点与实体映射供结果阶段复用。processOnCreateHooks布尔值默认false。开启后INSERT查询会自动处理实体属性上定义的onCreate钩子。如果你的插入数据缺少配置了onCreate的属性如createdAt插件会自动生成并追加到查询中// 实体属性上的 onCreate 钩子 // Property({ onCreate: () new Date() }) // createdAt!: Date; const kysely orm.em.getKysely({ processOnCreateHooks: true }); // 未提供 createdAt——它会被自动补充 await kysely.insertInto(user).values({ name: John }).execute(); // 生成的 SQL 自动包含 created_at // insert into user (name, created_at) values (?, ?)实现上processOnCreateHooks会找出元数据中prop.onCreate存在且未出现在插入列中的属性将prop.onCreate!(undefined, this.#em)的返回值追加为新列与新值见 transformer.ts。测试 mikro-kysely-plugin.test.ts 验证了createdAt、updatedAt、children三个钩子属性被自动填充显式提供的值如children: 5不会被钩子覆盖mikro-kysely-plugin.test.ts显式传null也会被尊重mikro-kysely-plugin.test.ts。processOnUpdateHooks布尔值默认false。开启后UPDATE查询会自动处理实体属性上定义的onUpdate钩子。例如自动更新updatedAt时间戳字段// 实体属性上的 onUpdate 钩子 // Property({ onUpdate: () new Date() }) // updatedAt!: Date; const kysely orm.em.getKysely({ processOnUpdateHooks: true }); await kysely .updateTable(user) .set({ name: Johnny }) .where(id, , 1) .execute(); // 生成的 SQL 自动包含 updated_at // update user set name ?, updated_at ? where id ?对应的processOnUpdateHooks实现会把缺失的onUpdate属性追加为ColumnUpdateNode见 transformer.ts。值得注意的是INSERT ... ON CONFLICT DO UPDATE语句中的冲突更新子句同样会经过该钩子处理transformer.ts。convertValues布尔值默认false。开启后插件使用 MikroORM 的类型系统转换查询参数与结果值。这对于处理驱动特有类型如 SQLite 中以数字/字符串存储的 Date或自定义类型至关重要。输入转换将 JavaScript 对象如Date转换为数据库支持的格式输出转换将数据库返回的原始值转换回 JavaScript 对象或自定义类型。const kysely orm.em.getKysely({ convertValues: true }); // 1. 输入转换Date 对象被自动处理 await kysely .insertInto(user) .values({ name: John, bornAt: new Date(1990-01-01) // 自动转换为数据库格式 }) .execute(); // 2. 输出转换读取时自动转换回 Date 对象 const user await kysely .selectFrom(user) .selectAll() .executeTakeFirst(); console.log(user.bornAt instanceof Date); // trueconvertValues的实现覆盖了查询的多个层面输入值prepareInputValue会调用prop.customType.convertToDatabaseValue(...)转换自定义类型Date则走platform.processDateProperty(value)见 transformer.tsWHERE 条件值transformBinaryOperation会解析比较操作左侧列对应的实体属性并转换右侧的操作数见 transformer.tsSELECT 列展开当列的自定义类型定义了convertToJSValueSQL时convertValues还会在编译期把普通选择展开为带 SQL 包装表达式的选择expandStar/wrapRead见 transformer.ts例如测试 get-kysely-transaction-context.test.ts 中的HexEncodedType会生成hex(...)/unhex(...)包装输出值prepareOutputValue与EntityComparator.getResultMapper对齐对customType、布尔值!!转换、Date含时区处理见 transformer.ts做规范化。插件机制与查询变换原理理解MikroKyselyPlugin的协作流程有助于排查复杂查询问题transformQuery每次查询编译时先reset()清空上下文然后调用MikroTransformer.transformNode遍历 Kysely 的 AST。对于 SELECT/INSERT/UPDATE/DELETE 根节点会把本次查询涉及的全部实体元数据拷贝进以 queryId 为键的 WeakMap 缓存plugin/index.ts变换过程中MikroTransformer通过上下文栈追踪每个查询作用域支持嵌套子查询、CTE、关联子查询通过子查询别名映射把子查询/CTE 别名解析回源表元数据从而正确处理带别名的列引用transformer.tstransformResult仅当columnNamingStrategy property或convertValues开启时用缓存的实体映射把结果行的列名映射回属性名并做值转换plugin/index.ts。适用前提与限制该集成属于mikro-orm/sql提供的 SQL 驱动能力MongoDB 驱动不适用装饰器实体的自动类型推断要求实体类显式出现在entities数组中且不支持自定义fieldName的精细列名映射——这类需求请改用defineEntity或手动类型声明type读/写连接选项仅在事务外生效结果转换依赖编译期缓存的实体映射对纯裸表无对应实体元数据的查询transformResult会原样返回行transformer.ts。延伸阅读Kysely 集成官方文档本文的文档源头SqlEntityManager 源码getKysely()与execute()的实现MikroKyselyPlugin 与 MikroTransformer查询变换与结果映射的底层实现类型推断工具InferKyselyDB、InferKyselyTable、InferClassEntityDB等类型工具discovery:export 命令barrel 文件生成逻辑测试参考get-kysely.test.ts、get-kysely-transaction-context.test.ts、mikro-kysely-plugin.test.ts。赞分享后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐MikroORM 与 Kysely 深度集成指南从 em.getKysely() 到类型安全 SQL 查询MikroORM 与 Kysely 深度集成指南从 em.getKysely 到类型安全 SQL 查询 本指南基于 MikroORM 7.x 版本系统讲解如后端MikroORM 与 Kysely 深度集成在 EntityManager 中获取类型安全的 SQL 查询构建器MikroORM 与 Kysely 深度集成在 EntityManager 中获取类型安全的 SQL 查询构建器 导读 MikroORM 提供了与 Kysel后端MikroORM 7.2 与 Kysely 深度集成指南从 em.getKysely() 到类型安全的底层 SQL 查询MikroORM 7.2 与 Kysely 深度集成指南从 em.getKysely 到类型安全的底层 SQL 查询 本篇指南完整讲解 MikroORM 对后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表