- 数据库
- 后端
【免费下载链接】objection.js
An SQL-friendly ORM for Node.js
Objection.js 是一个面向 Node.js 的 SQL 友好 ORM,而插件(Plugin)是其扩展机制的核心:通过 class mixin 为模型类(Model)与查询构建器(QueryBuilder)注入自定义能力,而无需修改框架源码或全局对象。本文以官方 doc/guide/plugins.md 为骨架,结合仓库内的mixin/compose源码、examples/plugin 与 examples/plugin-with-options 两个官方示例工程及对应测试,系统讲解插件的编写规范、组合方式、带选项插件的工厂模式,以及 TypeScript 下的类型安全写法,读完后你可以独立开发并发布自己的 Objection.js 插件。
一、Objection.js 插件生态:官方收录的第三方插件
doc/guide/plugins.md维护了一份精选插件清单,收录条件是该插件遵循官方插件开发最佳实践(即下文所述的 class mixin 方式);少数无法用最佳实践实现的模块(如为其他框架编写的适配模块)作为例外收录。如果你的插件足够优秀,也可以通过 Pull Request 或 Issue 申请加入该列表。
第三方插件(模型能力增强)
以下插件直接扩展模型类的行为,官方文档描述如下:
- objection-authorize:将访问控制(access control)集成进 Objection 查询,实现行级权限过滤。
- objection-dynamic-finder:为模型提供动态查找器(dynamic finders),按需生成常用查询方法。
- objection-guid:自动为模型生成 GUID 主键值。
- objection-password:自动为模型密码字段做哈希处理(如 bcrypt),避免散落的手动加密逻辑。
- objection-soft-delete:以极简配置实现软删除(soft delete)功能。
- objection-unique:为模型提供唯一性校验(unique validation)。
- objection-visibility:以白名单/黑名单方式控制模型属性的可见与隐藏。
其他第三方模块(框架集成与 API 层)
- objection-filter:为数据及其关联模型提供 API 过滤能力。
- objection-graphql:自动为 Objection 模型生成丰富的 GraphQL schema。
- objectionjs-graphql:生成与 Objection 3.x 兼容(支持 Graph fetch)的 GraphQL schema。
从这批插件可以看出,Objection.js 的扩展点集中在模型生命周期钩子、查询构建器方法、属性校验与序列化等维度,而这些能力几乎全部可以通过下文将要介绍的 class mixin 技术实现。
二、插件开发最佳实践:插件即 class mixin
官方文档明确要求:插件应该实现为 class mixin。mixin 本质上就是一个"接收类作为参数、返回其子类"的函数。关于这一模式的经典描述可参考 "Real Mixins with JavaScript Classes"(即 Fagnani 的 mixin 模式文章,示例工程 examples/plugin/index.js 的注释也引用了它)。
插件不应直接修改objection.Model、objection.QueryBuilder 或其他任何全局变量——这是保证多个插件可以无冲突叠加、可组合可替换的基石。
2.1 最基本的 mixin 形态
function SomeMixin(Model) { // 返回的类不要命名(不要给 class 起名字), // 这样父类的名称会被继承下来,模型类名保持稳定。 return class extends Model { // 你的修改。 }; }2.2 应用 mixin 的正确姿势
class Person extends SomeMixin(Model) {}特别注意:mixin 永远不会修改传入的类,因此下面的写法完全无效:
// 这行代码什么都不做。 SomeMixin(Model); class Person extends Model {}也就是说,SomeMixin(Model)返回的是一个全新子类,如果不对其进行extends继承,调用结果会被直接丢弃。
2.3 多个 mixin 的叠加
class Person extends SomeMixin(SomeOtherMixin(Model)) {}嵌套写法在 mixin 数量较多时不易阅读,官方在 objection 主模块中提供了两个组合辅助函数:mixin与compose。
三、mixin 与 compose:组合多个插件的官方工具
3.1 使用mixin一次叠加多个插件
const { mixin, Model } = require('objection'); class Person extends mixin(Model, [ SomeMixin, SomeOtherMixin, EvenMoreMixins, LolSoManyMixins, ImAMixinWithOptions({ foo: 'bar' }) ]) {}3.2 使用compose预组合成可复用的"复合插件"
const { compose, Model } = require('objection'); const mixins = compose( SomeMixin, SomeOtherMixin, EvenMoreMixins, LolSoManyMixins, ImAMixinWithOptions({ foo: 'bar' }) ); class Person extends mixins(Model) {}两种方式在语义上等价:mixin(Model, plugins...)直接产出增强后的类;compose(plugins...)先产出一个"复合插件函数",再应用到某个模型类上。从源码 lib/utils/mixin.js 可以看到二者的实现非常简洁:
function mixin() { const args = flatten(arguments); const mixins = args.slice(1); return mixins.reduce((Class, mixinFunc) => { return mixinFunc(Class); }, args[0]); } function compose() { const mixins = flatten(arguments); return function (Class) { return mixin(Class, mixins); }; }mixin借助reduce将多个插件函数从左到右依次应用到模型类上(后一个插件继承前一个插件的返回类);compose只是把这一过程包装成了一个新的插件函数。参数经过flatten处理,因此既支持传入数组,也支持展开的多个参数——这正是类型定义中ComposeFunction与MixinFunction同时支持(plugins)与(...plugins)两种签名(见 typings/objection/index.d.ts)的原因。
对应 TypeScript 类型(来自 typings/objection/index.d.ts):
export interface Plugin { <M extends typeof Model>(modelClass: M): M; } export interface ComposeFunction { (...plugins: Plugin[]): Plugin; (plugins: Plugin[]): Plugin; } export interface MixinFunction { <MC extends AnyModelConstructor>(modelClass: MC, ...plugins: Plugin[]): MC; <MC extends AnyModelConstructor>(modelClass: MC, plugins: Plugin[]): MC; }3.3 作为装饰器(decorator)使用
如果你的项目启用了装饰器语法,mixin 也可以直接作为类装饰器使用:
@SomeMixin @MixinWithOptions({ foo: 'bar' }) class Person extends Model {}装饰器按书写顺序从下往上应用,MixinWithOptions({ foo: 'bar' })先执行(因为它本身是工厂调用,返回真正的装饰器),随后@SomeMixin再作用其上。
四、带选项的插件:工厂函数模式
官方为"需要接收配置参数的插件"单独提供了示例工程 examples/plugin-with-options。核心约定是:主模块导出一个工厂函数,工厂接收选项并返回一个 mixin。其骨架如下(摘自 examples/plugin-with-options/index.js):
module.exports = (options) => { // 尽可能为选项提供良好默认值。 options = Object.assign( { setModifiedBy: true, setModifiedAt: true, setCreatedBy: true, setCreatedAt: true, }, options, ); // 返回 mixin。如果插件不接收选项,可以直接导出 mixin 本身,无需工厂函数。 return (Model) => { return class extends Model { // ... }; }; };使用者这样调用(见 examples/plugin-with-options/README.md):
const Model = require('objection').Model; const Session = require('path/to/this/example')({ setCreatedBy: false, setModifiedBy: false }); class Person extends Session(Model) { static get tableName() { return 'Person'; } }对应测试(examples/plugin-with-options/tests.js)验证了关闭setCreatedBy/setModifiedBy后,插入/更新不再写入这两个字段,而createdAt/modifiedAt仍正常填充:
const sessionPlugin = sessionPluginFactory({ setModifiedBy: false, setCreatedBy: false, }); // ... expect(jonnifer.createdBy).to.equal(null); expect(jonnifer.createdAt).to.match(ISO_DATE_REGEX);这一模式的优点在于:选项在"工厂调用时"一次性固化,插件内部所有方法(包括钩子)都能闭包访问这些配置,无需依赖任何全局状态。
五、完整插件实例:自动记录会话审计字段的 session 插件
官方示例插件(examples/plugin/index.js)演示了插件开发的全部关键点:为QueryBuilder增加自定义方法、扩展模型钩子、通过查询上下文(query context)传递数据。其功能是:调用链上指定session后,模型在插入/更新时自动填充createdBy/createdAt/modifiedBy/modifiedAt,类似 passport.js 的登录用户审计。
5.1 扩展Model.QueryBuilder添加自定义方法
module.exports = (Model) => { // 如果要扩展 QueryBuilder,必须基于 `Model.QueryBuilder` 继承, // 因为该属性可能已被其他插件扩展过。 class SessionQueryBuilder extends Model.QueryBuilder { session(session) { // 把 session 存入查询上下文,使其在本次构建器派生的所有查询 // 以及模型钩子中都可用。`session` 并非保留字,你可以存任意数据。 return this.mergeContext({ session: session, }); } } // ... };从源码结构看,QueryBuilderBase.js 中的mergeContext负责将对象合并进当前查询上下文;上下文会随查询一路传播到模型钩子(如$beforeInsert、$beforeUpdate),这正是插件实现"隐式传参"的机制。
5.2 扩展模型钩子并兼容异步
return class extends Model { static get QueryBuilder() { return SessionQueryBuilder; } $beforeUpdate(opt, context) { // 扩展既有钩子时必须先调用父类实现, // 并注意父类实现可能是异步的(返回 Promise)。 const maybePromise = super.$beforeUpdate(opt, context); return Promise.resolve(maybePromise).then(() => { if (context.session) { this.modifiedAt = new Date().toISOString(); this.modifiedBy = context.session.userId; } }); } $beforeInsert(context) { const maybePromise = super.$beforeInsert(context); return Promise.resolve(maybePromise).then(() => { if (context.session) { this.createdAt = new Date().toISOString(); this.createdBy = context.session.userId; } }); } };两个值得注意的实践细节:
- 必须调用
super实现:插件叠加时每个钩子都是一条链,跳过super会破坏后续插件的逻辑。 - 用
Promise.resolve(maybePromise)统一处理同步/异步父类:官方文档与示例注释均提示,要检查钩子是否可能返回 Promise 并做好兼容,这保证了插件在任意叠加顺序下都安全。
5.3 返回匿名类,保持模型类名
示例反复强调:返回的类不要命名(return class extends Model),这样新类会继承父类的名称(Node 8 起支持),避免Person变成class_1之类影响调试与序列化行为。
5.4 在应用中与查询链路上使用
以 Express 路由为例(见 examples/plugin/README.md):
router.post('/persons', (req, res) => { return ( Person.query() // 下面的方法由我们的插件提供。 .session(req.session) .insert(req.body) .then((person) => { // 插件已自动填充以下属性。 console.log(person.createdAt); console.log(person.createdBy); res.send(person); }) ); });5.5 测试验证
examples/plugin/tests.js 使用 mocha + sqlite3 验证了插件的端到端行为:插入后断言createdBy等于传入 session 的userId、createdAt匹配 ISO 日期正则;随后通过$query(knex).session(...).patchAndFetch(...)更新,断言modifiedBy/modifiedAt被正确写入,且createdBy保持首次插入时的值。这也是插件作者自己编写测试时的直接模板。
六、TypeScript 下开发插件
仓库 doc/recipes/plugins.md 提供了 TypeScript 写法的权威示例,其核心是保持"高阶函数返回泛型子类"的形态,以获得完整的类型推断。
6.1 基础 TypeScript mixin
export function Mixin(options = {}) { return function<T extends typeof Model>(Base: T) { return class extends Base { mixinMethod() {} }; }; } // 使用方式一:函数式应用 class Person extends Model {} const MixinPerson = Mixin(Person); // 使用方式二:作为装饰器 @Mixin class Person extends Model {}6.2 附带自定义 QueryBuilder 的 TypeScript mixin
很多插件(如上面的 session 插件)同时扩展查询构建器。TypeScript 示例展示了如何声明QueryBuilderType及相关类型别名,让自定义查询方法获得完整链式类型检查:
class CustomQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> { ArrayQueryBuilderType!: CustomQueryBuilder<M, M[]>; SingleQueryBuilderType!: CustomQueryBuilder<M, M>; NumberQueryBuilderType!: CustomQueryBuilder<M, number>; PageQueryBuilderType!: CustomQueryBuilder<M, Page<M>>; someCustomMethod(): this { return this; } } export function CustomQueryBuilderMixin(options = {}) { return function<T extends typeof Model>(Base: T) { return class extends Base { static QueryBuilder = QueryBuilder; QueryBuilderType: CustomQueryBuilder<this, this[]>; mixinMethod() {} }; }; } // 使用:类型安全地链式调用插件方法与原生查询方法 const z = await MixinPerson.query() .whereIn('id', [1, 2]) .someCustomMethod() .where('foo', 1) .someCustomMethod(); z[0].mixinMethod();注意该示例中static QueryBuilder = QueryBuilder;的写法:与 JS 示例一致,插件应当在自身模型类上声明QueryBuilder的替换版本,从而让插件定义的查询方法对使用者可见。
七、编写插件必须遵守的几条铁律
综合官方文档 doc/guide/plugins.md 与两个示例工程,可以提炼出插件开发的核心约束:
- 插件是纯函数式 mixin:
(Model) => class extends Model {},接收类、返回子类,绝不修改传入类与任何全局对象。 - 命名谨慎:返回匿名类,让模型继承原有类名;若插件工厂需要导出,命名空间与包名保持清晰(示例包名为
objection-plugin-example,见 examples/plugin/package.json)。 - 扩展 QueryBuilder 时基于
Model.QueryBuilder:因为它可能已被其他插件扩展,直接基于objection.QueryBuilder会丢失先前插件的自定义方法。 - 尊重生命周期钩子:覆盖
$beforeInsert/$beforeUpdate等钩子时先调用super并用Promise.resolve兼容异步父实现。 - 用查询上下文传递隐式数据:通过
mergeContext在查询链与钩子之间传递 session 等业务上下文,避免静态变量与全局污染。 - 带选项则用工厂函数:
module.exports = (options) => (Model) => ...,并用Object.assign提供默认值。 - 提供测试:参照 examples/plugin/tests.js 与 examples/plugin-with-options/tests.js,用 mocha + 内存数据库验证插件的插入、更新与选项开关行为。
结语
Objection.js 的插件体系围绕"class mixin"这一单一而强大的概念展开:官方提供的mixin/compose辅助函数(lib/utils/mixin.js)让多插件组合保持优雅,examples/plugin与examples/plugin-with-options两个示例给出了无选项与带选项两种标准模板,而 doc/recipes/plugins.md 的 TypeScript 示例则让类型安全的插件开发成为可能。无论是实现软删除、权限过滤、GUID 生成还是 GraphQL schema 生成,遵循上述最佳实践开发出的插件都可以被社区收录进官方插件列表,并安全地与其他插件叠加使用。
- 数据库
- 后端
【免费下载链接】objection.js
An SQL-friendly ORM for Node.js
相关推荐
Objection.js插件开发最佳实践指南
Objection.js插件开发最佳实践指南 引言 还在为Objection.js缺少某些功能而烦恼?想要扩展ORM能力却不知从何下手?本文将为你揭秘Objec
数据库后端Top-AI-Tools免费资源汇总:无需付费也能使用的优质AI工具
Top AI Tools免费资源汇总:无需付费也能使用的优质AI工具 在当今AI技术快速发展的时代,找到既实用又免费的AI工具成为许多用户的需求。Top AI
F´(F Prime)GDS 插件开发实战指南:从 SELECTION 到 FEATURE 插件的完整实现
F´(F Prime)GDS 插件开发实战指南:从 SELECTION 到 FEATURE 插件的完整实现 F´(F Prime)地面数据系统(GDS)通过一套
嵌入式系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考