☰
objection.js 迁移指南:从 1.x 到 2.0 再到 3.0 的破坏性变更详解
2026/9/29 3:08:13 网站建设 项目流程
  • 数据库
  • 后端

【免费下载链接】objection.js

An SQL-friendly ORM for Node.js

项目地址:https://gitcode.com/gh_mirrors/ob/objection.js
点击查看免费下载

本篇技术指南基于 objection.js 官方 迁移文档 编写,系统梳理从 objection 1.x → 2.0 → 3.0 两个大版本升级中引入的全部破坏性变更(breaking changes),并结合当前仓库(objection 3.1.5)的源码与集成测试,说明每项变更的成因、影响范围以及可操作的迁移步骤。读完本文,你将能对照自己的项目逐条排查:Node/knex 版本约束、TypeScript 类型收窄、PromiseLike语义、被移除的废弃方法、#ref安全策略、relate/$relatedQuery返回值变化、db-errors 错误包装等,从而以最小的代价平滑升级到 objection 3.x。

迁移路线总览

objection.js 的版本演进中有两条关键的迁移路径:

  • 1.x → 2.0:以 API 清理为主,重写了 TypeScript 类型,移除了 Bluebird 与 lodash 依赖,并引入 db-errors 错误包装;
  • 2.0 → 3.0:继续收紧运行环境要求,修正 TypeScript 类型语义,并彻底删除所有在 2.0 中被标记废弃的方法。

当前仓库的 package.json 显示版本为3.1.5,engines声明node >= 14.0.0,peerDependencies要求knex >= 1.0.1(详见下文“版本要求已进一步提高”一节)。这说明 3.x 的实际运行门槛比迁移文档写作时更高,升级前务必核对你的运行时环境。

以下各节按“2.x → 3.0 → 1.x → 2.0”的顺序依次讲解,先从离你最近、最紧迫的 3.0 破坏性变更开始。

第一部分:objection 2.x → 3.0 迁移

3.0 的破坏性变更集中于运行环境下限提升、TypeScript 类型修正与废弃 API 清除三类,多数情况下编译器的报错信息就能指引你完成迁移。

不再支持 Node < 12

objection 3.0 要求至少 Node 12 才能运行。迁移文档原文如此规定,而当前仓库的 package.json 进一步将engines收紧为node >= 14.0.0。因此在实际升级前,请先确认你的部署环境与 CI 流水线使用的 Node 版本,低于 14 的运行时将无法通过 npm 安装或启动。

不再支持 knex < 0.95

objection 3.0 要求至少 knex 0.95.0,原因是 knex 在 0.95.0 中引入了破坏性变更。当前仓库的 package.json 将 peerDependencies 进一步提高为knex >= 1.0.1,而仓库自身的开发依赖使用的是 knex ^3.1.0。升级 objection 时请同步升级 knex,并阅读 knex 0.95.0 与 1.x 的发布说明,排查查询构建层面的兼容问题。

TypeScript:findById/findOne的返回值类型被修正

在 2.0 中,findById、findOne、first等“返回单条记录或空值”的方法被错误地标注为总是返回值。3.0 将其修正为SomeModel | undefined。

查看 typings/objection/index.d.ts 中的相关定义可以印证这一点:

findById(id: MaybeCompositeId): MaybeSingleQueryBuilder<this>; findByIds(ids: MaybeCompositeId[]): this; findOne: WhereMethod<MaybeSingleQueryBuilder<this>>;

其中MaybeSingleQueryBuilder最终解析为 QueryBuilder<M, M | undefined>,即查询结果可能为undefined。这一改动会让“此前信任旧类型”的代码产生编译错误,你需要显式收窄类型。

迁移方式一:手动判空

升级前(编译可通过但运行时可能因访问undefined.id而崩溃):

const person = await Person.query().findById(id) console.log(person.id)

升级后(先判空再使用):

const person = await Person.query().findById(id) if (!person) { throw new Error('Person not found') } console.log(person.id)

迁移方式二:链式调用throwIfNotFound

throwIfNotFound会在结果为空时抛出错误,并把返回类型从M | undefined收窄回M,这是最简洁的写法:

const person = await Person.query().findById(id).throwIfNotFound() console.log(person.id)

