- 数据库
- 后端
【免费下载链接】objection.js
An SQL-friendly ORM for Node.js
Objection.js 的 QueryBuilder 除查找、修改、关联加载等主流程方法外,还提供了一批支撑性工具方法:它们负责查询上下文传递、SQL 构建钩子、结果类型控制、分页计数、查询内省与伪造结果等能力。本文基于 other-methods.md 完整梳理这些方法,并结合 QueryBuilder.js 等源码验证其底层行为,帮助你写出更健壮、更可维护的 Objection.js 查询代码。
debug():打印执行的 SQL
将debug()链式追加到任意查询上,即可把所有将要执行的 SQL 打印到控制台:
const people = await Person.query().debug().where('age', '>', 30);在源码中该方法通过addOperation(new KnexOperation('debug'), args)注册为一次 Knex 操作(见 QueryBuilderBase.js),意味着它直接透传给 Knex 的debug能力。注意:当一个 QueryBuilder 会触发多条 SQL(例如withGraphFetched)时,每一条都会被打印,非常适合排查 N+1 与慢查询。
toKnexQuery():编译为 Knex 查询
knexQueryBuilder = queryBuilder.toKnexQuery();该方法将 objection 查询编译成对应的 Knex QueryBuilder 实例返回,供需要直接操作 Knex 的底层场景使用。需要注意两点:
- 多查询场景只返回第一条:像
withGraphFetched这样的方法实际会执行多条查询,此时返回的是第一条查询对应的 knex builder; - 少数情况无法同步构建:某些查询无法同步编译成 knex 查询,此时会抛出明确的错误信息。可以通过在调用失败前执行一次 initialize 来解决:
const { initialize } = require('objection'); await initialize([Person, Pet, Movie, SomeOtherModelClass]);initialize会预绑定模型类所需的表元数据,从而让后续的toKnexQuery()可以同步完成。
for():配合 relatedQuery 指定关系所有者
queryBuilder = queryBuilder.for(relationOwner);for()只能与静态方法 relatedQuery 配合使用,用于指定关系查询的“所有者”。其参数可以是以下任意类型:
- 单个标识符(支持复合主键);
- 标识符数组(支持复合主键);
- 一个 QueryBuilder;
- 一个模型实例;
- 模型实例数组。
典型用法是查询某条记录关联的另一侧数据,例如Person.relatedQuery('pets').for(somePerson)。
context() 与 clearContext():查询上下文
queryBuilder = queryBuilder.context(queryContext);查询上下文是一个在所有由该 builder 发起的查询之间共享的对象——有些 builder 方法(如withGraphFetched)会触发不止一条查询,上下文会贯穿始终。它还会被传递给查询触发的$beforeInsert、$afterInsert、$beforeUpdate、$afterUpdate、$beforeDelete、$afterDelete、$afterFind等实例生命周期钩子(详见 instance-methods.md)。
上下文始终带有一个transaction属性:当查询处于事务中时它持有活动事务对象,否则持有普通 knex 实例。由于两者都可用在任何需要事务对象的地方,你永远不需要显式检查transaction是否存在。
context()会与当前上下文做合并(不是替换),需要清空时调用clearContext():
queryBuilder = queryBuilder.clearContext();它把当前上下文替换为一个空对象。设置与读取:
await Person.query().context({ something: 'hello' }); // ... const context = builder.context();你可以在上下文中存放任意数据,甚至可以注册 QueryBuilder 生命周期方法,让所有共享该上下文的查询都执行这些钩子:
Person.query().context({ runBefore(result, builder) { return result; }, runAfter(result, builder) { return result; }, onBuild(builder) {} });一个典型场景:withGraphFetched会从一个 builder 派生出多条查询,若希望它们全部使用同一 schema,可以这样写:
Person.query() .withGraphFetched('[movies, children.movies]') .context({ onBuild(builder) { builder.withSchema('someSchema'); } });从源码看,QueryBuilderContext内部维护runBefore、runAfter、onBuild三个数组用于存放注册的钩子(见 QueryBuilderContext.js),而QueryBuilderContextBase则持有userContext、options、knex、aliasMap、tableMap等内部状态(见 QueryBuilderContextBase.js),克隆 builder 时会同步复制这些数组与状态。
transacting():为查询绑定事务
queryBuilder = queryBuilder.transacting(transaction);为查询显式设置事务对象,返回 builder 本身以便链式调用。源码中它直接写入内部上下文:this._context.knex = trx || null(见 QueryBuilderBase.js)。事务的完整用法(startTransaction、transaction()帮助函数、withTransaction等)参见 transactions.md。
tableNameFor() 与 tableRefFor():解析表名与引用名
const tableName = queryBuilder.tableNameFor(modelClass); const tableRef = queryBuilder.tableRefFor(modelClass);tableNameFor(modelClass)返回查询中该模型类的源表(或视图)名。通常可直接用Model.tableName,但若通过 table 方法改过源表,就必须用tableNameFor才能拿到正确值;tableRefFor(modelClass)返回查询中引用该表时应使用的名称。一般情况下表名即可直接引用,但当表被赋予别名时,返回值会不同。
源码中tableNameFor维护在ctx.tableMap中,tableRefFor则是aliasFor(tableName) || tableNameFor(tableName)(见 QueryBuilderOperationSupport.js)。这两个方法也是内部实现的重要基础设施:关联连接、图查询拼接列名时都会用到,例如 JoinRelatedOperation.js 用tableRefFor决定关联表引用,RelationJoiner.js 用tableNameFor获取表元数据。
resolve()、reject() 与 isExecutable():伪造结果与控制执行
queryBuilder = queryBuilder.resolve(value); queryBuilder = queryBuilder.reject(reason);resolve(value)跳过真实数据库查询、“伪造”一个成功结果;reject(reason)则“伪造”一个错误结果。它们都返回 builder 以便链式调用。这在单元测试、Mock 数据或短路逻辑中非常有用。
源码中两者分别把值存入_explicitResolveValue与_explicitRejectValue(见 QueryBuilder.js)。
const isExecutable = queryBuilder.isExecutable();isExecutable()返回false表示该查询永远不会真正执行,可能的原因有两类:
- 查询被显式
resolve或reject; - 查询执行时会启动另一条不同的查询。
对应源码为return !this.isExplicitlyResolvedOrRejected() && !findQueryExecutorOperation(this)(见 QueryBuilder.js)。后一种情况的典型例子是withGraphFetched或range这类“由一个 builder 派发多条查询”的方法,外层 builder 本身不再直接执行 SQL。
查询内省方法族:isXxx 与 hasXxx
Objection.js 提供一组无副作用的查询状态判断方法,常用于编写通用工具或中间件。
操作类型判断,全部返回boolean:
| 方法 | 说明 |
|---|---|
isFind() | 查询是否为只读查询 |
isInsert() | 查询是否执行 insert 操作 |
isUpdate() | 查询是否执行 update 或 patch 操作 |
isDelete() | 查询是否执行 delete 操作 |
isRelate() | 查询是否执行 relate 操作 |
isUnrelate() | 查询是否执行 unrelate 操作 |
isInternal() | 是否为内部“辅助”查询(不属于正在执行的主操作),例如upsertGraph为获取图当前状态而执行的 select 查询 |
这些方法的源码实现非常直观:isInsert()即this.has(InsertOperation),isUpdate()即this.has(UpdateOperation),依此类推(见 QueryBuilder.js)。
语句存在性判断:
| 方法 | 说明 |
|---|---|
hasWheres() | 是否包含 where 语句 |
hasSelects() | 是否包含明确的 select 语句(select、columns、column、distinct、count、countDistinct、min、max、sum、sumDistinct、avg、avgDistinct) |
hasWithGraph() | 是否已调用withGraphFetched或withGraphJoined |
注意hasWheres()在源码中会先clone().clearWithGraph()再判断(见 QueryBuilder.js),即它只关心查询主体自身的 where 条件,而不包括关联图展开产生的条件。
按选择器匹配操作:
const has = queryBuilder.has(selector);has(selector)接受字符串或正则表达式,返回查询中是否存在匹配该选择器的操作:
console.log( Person.query() .range(0, 4) .has('range') ); // --> truequeryBuilder = queryBuilder.clear(selector);clear(selector)移除所有匹配给定选择器(字符串或正则)的操作:
console.log( Person.query() .orderBy('firstName') .clear('orderBy') .has('orderBy') ); // --> falserunBefore()、onBuild()、onBuildKnex()、runAfter()、onError():生命周期钩子
这五个方法是 QueryBuilder 执行流程的核心扩展点。从源码看它们统一通过addOperation(...)注册为对应 Operation(见 QueryBuilder.js),执行顺序为:runBefore→onBuild→onBuildKnex→ (执行 SQL) →runAfter→onError(出错时)。
runBefore():执行 SQL 之前
queryBuilder = queryBuilder.runBefore(runBefore);注册一个在数据库查询之前调用的函数,多个函数可以像 Promise 的then一样链式串联,且支持 async。注意函数必须返回供后续调用链继续处理的结果:
const query = Person.query(); query .runBefore(async result => { console.log('hello 1'); await Promise.delay(10); console.log('hello 2'); return result; }) .runBefore(result => { console.log('hello 3'); return result; }); await query; // --> hello 1 // --> hello 2 // --> hello 3onBuild():构建 SQL 时
queryBuilder = queryBuilder.onBuild(onBuild);注册的函数在每次将查询构建为 SQL 字符串时被调用,位于runBefore之后、runAfter之前。如果需要修改生成的 SQL,这里才是正确的位置,不应在任何run方法中改查询。
与run系列方法不同,onBuild回调必须是同步的,也不应从其中注册任何run方法——你只应该调用作为参数传入的 builder 的查询构建方法:
const query = Person.query(); query .onBuild(builder => { builder.where('id', 1); }) .onBuild(builder => { builder.orWhere('id', 2); });onBuildKnex():在 Knex 层修改 SQL
queryBuilder = queryBuilder.onBuildKnex(onBuildKnex);与onBuild的执行时机相同(都在 SQL 构建阶段,位于onBuild之后、runAfter之前),区别在于:此时 objection builder已经被编译成 knex query builder,onBuildKnex收到的参数是(knexBuilder, objectionBuilder)。
::: warning 在onBuildKnex中绝不要对objectionBuilder调用任何查询构建(或其他变更)方法——这些调用会被忽略,因为 builder 已经编译完成,你只应修改knexBuilder。不过可以在 objection builder 上调用hasSelects、hasWheres等只读方法。 :::
const query = Person.query(); query.onBuildKnex((knexBuilder, objectionBuilder) => { knexBuilder.where('id', 1); });runAfter():查询执行之后
queryBuilder = queryBuilder.runAfter(runAfter);注册的函数在 builder 执行时被调用,作为then方法注册的任何 Promise 处理器执行前的最后一步,多个函数可像 Promisethen一样链式串联,支持 async,同样必须返回结果:
const query = Person.query(); query .runAfter(async (models, queryBuilder) => { return models; }) .runAfter(async (models, queryBuilder) => { models.push(Person.fromJson({ firstName: 'Jennifer' })); return models; }); const models = await query;onError():错误处理
queryBuilder = queryBuilder.onError(onError);注册错误处理器,行为类似catch,但不会执行查询:
const query = Person.query(); query .onError(async (error, queryBuilder) => { // 处理 `SomeError`,其余错误继续抛出 if (error instanceof SomeError) { // 返回对象会让查询以该对象作为结果 resolve 而不是抛错 return { error: 'some error occurred' }; } else { return Promise.reject(error); } }) .where('age', '>', 30);castTo() 与 modelClass():结果类型控制
queryBuilder = queryBuilder.castTo(ModelClass);queryBuilder = queryBuilder.castTo<SomeType>();castTo()用于设置结果行的模型类。典型场景是:从Person发起查询、join 一系列关联、只 select 关联Animal的列,然后把结果转成Animal实例而非Person实例:
const animals = await Person.query() .joinRelated('children.children.pets') .select('children:children:pets.*') .castTo(Animal);如果不传参数,只提供 TypeScript 泛型参数,则运行时不改变结果,仅把 TS 类型“断言”为给定泛型:
interface Named { name: string; } const result = await Person.query() .select('firstName as name') .castTo<Named[]>(); console.log(result[0].name);源码中castTo(modelClass)将_resultModelClass设置为传入的模型类(见 QueryBuilder.js),最终结果实例化时即使用该模型类。
const modelClass = queryBuilder.modelClass();modelClass()返回该 builder 所绑定的 Model 子类,用于在通用逻辑中反查模型定义。
skipUndefined():忽略 undefined 参数
queryBuilder = queryBuilder.skipUndefined();一旦调用,传入查询构建方法的undefined值将不再抛出异常,而是被直接忽略。典型场景是 Web 查询参数可能缺失:
Person.query() .skipUndefined() .where('firstName', req.query.firstName);当req.query.firstName为undefined时,上述查询会返回所有Person行而不是报错。这一行为同样作用于findById等便捷方法:源码中 FindByIdOperation.js 会先检查builder.internalOptions().skipUndefined,为真时跳过assertIdNotUndefined的断言,避免传入undefined主键时报错。
first():取结果第一项
queryBuilder = queryBuilder.first();如果查询结果是数组,则取第一个元素;否则原样返回:
const firstPerson = await Person.query().first(); console.log(firstPerson.age);注意:first()默认不会给查询追加limit 1。如需该行为,可通过覆盖 Model.useLimitInFirst 静态属性来改变。源码中FirstOperation正是这样实现的:仅当builder.isFind() && modelClass.useLimitInFirst时才limit(1)(见 FirstOperation.js)。作为便捷方法,可替代 findById 与 findOne 的某些用法。
throwIfNotFound():空结果即抛错
queryBuilder = queryBuilder.throwIfNotFound(data);当查询结果为空时抛出 Model.NotFoundError。可选参数data可携带自定义数据(如message、type),这些数据会挂在所抛错误的data属性下,其中message特殊——它用于设置错误的标题。这些附加属性可供错误处理中间件利用。
try { await Language.query() .where('name', 'Java') .andWhere('isModern', true) .throwIfNotFound({ message: `Custom message returned`, type: `Custom type` }); } catch (err) { // 没有查到结果 console.log(err instanceof Language.NotFoundError); // --> true }若想用自定义错误替换Model.NotFoundError,可以实现静态方法 Model.createNotFoundError(ctx)。源码中该方法通过runAfter检查结果并抛出错误(见 QueryBuilder.js),错误类型与定制方式可进一步参考 error-handling.md。
resultSize():查询结果总数
const promise = queryBuilder.resultSize();返回当前查询在不施加 limit 与 offset时会产生多少行。注意它执行的是查询的一个副本,并返回Promise<number>。相比返回对象数组的count,resultSize直接给出数字,往往更方便:
const query = Person.query().where('age', '>', 20); const [total, models] = await Promise.all([ query.resultSize(), query.offset(100).limit(50) ]);page() 与 range():分页查询
page(page, pageSize)
queryBuilder = queryBuilder.page(page, pageSize);以“页码 + 页大小”的方式分页,第一页索引为 0:
const result = await Person.query() .where('age', '>', 20) .page(5, 100); console.log(result.results.length); // --> 100 console.log(result.total); // --> 3341range(start, end)
queryBuilder = queryBuilder.range(start, end);以“起止索引”的方式切片,两端都包含:
const result = await Person.query() .where('age', '>', 20) .range(0, 100); console.log(result.results.length); // --> 101 console.log(result.total); // --> 3341range()也可以不传参数调用,此时显式使用limit/offset指定范围:
const result = await Person.query() .where('age', '>', 20) .limit(10) .range(); console.log(result.results.length); // --> 101 console.log(result.total); // --> 3341两种方法都会执行两条查询:实际数据查询 + 计算total的计数查询。page()在源码上就是range的语法糖:this.range(+page * +pageSize, (+page + 1) * +pageSize - 1)(见 QueryBuilder.js)。
为什么不直接用数据库原生方案?原文档给出了作者调研的结论:MySQL 的SQL_CALC_FOUND_ROWS与FOUND_ROWS()虽然能算结果大小,但实测性能明显比单独执行一次 count 查询差;PostgreSQL 可以用select count(*) over () as total窗口函数,但结果集为空时拿不到 total(如果你能绕过这个限制,欢迎提交 PR)。因此 Objection.js 选择“两条查询”策略。
从 RangeOperation.js 的源码可以看清实现细节:onAdd阶段把limit(end - start + 1).offset(start)设置到主查询(特意放在这里,避免进入结果大小查询);onBefore1阶段克隆一个resultSizeBuilder;onAfter3阶段执行克隆查询得到total,最终返回{ results, total }结构(见 RangeOperation.js)。
execute()、then()、catch()、bind() 与 clone():执行与复制
const promise = queryBuilder.execute();execute()执行查询并返回 Promise,resolve 为查询结果。
const promise = queryBuilder.then(successHandler, errorHandler);then(successHandler, errorHandler)执行查询并返回 Promise,两个处理器默认均为 identity((x) => x)。
const promise = queryBuilder.catch(errorHandler);catch(errorHandler)执行查询,并对返回的 Promise 调用catch(errorHandler)。
const promise = queryBuilder.bind(returnValue);bind(context)执行查询并对返回的 Promise 调用bind(context)(第二个参数context默认undefined),相当于把查询当作普通 Promise 参与异步流程。
从源码看,then与catch都只是先调用execute()再转发参数(见 QueryBuilder.js),这也解释了为什么“直接await一个 QueryBuilder”是可行的——builder 实现了 thenable 接口。
const clone = queryBuilder.clone();clone()创建当前 builder 的深拷贝。这对在多个分支上复用同一查询模板非常关键,resultSize、range等内部实现也大量依赖克隆(见 QueryBuilder.js 及上述 RangeOperation.js)。
modify() 与 modifiers():命名修饰符与内联修饰
modify()
queryBuilder = queryBuilder.modify(modifier, ...args);功能类似 Knex 的modify,但额外支持传入修饰符名称。第一个参数可以是:
- 模型修饰符名称(字符串):其余参数作为该修饰符的参数传入:
Person.query().modify('someModifier', 'foo', 1);- 修饰符名称数组:
Person.query().modify(['someModifier', 'someOtherModifier'], 'foo', 1);- 回调函数:接收 builder 作为第一个参数,随后是可选参数:
function modifierFunc(query, arg1, arg2) { query.where(arg1, arg2); } Person.query().modify(modifierFunc, 'foo', 1);模型修饰符通过 Model.modifiers 静态属性定义,更多玩法参见 modifiers.md 菜谱。
modifiers()
queryBuilder = queryBuilder.modifiers(modifiers);为当前查询注册内联修饰符;不传参数调用则返回当前已注册的修饰符:
const people = await Person.query() .modifiers({ selectFields: query => query.select('id', 'name'), // 下面的 `filterGender` 是 Person.modifiers 中注册的修饰符, // 查询修饰符可以通过这种方式给模型修饰符绑定参数 filterWomen: query => query.modify('filterGender', 'female') }) .modify('selectFields') .withGraphFetched('children(selectFields, filterWomen)');读取当前注册的修饰符:
const modifiers = query.modifiers();源码中modifiers(modifiers)在无参数调用时直接返回已注册修饰符(见 QueryBuilder.js)。
timeout() 与 connection():Knex 透传方法
timeout()与connection()与 Knex 同名方法行为一致,分别用于设置查询超时与指定查询连接,返回 builder 以便链式调用。它们与debug()一样,在源码中通过KnexOperation透传给 Knex(见 QueryBuilderBase.js)。
小结
Objection.js 的这批“其他方法”虽然不直接负责增删改查,却是搭建健壮查询链的关键拼图:context让多查询共享数据与钩子,onBuildKnex/runBefore/runAfter/onError提供了完整的 SQL 生命周期扩展点,page/range/resultSize覆盖了主流分页需求,has*/is*内省方法让通用工具代码得以实现,而resolve/reject/skipUndefined则在测试与容错场景中非常实用。配合 find-methods.md、eager-methods.md 与 other-methods.md 等 API 文档阅读,可以完整掌握 QueryBuilder 的全部能力。
- 数据库
- 后端
【免费下载链接】objection.js
An SQL-friendly ORM for Node.js
相关推荐
objection.js 模型静态方法完全指南:query、relatedQuery、事务、钩子与工具方法详解
objection.js 模型静态方法完全指南:query、relatedQuery、事务、钩子与工具方法详解 导读 Model 静态方法是 objection
数据库后端MikroORM QueryBuilder 完全指南:从原生 SQL 构造到高级子查询与锁机制
MikroORM QueryBuilder 完全指南:从原生 SQL 构造到高级子查询与锁机制 本篇技术指南以 MikroORM v5.9 官方文档 query
后端objection.js QueryBuilder 数据变更方法完全指南:insert、patch、update、delete 与关系挂接操作详解
objection.js QueryBuilder 数据变更方法完全指南:insert、patch、update、delete 与关系挂接操作详解 object
数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考