ARTICLE DETAIL

资讯详情

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

Kysely 插件系统完全指南:从内置插件到自定义 KyselyPlugin 的实战解析

Kysely 插件系统完全指南:从内置插件到自定义 KyselyPlugin 的实战解析 后端数据库【免费下载链接】kyselyA type-safe TypeScript SQL query builder项目地址https://gitcode.com/gh_mirrors/ky/kysely点击查看免费下载Kysely 是一个类型安全的 TypeScript SQL 查询构建器其插件系统允许你在查询执行前改写查询OperationNode 语法树并在执行后改写结果集从而在保持查询所见即所得的同时实现标识符命名转换、重复 JOIN 去重、空 IN 列表处理与 NULL 安全比较等横切能力。本文将围绕 site/docs/plugins.md 讲解插件系统的核心机制、四个官方内置插件的配置与底层实现并带你基于 src/plugin/kysely-plugin.ts 的接口契约编写自己的插件。读完本文你将能在实际项目中正确选择、配置甚至定制 Kysely 插件彻底解决命名风格不统一、动态查询 JOIN 重复、in ()空列表报错和 NULL 比较语义错误四类典型问题。一、插件系统概述KyselyPlugin 接口与执行时机插件本质上是实现了KyselyPlugin接口的类。该接口在 src/plugin/kysely-plugin.ts 中定义只包含两个方法transformQuery(args: PluginTransformQueryArgs): RootOperationNode在每条查询执行前被调用。你可以通过args.node拿到整棵OperationNode语法树并返回修改后的树。通常配合OperationNodeTransformer子类使用例如SnakeCaseTransformer、SafeNullComparisonTransformer都继承了 src/operation-node/operation-node-transformer.ts。transformResult(args: PluginTransformResultArgs): PromiseQueryResultUnknownRow在每条查询执行后被调用。通过args.result访问查询结果并返回修改后的结果典型用途是 CamelCasePlugin 把返回行的 snake_case 键改写为 camelCase。如果需要在transformQuery与transformResult之间传递与查询相关的数据源码注释明确建议使用WeakMap并以args.queryId作为键。因为transformQuery并不总是被transformResult配对调用使用强引用 Map 会导致孤立条目泄漏内存import type { KyselyPlugin, QueryResult, RootOperationNode, UnknownRow } from kysely const data new WeakMapany, MyData() const plugin { transformQuery(args) { data.set(args.queryId, /* ... */) return args.node }, async transformResult(args) { const something data.get(args.queryId) // ... return args.result }, } satisfies KyselyPluginPluginTransformResultArgs还继承了AbortableOperationOptions意味着结果转换阶段同样支持中止语义见 src/util/abort.ts。另外仓库中提供了一个不执行任何操作的NoopPluginsrc/plugin/noop-plugin.ts它直接原样返回节点与结果可作为编写自定义插件时的最小参考骨架。二、注册插件全局配置与查询级withPlugin插件的注册方式在文档中给出了最基础的形式——通过Kysely实例构造参数中的plugins数组const db new KyselyDatabase({ dialect: new PostgresDialect({ database: kysely_test, host: localhost, }), plugins: [new CamelCasePlugin()], })除全局注册外site/docs/recipes/0008-deduplicate-joins.md 还展示了查询级注册方式通过查询构建器的.withPlugin()方法在个别查询上临时启用插件适合只在特定业务路径需要去重的场景await db .withPlugin(new DeduplicateJoinsPlugin()) .selectFrom(person) .selectAll(person) // ... .executeTakeFirst()两种方式可组合使用全局插件作用于所有查询查询级插件只影响当前链式调用。这在实践中很有用——比如CamelCasePlugin全局启用而DeduplicateJoinsPlugin只在构建动态查询的函数里按需启用。三、CamelCasePlugin数据库 snake_case 与 JS camelCase 的双向桥接CamelCasePlugin 是使用率最高的内置插件它把数据库中的 snake_case 标识符与 JavaScript 侧的 camelCase 相互转换。其核心实现位于 src/plugin/camel-case/camel-case-plugin.tstransformQuery使用SnakeCaseTransformersrc/plugin/camel-case/camel-case-transformer.ts遍历语法树对每个IdentifierNode调用 snake_case 映射把查询中的 camelCase 表名、列名改写为数据库的 snake_casetransformResult遍历返回行把每一行的键通过mapRow递归转换为 camelCase——注意嵌套对象与数组元素也会被递归处理除非配置了maintainNestedObjectKeys。配置选项插件构造参数类型为CamelCasePluginOptions各选项的默认值与行为如下选项默认值说明upperCasefalse为true时 camelCase 转换为全大写 SNAKE_CASE例如fooBar FOO_BAR反向FOO_BAR fooBarunderscoreBeforeDigitsfalse为true时在数字前插入下划线例如foo12Bar foo_12_bar反向foo_12_bar foo12Bar多位数字只在第一个数字前插入underscoreBetweenUppercaseLettersfalse为true时在连续大写字母之间加下划线例如fooBAR foo_b_a_r默认为foo_barmaintainNestedObjectKeysfalse为true时嵌套对象的键不再转换为 camelCase仅转换顶层键这些选项在 src/plugin/camel-case/camel-case.ts 的createSnakeCaseMapper与createCamelCaseMapper中实现且两个映射函数都通过memoize做结果缓存避免重复字符串反复计算。createCamelCaseMapper在upperCase开启时会先判断字符串是否为全大写 snake_case如FOO_BAR是才转小写再做驼峰化从而保证已符合要求的字符串不被意外改动。使用示例文档中的示例SQLite 方言演示了数据库里的一切都以 camelCase 来写这一核心用法import * as Sqlite from better-sqlite3 import { CamelCasePlugin, Kysely, SqliteDialect } from kysely interface CamelCasedDatabase { userMetadata: { firstName: string lastName: string } } const db new KyselyCamelCasedDatabase({ dialect: new SqliteDialect({ database: new Sqlite(:memory:), }), plugins: [new CamelCasePlugin()], }) const person await db .selectFrom(userMetadata) .where(firstName, , Arnold) .select([firstName, lastName]) .executeTakeFirst() if (person) { console.log(person.firstName) }生成的 SQLSQLite为select first_name, last_name from user_metadata where first_name ?需要强调的是使用该插件时TypeScript 代码里的表名、列名、schema 等一切标识符都要写成 camelCaseKysely 会表现得像数据库本来就是 camelCase 定义的一样。高级定制覆写映射方法如果内置的转换规则仍不满足需求例如需要保留特定缩写文档与源码都支持通过继承覆写两个受保护方法class MyCamelCasePlugin extends CamelCasePlugin { protected override snakeCase(str: string): string { // 自定义 camelCase - snake_case 逻辑 return str } protected override camelCase(str: string): string { // 自定义 snake_case - camelCase 逻辑 return str } }四、DeduplicateJoinsPlugin消除动态查询中的重复 JOIN在动态构建查询时同一个 JOIN 可能被条件性地重复添加。例如 site/docs/recipes/0008-deduplicate-joins.md 中的getPerson函数当withPetName与withPetSpecies同时为true时pet表会被innerJoin两次导致数据库报错async function getPerson( id: number, withPetName: boolean, withPetSpecies: boolean, ) { return await db .selectFrom(person) .selectAll(person) .$if(withPetName, (qb) qb .innerJoin(pet, pet.owner_id, person.id) .select(pet.name as pet_name), ) .$if(withPetSpecies, (qb) qb .innerJoin(pet, pet.owner_id, person.id) .select(pet.species as pet_species), ) .where(person.id, , id) .executeTakeFirst() }安装DeduplicateJoinsPluginsrc/plugin/deduplicate-joins/deduplicate-joins-plugin.ts后其内部DeduplicateJoinsTransformer会扫描语法树并移除重复的 JOIN 节点。该插件既可按上文全局注册也可用.withPlugin(new DeduplicateJoinsPlugin())按需启用因此不会给绝大多数不需要去重的用户带来额外开销。关于为什么去重是插件而非默认行为文档给出了清晰的解释判断两个 JOIN 是否完全等价非常困难——简单 JOIN 很容易判断但包含嵌套子查询等复杂 JOIN 时会出现难以识别的边界情况。因此 Kysely 将其设计为可选项避免去重逻辑误伤不需要它的场景。五、HandleEmptyInListsPlugin从容应对in ()与not in ()in ()和not in ()在很多数据库中都是非法 SQL直接执行会触发运行时错误。HandleEmptyInListsPluginsrc/plugin/handle-empty-in-lists/handle-empty-in-lists-plugin.ts允许你通过strategy选项选择空列表的处理策略。EmptyInListsStrategy是一个接收EmptyInListNode、返回新的BinaryOperationNode的函数类型定义见 src/plugin/handle-empty-in-lists/handle-empty-in-lists.ts。需要特别说明设计哲学源码注释强调其他 ORM 的做法都是在底层偷偷改写查询这与 Kysely所见即所得WYSIWYG的理念不符。因此 Kysely 官方推荐在使用in/not in之前手动检查数组是否为空该插件采用 opt-in 方式由开发者显式选择是否以及如何处理并建议充分测试后再投入使用。策略一replaceWithNoncontingentExpression恒真/恒假替换将in替换为恒假表达式1 0将not in替换为恒真表达式1 1这与 Knex、Prisma、Laravel、SQLAlchemy 的处理方式一致。实现位于 src/plugin/handle-empty-in-lists/handle-empty-in-lists.ts 的replaceWithNoncontingentExpression其中1、等节点被模块级缓存复用||惰性初始化保证重复查询时零额外分配import Sqlite from better-sqlite3 import { HandleEmptyInListsPlugin, Kysely, replaceWithNoncontingentExpression, SqliteDialect, } from kysely const db new KyselyDatabase({ dialect: new SqliteDialect({ database: new Sqlite(:memory:) }), plugins: [ new HandleEmptyInListsPlugin({ strategy: replaceWithNoncontingentExpression, }), ], }) const results await db .selectFrom(person) .where(id, in, []) .where(first_name, not in, []) .selectAll() .execute()生成的 SQLSQLiteselect * from person where 1 0 and 1 1策略二pushValueIntoList向列表推入哨兵值对于in向空列表推入null得到in (null)。in (null)在逻辑上等价于 null结果为null大多数 SQL 数据库中的假值这与 TypeORM、Sequelize 的做法一致。源码明确建议如果打算在select、returning、output子句中使用in不要选用此策略因为其返回类型与比较运算默认的SqlBool类型不同。对于not in将左操作数强制转换为char并推入一个唯一字面值得到cast({{lhs}} as char) not in ({{VALUE}})。强制转换是为了避免非字符串列出现数据库错误。import Sqlite from better-sqlite3 import { HandleEmptyInListsPlugin, Kysely, pushValueIntoList, SqliteDialect, } from kysely const db new KyselyDatabase({ dialect: new SqliteDialect({ database: new Sqlite(:memory:) }), plugins: [ new HandleEmptyInListsPlugin({ // 为 not in 选择一个数据中绝对不可能出现的唯一值 strategy: pushValueIntoList(__kysely_no_values_were_provided__), }), ], }) const results await db .selectFrom(person) .where(id, in, []) .where(first_name, not in, []) .selectAll() .execute()生成的 SQLSQLiteselect * from person where id in (null) and cast(first_name as char) not in (__kysely_no_values_were_provided__)从 src/plugin/handle-empty-in-lists/handle-empty-in-lists.ts 的实现可以看到pushValueIntoList返回的闭包通过freeze复用in (null)的ValueListNode与cast ... as char所需的CastNode/DataTypeNode同样做了节点级缓存优化。自定义策略抛错或告警strategy可以是任意函数因此你也可以直接抛错避免向数据库发送注定失败的请求import Sqlite from better-sqlite3 import { HandleEmptyInListsPlugin, Kysely, SqliteDialect } from kysely const db new KyselyDatabase({ dialect: new SqliteDialect({ database: new Sqlite(:memory:) }), plugins: [ new HandleEmptyInListsPlugin({ strategy: () { throw new Error(Empty in/not-in is not allowed) }, }), ], }) const results await db .selectFrom(person) .where(id, in, []) .selectAll() .execute() // 抛出 Empty in/not-in is not allowed 错误同理你也可以在策略函数内打印警告日志后返回原节点实现只提醒不干预的效果。六、SafeNullComparisonPlugin让 NULL 比较符合直觉SQL 中 NULL、! NULL、 NULL永远返回NULL未知而不是开发者期望的真/假正确的写法是IS NULL/IS NOT NULL。SafeNullComparisonPluginsrc/plugin/safe-null-comparison/safe-null-comparison-plugin.ts专门解决这一问题当右侧操作数是null时自动执行如下算子改写原始算子改写后IS!IS NOTIS NOT其内部SafeNullComparisonTransformersrc/plugin/safe-null-comparison/safe-null-comparison-transformer.ts覆写transformBinaryOperation先递归转换子节点然后检查右操作数是否为null值的ValueNode、且算子属于/!/三者之一命中则重建为IS/IS NOT二元运算节点。典型收益当你在代码里持有可空变量如string | null时无需再手写条件化 WHERE 分支直接写query.where(name, , name)插件会根据运行时值自动选择正确算子const db new KyselyDatabase({ dialect, plugins: [new SafeNullComparisonPlugin()], }) // 当 name 为 null 时自动生成 where name is null // 当 name 为 foo 时生成 where name ? await db.selectFrom(person).where(name, , name).selectAll().execute()七、编写自定义插件从接口到完整实现基于 src/plugin/kysely-plugin.ts 的接口契约自定义插件的完整流程如下实现KyselyPlugin必须提供transformQuery同步返回RootOperationNode与transformResult异步返回PromiseQueryResultUnknownRow。用OperationNodeTransformer做查询改写从 src/operation-node/operation-node-transformer.ts 继承覆写对应的transform*方法如transformIdentifier、transformBinaryOperation即可精准修改语法树特定节点。所有内置插件的 transformer 都遵循这一模式。用WeakMapQueryId, T传递查询级数据在transformQuery写入、在transformResult读取避免内存泄漏。结果改写在transformResult中按需修改args.result.rows后返回新对象。仓库中还提供了几个便于参考的插件变体ImmediateValuePluginsrc/plugin/immediate-value/immediate-value-plugin.ts、ParseJSONResultsPluginsrc/plugin/parse-json-results/parse-json-results-plugin.ts以及WithSchemaPluginsrc/plugin/with-schema/with-schema-plugin.ts它们均从 src/index.ts 对外导出展示了查询改写与结果改写两类插件的不同侧重点可作为进一步学习的模板。八、结语Kysely 的插件系统以其简洁的两阶段钩子查询前改写 结果后改写实现了强大的可扩展性而官方内置的四个插件则分别覆盖了标识符命名转换、动态查询 JOIN 去重、空 IN 列表处理与 NULL 安全比较四个高频痛点。选择插件时的总原则是能用官方插件解决的优先使用能手动检查空数组的就不要引入额外策略需要特殊行为时再基于KyselyPlugin接口编写自定义插件。本文涉及的核心源码文件均可直接在仓库中查阅src/plugin/kysely-plugin.ts、src/plugin/camel-case/camel-case-plugin.ts、src/plugin/deduplicate-joins/deduplicate-joins-plugin.ts、src/plugin/handle-empty-in-lists/handle-empty-in-lists-plugin.ts 与 src/plugin/safe-null-comparison/safe-null-comparison-plugin.ts。赞分享后端数据库【免费下载链接】kyselyA type-safe TypeScript SQL query builder项目地址https://gitcode.com/gh_mirrors/ky/kysely点击查看免费下载相关推荐Kubo 插件系统完全指南加载机制、内置插件与自定义开发实战Kubo 插件系统完全指南加载机制、内置插件与自定义开发实战 KuboIPFS 的 Go 实现自 0.4.11 起引入了一套实验性插件系统允许在不重新编网络存储后端rrweb 插件机制完全指南从自定义 Record/Replay 插件到内置插件生态rrweb 插件机制完全指南从自定义 Record/Replay 插件到内置插件生态 导读 rrweb 提供了一套独立的 插件 APIPlugin API前端可观测性开发工具Bloatynosy插件系统完全指南从安装到自定义配置Bloatynosy是Windows系统优化和清理的终极工具其强大的插件系统让用户能够轻松扩展功能实现个性化系统配置。本文将为你详细介绍如何充分利用Bloa桌面应用上一篇InfoSpider日志系统设计结构化日志的实现与分析下一篇Wan2.2-S2V-14B技术深度解析音频驱动电影级视频生成的架构创新与实践应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表