仓库集成测试 tests/integration/find.js 覆盖了.throwIfNotFound()的空结果抛错、非空结果放行、自定义错误消息以及createNotFoundError钩子等场景,可作为行为参考。该方法还能配合自定义消息:

const person = await Person.query().findById(id).throwIfNotFound({ message: 'customMessage' })

TypeScript:QueryBuilder由继承Promise改为实现PromiseLike

2.0 及更早版本在类型层面让QueryBuilder继承Promise,因此下面的代码可以编译:

function findPerson(id: number): Promise<Person> { return Person.query().findById(id); }

但运行时QueryBuilder并非真正的Promise,它只是一个“thenable”(可 await 对象)。3.0 改为使用 TypeScript 为 thenable 设计的PromiseLike类型。查看 typings/objection/index.d.ts 的定义:

export class QueryBuilder<M extends Model, R = M[]> implements CatchablePromiseLike<R> {

CatchablePromiseLike继承自PromiseLike(见 typings/objection/index.d.ts),并额外提供了catch方法。这一改动会让“把查询构建器直接当作Promise返回”的函数产生编译错误,有三种修复方式:

方式一:链式调用execute()得到真正的 Promise

function findPerson(id: number): Promise<Person> { return Person.query().findById(id).execute(); }

方式二:函数返回类型改为PromiseLike

function findPerson(id: number): PromiseLike<Person> { return Person.query().findById(id); }

方式三:将函数声明为async

async function findPerson(id: number): Promise<Person> { return Person.query().findById(id); }

三种方式语义等价,按团队代码风格选择其一即可。

移除了一批废弃方法与特性

2.0 中所有被标记废弃(运行时打印废弃警告)的方法,在 3.0 中已全部删除。仓库中遗留的deprecate工具实现(lib/utils/deprecate.js)可以帮你理解 2.0 时代的警告机制:它通过LOGGED_DEPRECATIONS集合保证同一条警告每个进程只打印一次,并调用console.warn输出。也就是说,2.x 时期你看到的“每个进程只出现一次”的警告,就是在提示这些将在 3.0 中被删除的 API。

升级到 3.0 之前,请在你的代码库中全局搜索这些废弃调用的残余,逐项替换为警告信息中提示的新方法。典型例子是 2.0 中已废弃的Model.relatedFindQueryMutates与Model.relatedInsertQueryMutates(详见第二部分“$relatedQuery不再修改实例”一节)。

第二部分:objection 1.x → 2.0 迁移

2.0 带来了大量新特性,但重心在于 API 清理,由此产生了一批破坏性变更。除下列条目外,2.0 还废弃并替换了大量方法——旧方法仍可使用,但每次进程会打印一次警告,警告内容会告诉你应该改用哪个新方法,你可以按警告逐项替换。

Node 6 和 7 不再受支持

objection 2.0 要求至少 Node 8 才能运行(Node 6、7 停止支持)。结合第一部分的说明,3.0 进一步要求 Node ≥ 12(当前仓库 package.json 实际要求 ≥ 14),请一并规划 Node 版本的升级节奏。

modify方法签名变更

旧版允许通过多个参数指定多个 modifier 名称:

Person.query().modify('foo', 'bar');

2.0 起,modify只使用第一个参数指定 modifiers,其余参数全部作为 modifier 的入参。如果确实需要一次应用多个 modifier,请把它们包进数组:

Person.query().modify(['foo', 'bar']);

这是本次迁移中最容易踩坑的签名变更之一,请检查所有使用多参数modify的调用点,避免修饰器入参被误当成 modifier 名称。

Bluebird 与 lodash 被移除

2.0 之前,objection 的所有异步操作返回 Bluebird Promise;2.0 起改为使用原生Promise。相应地,以下 Bluebird 专属方法从QueryBuilder上移除:

