ARTICLE DETAIL

资讯详情

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

Objection.js QueryBuilder 其他实用方法完全指南:上下文、钩子、分页与查询内省

Objection.js QueryBuilder 其他实用方法完全指南:上下文、钩子、分页与查询内省 数据库后端【免费下载链接】objection.jsAn SQL-friendly ORM for Node.js项目地址https://gitcode.com/gh_mirrors/ob/objection.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时每一条都会被打印非常适合排查 N1 与慢查询。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 与 hasXxxObjection.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、avgDistincthasWithGraph()是否已调用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 层修改 SQLqueryBuilder queryBuilder.onBuildKnex(onBuildKnex);与onBuild的执行时机相同都在 SQL 构建阶段位于onBuild之后、runAfter之前区别在于此时 objection builder已经被编译成 knex query builderonBuildKnex收到的参数是(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.castToSomeType();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) .castToNamed[](); 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时会产生多少行。注意它执行的是查询的一个副本并返回Promisenumber。相比返回对象数组的countresultSize直接给出数字往往更方便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);以“页码 页大小”的方式分页第一页索引为 0const 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阶段克隆一个resultSizeBuilderonAfter3阶段执行克隆查询得到total最终返回{ results, total }结构见 RangeOperation.js。execute()、then()、catch()、bind() 与 clone()执行与复制const promise queryBuilder.execute();execute()执行查询并返回 Promiseresolve 为查询结果。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.jsAn SQL-friendly ORM for Node.js项目地址https://gitcode.com/gh_mirrors/ob/objection.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数据库后端上一篇KMS_VL_ALL_AIO 使用完整指南一个批处理文件搞定 Windows 与 Office 免费激活下一篇KMS激活工具完全实战指南一个脚本免费搞定Windows与Office全系列激活创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表