  • map
  • reduce
  • reflect
  • bind
  • spread
  • asCallback
  • nodeify

你需要全库排查,确保没有使用上述 Bluebird 方法,也不要假设 objection 返回的是 Bluebird Promise。此外,以下导出已被删除:

import { Promise, lodash } from 'objection';

2.0 起 objection 不再导出Promise和lodash属性,请改用原生Promise与独立的 lodash 包。

数据库错误统一来自 db-errors 库

2.0 之前,数据库操作失败时 objection 会直接透传数据库客户端抛出的原生错误;2.0 起错误被 db-errors 库包装,而当前仓库的 package.json 中db-errors: ^0.2.3依然作为直接依赖存在,说明这一机制延续到了 3.x。

db-errors 的包装错误暴露了nativeError属性。如果你依赖旧错误的属性(例如err.code),最快的迁移办法是:

try { await Person.query().where('foo', 'bar') } catch (err) { if (err.code === 13514) { ... } }

改为:

try { await Person.query().where('foo', 'bar') } catch (err) { err = err.nativeError || err if (err.code === 13514) { ... } }

更推荐的做法是使用 db-errors 提供的具体错误类(如UniqueViolationError、NotNullViolationError等),按错误类型而不是错误码处理,参见仓库的 错误处理配方。

insertGraph/upsertGraph中的#ref引用必须显式开启allowRefs: true

从 2.0 开始,在insertGraph与upsertGraph中使用'#ref': 'someId'或#ref{someId.someProp}引用,必须显式传入allowRefs: true选项:

await Person.query().insertGraph(graphWithRefs, { allowRefs: true });

为什么这样做?这是出于安全考虑。攻击者理论上可以利用#ref{someId.someProperty}引用访问对象中敏感字段,例如窃取用户密码哈希:

const graphUpsertSentByTheAttacker = { user: { id: 13431, '#id': 'user', }, movie: { name: '#ref{user.passwordHash}', }, };

随后攻击者就能从 movie 的 name 字段中取出密码哈希。

需要澄清的是,要让该攻击成立,攻击者必须先能访问修改用户信息的 API;更重要的是,引用只能访问对象本身里的属性,永远无法访问数据库中的列——只有当程序在调用upsertGraph之前把哈希写进 graph 对象时,上述 graph(以及其它可构想的 graph)才会把密码哈希泄露到 movie.name 中。这属于极低概率场景,且就密码场景而言,还要求攻击者能访问修改用户密码的路由。

结论:尽管当前能实际发起的攻击可能性很小,官方仍建议永远不要对未经校验的用户输入使用upsertGraph(graph, { allowRefs: true })。

从源码看,该选项的解析位于 lib/queryBuilder/graph/GraphOptions.js,而 lib/queryBuilder/graph/GraphUpsert.js 会在未开启allowRefs时抛出错误:'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option'。集成测试 tests/integration/insertGraph.js 验证了未开启时使用#ref与#ref{}都会抛错、开启后正常工作的行为;针对该安全问题还专门有回归测试 tests/integration/misc/refAttack.js。

relate方法现在始终返回受影响行数

1.x 中,relate在ManyToManyRelation场景返回插入的中间表行,在其它关系场景返回更新的行数。2.0 起统一返回表示受影响行数的整数。请检查代码中所有对relate返回值的使用方式并相应调整。

$relatedQuery不再修改实例

在 objection 1.x 中,执行下面的代码会在somePerson上新增pets属性并保存结果:

await somePerson.$relatedQuery('pets');

2.0 起这一行为不再发生;同理,以下插入操作也不会再把新宠物追加到somePerson.pets数组:

await somePerson.$relatedQuery('pets').insert(pet);

替代方案:

  1. 需要填充关系时,使用withGraphFetched与fetchGraph方法;
  2. 或手动赋值:
somePerson.pets = await somePerson.$relatedQuery('pets');
  1. 若想恢复 1.x 行为,可通过静态属性Model.relatedInsertQueryMutates与Model.relatedFindQueryMutates切换,但注意它们在 2.0 中已标记废弃,且已在 3.0 中被移除。

从源码看,这两个静态属性在 lib/model/Model.js 中默认被设为false,对应的读取访问器位于 lib/model/Model.js。升级到 3.0 时请删除任何对这两个属性的依赖。

context()现在等价于mergeContext()

1.x 中QueryBuilder的context方法会用传入对象替换当前上下文;2.0 起改为合并。如需旧行为,请先调用clearContext清空上下文:

builder.clearContext().context(newObject);

Model.raw与Model.fn现在返回 objection 的 raw 与 fn

1.x 中Model.raw返回 knex 的 raw builder,Model.fn返回 knex 的FunctionHelper实例;2.0 起它们改为返回 objection 自己的 raw 与 fn 辅助工具。需要 knex 原生 raw builder 时,请直接使用knex.raw。

QueryBuilder.toString与QueryBuilder.toSql被移除

这两个方法在 2.0 中被移除,替代写法是:

builder.toKnexQuery().toSQL()

从源码看,lib/queryBuilder/QueryBuilderOperationSupport.js 中保留了toKnexQuery(将 objection 查询构建器转换为 knex 查询构建器),并基于它提供了toString()(返回 SQL 字符串)。也就是说,生成 SQL 的推荐路径始终是先toKnexQuery()再调用 knex 的toSQL()。

TypeScript 类型被完全重写

2.0 对类型定义做了彻底重写,大量类型名称发生了变化。如果你只依赖类型推断,默认不会遇到太多错误;但凡是显式声明过 objection 类型的地方(除Model外),都可能需要调整。

无法给出通用的迁移步骤,因为迁移量取决于你在多大程度上信任类型推断、又在多大程度上使用了显式类型。但有几点值得注意:

  • QueryBuilder不再接受三个泛型参数,而是两个(<M, R>);
  • 关系属性不应再声明为Partial<Model>或Partial<Model>[],直接使用Model与Model[]即可,insertGraph、upsertGraph等方法会正常工作。

当前仓库的 typings/objection/index.d.ts 即为 3.x 重写后的类型定义,可作为升级后的对照基准;仓库自带的 TypeScript 测试(如 tests/ts/query-builder-api/find-methods.ts、tests/ts/query-builder-api/mutating-methods.ts)与npm run test:typings(内部执行tsc,见 package.json)可用于验证你的类型迁移结果。

迁移清单与常见问题

将本文要点整理为一份可勾选的升级清单:

  1. 运行环境:Node 升级到 ≥ 12(当前仓库要求 ≥ 14);knex 升级到 ≥ 0.95(当前仓库要求 ≥ 1.0.1);
  2. 类型收窄:所有findById/findOne/first的结果先判空,或链式调用.throwIfNotFound();
  3. Promise 语义:凡把查询构建器直接返回为Promise<T>的函数,改为.execute()、PromiseLike<T>或async函数;
  4. 废弃 API:清理relatedFindQueryMutates/relatedInsertQueryMutates及其它 2.x 时代打印过废弃警告的方法;
  5. modify签名:多 modifier 改为数组传参,其余参数视为 modifier 入参;
  6. 错误处理:数据库错误改为通过err.nativeError取原生错误,或改用 db-errors 错误类;
  7. #ref安全:insertGraph/upsertGraph使用引用时显式传{ allowRefs: true },并避免对未校验的用户输入开启;
  8. 返回值语义:relate统一返回受影响行数;$relatedQuery不再写回实例,改用withGraphFetched/fetchGraph或手动赋值;
  9. 上下文与工具方法:context()改为合并语义(必要时先clearContext());Model.raw/Model.fn语义变更;SQL 生成改用toKnexQuery().toSQL();
  10. 类型定义:QueryBuilder泛型参数由三个改为两个,关系属性直接用Model/Model[]。

常见问题速览:

  • 为什么console.log(person.id)在 3.0 编译报错?因为findById现在返回M | undefined,需要判空或throwIfNotFound()。
  • 为什么把 query builder 当作Promise返回会报错?因为类型已从继承Promise改为实现PromiseLike,两者在类型系统中不可直接互赋。
  • 升级后数据库错误码怎么取?用err.nativeError || err拿到原生错误,或改用 db-errors 的错误类判断。
  • #ref为什么默认禁止?防止攻击者通过引用读取对象中的敏感字段,官方建议永远不要对未校验输入开启allowRefs。

最后提醒:本文所有迁移步骤均以当前仓库(objection 3.1.5)的源码与测试为准;若你在迁移中遇到文档未覆盖的缺口,可在项目中结合 源码、类型定义 与 集成测试 自行验证,并参考 发布说明 了解各版本细节。

  • 数据库
  • 后端

【免费下载链接】objection.js

An SQL-friendly ORM for Node.js

项目地址:https://gitcode.com/gh_mirrors/ob/objection.js
点击查看免费下载
上一篇:炉石传说脚本终极指南:5分钟快速上手开源自动化工具
下一篇:免费压缩包密码恢复工具:让遗忘的密码不再成为障碍

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

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

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

立即咨询