
数据库后端【免费下载链接】objection.jsAn SQL-friendly ORM for Node.js项目地址https://gitcode.com/gh_mirrors/ob/objection.js点击查看免费下载objection.js 的 QueryBuilder 是所有数据读写操作的统一入口其中以insert、patch、update、delete、relate、unrelate、upsertGraph为代表的变更Mutating方法构成了 ORM 写侧能力的核心。本文基于官方 API 文档 doc/api/query-builder/mutate-methods.md 并结合仓库源码完整梳理这些方法的签名、参数、返回值、校验规则与底层实现帮助读者正确选择写操作 API写出既安全又高效的插入、更新与删除代码。一、写操作方法的共同特征在进入具体方法之前先理解 QueryBuilder 写操作的三个底层事实写方法都返回 QueryBuilder 本身用于链式调用查询是 thenable 的await或.then()时才真正执行。所有写方法在 lib/queryBuilder/QueryBuilder.js 中统一通过writeOperation(this, ...)包裹将具体行为委托给对应的 Operation 类InsertOperation、UpdateOperation、DeleteOperation等位于 lib/queryBuilder/operations/。写操作同样触发模型生命周期钩子。例如InsertOperation会依次调用实例级$beforeInsert、静态beforeInsert、实例级$afterInsert、静态afterInsert见 lib/queryBuilder/operations/InsertOperation.jsUpdateOperation与DeleteOperation也分别有对应的 before/after 钩子调用。这意味着自定义逻辑如生成时间戳、审计日志可以统一放在钩子中与本文所有写方法无缝配合。写方法前会进行 JSON Schema 校验。模型通过jsonSchema静态属性声明校验规则见 doc/api/model/static-properties.md校验失败时 Promise 以 ValidationError 拒绝。二、insert()创建插入查询queryBuilder queryBuilder.insert(modelsOrObjects);insert创建一个 SQL 插入查询。插入的对象会依据模型的jsonSchema进行校验校验失败则 Promise 以ValidationError拒绝。参数表参数类型说明modelsOrObjectsObject | Model | Object[] | Model[]待插入的对象可传单个或数组返回值QueryBuilderthis供链式调用。返回值只包含“输入属性 标识符”insert返回的结果只包含你传给 insert 的属性加上主键因为 objection 在插入后不会额外发起一次 fetch 查询。源码中 InsertOperation.onBuildKnex 的注释印证了这一设计如果用户没有指定returning子句我们确保至少返回标识符。对应地在 Postgres 上可以链式调用returning(*)取回全部属性returning-tricks 配方 中有更多示例。如果使用returning([only, some, props])结果对象仍会包含输入属性加上returning中列出的属性。在其他数据库上可以使用insertAndFetch方法见下文达到同样效果。const jennifer await Person.query().insert({ firstName: Jennifer, lastName: Lawrence }); console.log(jennifer.id);批量插入仅 Postgres 完全可靠批量插入只在 Postgres 上可靠因为 Postgres 是唯一能返回所有插入行标识符的数据库引擎。knex 在其他数据库上也支持批量插入但只会返回第一个或最后一个对象的 id。如果需要在其他数据库上批量插入可以直接通过knexQuery使用 knex。从源码看InsertOperation.onBefore2 在非 Postgres 且非 SQL Server 的数据库上直接抛出batch insert only works with Postgresql and SQL Server——也就是说实现层面同时放行了 SQL Server但文档明确推荐以 Postgres 为准其他数据库请走 knex 通道。const actors await Movie.relatedQuery(actors) .for(someMovie) .insert([ { firstName: Jennifer, lastName: Lawrence }, { firstName: Bradley, lastName: Cooper } ]); console.log(actors[0].firstName); console.log(actors[1].firstName);raw 表达式与子查询作为值insert的值可以是 raw 表达式与子查询const { raw } require(objection); await Person.query().insert({ age: Person.query().avg(age), firstName: raw(Jenni || fer) });多对多关系 extras 自动写入 join 表在 relationMappings 中被标记为extras的多对多关系字段会自动写入 join 表而不是目标表。下面的someExtra字段只有在关系映射的extra数组中包含someExtra时才会被写入 join 表详见 extra-properties 配方。const jennifer await Movie.relatedQuery(actors) .for(someMovie) .insert({ firstName: Jennifer, lastName: Lawrence, someExtra: Ill be written to the join table }); console.log(jennifer.someExtra);底层实现要点插入前对象先经ensureModelArray转成模型实例再通过$toDatabaseJson转成数据库 JSON 写入knexBuilder.insert(...)除 SQLite/MySQL 外若未显式指定returning会自动追加returning(模型主键列)保证拿到 id插入返回的 id 会被回填到模型实例若模型尚无 id见 InsertOperation.onAfter1单个对象插入返回该模型实例数组插入返回模型数组。三、insertAndFetch()插入后回读queryBuilder queryBuilder.insertAndFetch(modelsOrObjects);与insert完全一致但插入后会额外 fetch 一次把数据库中的完整记录取回。注意在 Postgres 上直接给普通insert链上returning(*)即可达到同样效果且不多发一条查询示例见 returning-tricks 配方。参数与返回值同insert。实现细节从 InsertAndFetchOperation 源码可见它委托给InsertOperation完成插入后收集所有新模型的 id通过findByIds(ids)回查数据库再把取回的新鲜值$set回输入模型并返回——因此你拿到的是“带全量数据库属性”的模型实例。四、insertGraph()图插入queryBuilder queryBuilder.insertGraph(graph, options);insertGraph将一棵对象/模型图含嵌套关联对象一次性插入数据库适合“一次创建整棵关联数据”的场景详见指南中的 graph inserts 章节。参数表参数类型说明graphObject | Model | Object[] | Model[]待插入的对象图optionsInsertGraphOptions可选配置返回值QueryBuilder。对应实现位于 InsertGraphOperation图遍历与插入逻辑在 lib/queryBuilder/graph/insert/ 下的GraphInsert、GraphInsertAction、JoinRowGraphInsertAction等类中完成——JoinRowGraphInsertAction负责处理多对多关系 join 行的生成与上文“extras 写入 join 表”的机制同源。五、insertGraphAndFetch()图插入后回读与insertGraph完全一致但插入后会从数据库回读整张图。与insertAndFetch同理Postgres 上可以直接给普通insertGraph链上returning(*)得到相同结果而无需额外查询。六、patch()部分更新最常用queryBuilder queryBuilder.patch(modelOrObject);patch创建一个 SQLupdate查询。patch 对象会依据模型的jsonSchema校验但jsonSchema中的required属性会被忽略——patch 对象里出现的属性仍然逐个校验但不会因为缺少必需属性而报错。校验失败时 Promise 以ValidationError拒绝。返回值更新操作返回受影响的行数。若想更新单行并直接取回更新后的行使用patchAndFetchByIdPostgres 用户也可参考 returning-tricks 配方。官方建议原文 Tippatch生成的是 SQLupdate查询。虽然还有一个update方法但大多数时候更新数据应该用patch。请仔细阅读两个方法的文档如果不确定或懒得读文档就用patch来更新数据。官方警告原文 Warningpatch 对象中的 raw、lit、子查询以及其他“查询属性”不会被校验使用 FieldExpression 指定的字段同样不参与校验。参数表参数类型说明modelOrObjectObject | Modelpatch 对象示例单行、多行与原子自增// 更新单行按 id 更新年龄 const numberOfAffectedRows await Person.query() .patch({ age: 24 }) .findById(personId); console.log(numberOfAffectedRows); // 批量更新所有 50 岁以下的人 age 改为 20 const numberOfAffectedRows await Person.query() .patch({ age: 20 }) .where(age, , 50); // 原子自增无需先读后写 const numberOfAffectedRows await Person.query() .patch({ age: raw(age 1) }) .where(age, , 50);示例raw、子查询、ref() 与嵌套 JSON 更新const { ref, raw } require(objection); await Person.query().patch({ age: Person.query().avg(age), // 也可以用 knex.raw 替代 raw() firstName: raw(Jenni || fer), oldLastName: ref(lastName), // 更新 json 列 detailsJsonColumn 深层嵌套的值 detailsJsonColumn:address.street: Elm street });底层实现要点patch在 QueryBuilder.patch 中通过_patchOperationFactory创建操作实际使用InstanceUpdateOperation带 id 条件或UpdateOperation派生的更新操作嵌套 JSON 字段表达式如detailsJsonColumn:address.street在 UpdateOperation.convertFieldExpressionsToRaw 中被转换为jsonb_set(detailsJsonColumn, {address,street}, ?, true)形式的 raw SQL仅 Postgres实现“只更新 JSON 的某个深层子路径”而不覆盖整列若 patch 对象为空$toDatabaseJson后无任何列操作会直接builder.resolve(0)结束查询不执行无意义的 SQL见 UpdateOperation.onBefore3。七、patchAndFetchById() 与 patchAndFetch()更新单行并回读queryBuilder queryBuilder.patchAndFetchById(id, modelOrObject); queryBuilder queryBuilder.patchAndFetch(modelOrObject);patchAndFetchById相当于“单行patch 更新后回读数据库”。patchAndFetch与其行为一致但用于实例的$query场景无需再指定 idid 已存在于实例中。参数表patchAndFetchById参数类型说明idany要更新项的标识符可以是复合主键modelOrObjectObject | Modelpatch 对象const updatedPerson await Person.query().patchAndFetchById(134, { age: 24 }); console.log(updatedPerson.firstName);实例用法const jennifer await Person.query().findOne({ firstName: Jennifer }); const updatedJennifer await jennifer.$query().patchAndFetch({ age: 24 }); console.log(updatedJennifer.firstName);实现细节patchAndFetchById与patchAndFetch都委托给 UpdateAndFetchOperation。该操作先为构建器加上findById(id)条件执行更新若numUpdated 0则直接返回undefined否则再发一条查询取回最新行并$set到输入模型上返回。patchAndFetch走实例路径时设置skipIdWhere true因为实例更新操作已自带where id ?条件见 QueryBuilder.patchAndFetch。八、update()整行更新queryBuilder queryBuilder.update(modelOrObject);update创建 SQLupdate查询适用于整行所有列都要更新的场景。update 对象同样按jsonSchema校验校验失败则以ValidationError拒绝。update与patch的关键区别原文明确说明update校验时尊重schema 的required属性——缺少任一必需属性都会抛ValidationErrorpatch忽略required属性只校验对象中实际出现的字段。对比维度patchupdate适用场景只更新部分列更新整行所有列required 校验忽略不强制强制执行缺失即报错返回值受影响行数受影响行数官方建议大多数更新首选仅在整行更新时使用返回值是受影响行数要更新单行并取回结果用updateAndFetchByIdPostgres 用户可参考 returning-tricks 配方。const numberOfAffectedRows await Person.query() .update({ firstName: Jennifer, lastName: Lawrence, age: 24 }) .where(id, 134); console.log(numberOfAffectedRows);raw 表达式、子查询与ref()同样可以作为值const { raw, ref } require(objection); await Person.query().update({ firstName: raw(Jenni || fer), lastName: Lawrence, age: Person.query().avg(age), oldLastName: ref(lastName) // 等价于 knex.raw(??, [lastName]) });还可以引用 json 列内部的属性仅 Postgresawait Person.query().update({ lastName: ref(someJsonColumn:mother.lastName).castText(), detailsJsonColumn:address.street: Elm street });实现要点update与patch共用 UpdateOperation区别只在于创建时使用的“更新工厂”不同_updateOperationFactoryvs_patchOperationFactory校验配置与钩子传参$beforeUpdate的modelOptions因此不同——这正是两者required校验行为差异的根源。九、updateAndFetchById() 与 updateAndFetch()queryBuilder queryBuilder.updateAndFetchById(id, modelOrObject); queryBuilder queryBuilder.updateAndFetch(modelOrObject);与patchAndFetchById/patchAndFetch对称updateAndFetchById是“单行整列更新 回读”updateAndFetch用于实例$query无需指定 id。底层同样走UpdateAndFetchOperation只是委托对象由patch操作换成update操作见 QueryBuilder.updateAndFetchById。const updatedPerson await Person.query().updateAndFetchById(134, person); console.log(updatedPerson.firstName);实例用法const jennifer await Person.query().findOne({ firstName: Jennifer }); const updatedJennifer await jennifer.$query().updateAndFetch({ age: 24 }); console.log(updatedJennifer.firstName);十、upsertGraph() 与 upsertGraphAndFetch()图级“有则更新、无则插入”queryBuilder queryBuilder.upsertGraph(graph, options); queryBuilder queryBuilder.upsertGraphAndFetch(graph, options);upsertGraph对一棵对象图执行“存在则更新、不存在则插入”的递归操作upsertGraphAndFetch在操作完成后额外从数据库回读整张图。详细用法见指南的 graph upserts 章节。对应实现位于 UpsertGraphOperation递归执行逻辑在 lib/queryBuilder/graph/recursiveUpsert/ 下。参数表参数类型说明graphObject | Model | Object[] | Model[]待 upsert 的对象图optionsUpsertGraphOptions可选配置官方严重警告原文完整引用务必阅读WARNING!在你开始使用upsertGraph之前要当心它并不是看起来那样的银弹。如果你因为它似乎为关系数据库提供了“mongodb API”而开始使用它那你使用它的理由就是错误的我们的建议是先尝试不用它写任何代码只有当upsertGraph能帮你节省大量代码并让事情更简单时才使用它。随着时间推移你会学到upsertGraph在哪些场景有帮助、哪些场景让事情更复杂。不要默认什么都用它。你可以翻翻 objection 的 issues看看过度使用upsertGraph会引发哪些问题。对简单场景而言upsertGraph调用容易理解且保持可读。当你开始给它传一堆 options 时其他开发者甚至你自己将越来越难理解它。过度使用upsertGraph还很容易写出在多用户场景下工作不佳的服务——因为如果你总是一次性 upsert 大图很容易覆盖其他用户的修改。永远尽量只更新最小数量的行和列长期来看会省去你大量麻烦。十一、delete()删除查询queryBuilder queryBuilder.delete();delete创建 SQL 删除查询。返回值是删除的行数Postgres 用户想取回被删的行可参考 returning-tricks 配方。也可配合deleteById使用。返回值QueryBuilder。注意delete()不接受任何参数需像普通查询一样用.where(...)等条件过滤。从源码看QueryBuilder.delete 在传入参数时会直接抛出错误提示Dont pass arguments to delete(). You should use it like this: delete().where(foo, bar).andWhere(...)。另提供别名del()。const numberOfDeletedRows await Person.query() .delete() .where(age, , 100); console.log(removed, numberOfDeletedRows, people);用子查询替代 join数据库限制删除查询可以使用子查询和所有查询构建方法。部分数据库不允许在 delete 中使用 join这是数据库限制不是 objection 的限制可以用子查询替代// 删除所有养了一只叫 Fluffy 的宠物的人 await Person.query() .delete() .whereIn( id, Person.query() .select(persons.id) .joinRelated(pets) .where(pets.name, Fluffy) ); // 实现同一查询的另一种方式 await Person.query() .delete() .whereExists(Person.relatedQuery(pets).where(pets.name, Fluffy));与实例查询配合delete当然也可以与$relatedQuery和$query一起使用const person await Person.query().findById(personId); // 删除某人的除猫、狗以外的所有宠物 await person .$relatedQuery(pets) .delete() .whereNotIn(species, [cat, dog]); // 删除某人的所有宠物 await person.$relatedQuery(pets).delete();实现要点DeleteOperation 内部调用knexBuilder.delete()并依次触发静态beforeDelete/afterDelete钩子$beforeDelete/$afterDelete实例钩子由相关操作在模型层调用返回值即受影响行数。十二、deleteById()按 id 删除queryBuilder queryBuilder.deleteById(id);按 id 删除一个条目返回值是删除的行数Postgres 用户想取回被删行参考 returning-tricks 配方。参数表参数类型说明idany | any[]要删除的 id。数组用于复合主键。注意此方法不接受多个标识符const numberOfDeletedRows await Person.query().deleteById(1); console.log(removed, numberOfDeletedRows, people);复合主键删除把各键值组成数组传入const numberOfDeletedRows await Person.query().deleteById([10, 20, 46]); console.log(removed, numberOfDeletedRows, people);十三、relate()挂接已存在的关联queryBuilder queryBuilder.relate(ids);relate将一个已存在的条目通过关系挂接到另一个条目上。它不会创建新条目只更新外键对于多对多关系则是在 join 表中创建一行关联记录。在 Postgres 上可以通过传标识符数组一次性挂接多个条目。返回值是受影响条目的数量。参数表参数类型说明idsnumber | string | Array | Object要挂接的模型标识符可传单个、数组或对象返回值QueryBuilder。示例与生成的 SQL下面的例子把一位演员挂接到一部电影上此处Person与Movie是多对多关系但relate对所有关系类型都适用const actor await Person.query().findById(100);select persons.* from persons where persons.id 100const movie await Movie.query().findById(200);select movies.* from movies where movies.id 200await actor.$relatedQuery(movies).relate(movie);insert into persons_movies (personId, movieId) values (100, 200)也可以直接把 id200传给relate而不传模型实例。更“objection 风格”的写法是利用静态relatedQueryawait Person.relatedQuery(movies) .for(100) .relate(200);insert into persons_movies (personId, movieId) values (100, 200)下面这个例子给第一个名叫 Arnold 的人挂接四部电影。注意此查询只在 Postgres 上可用因为其他数据库需要多条查询await Person.relatedQuery(movies) .for( Person.query() .where(firstName, Arnold) .limit(1) ) .relate([100, 200, 300, 400]);relate返回受影响行数const numRelatedRows await person .relatedQuery(movies) .for(123) .relate(50); console.log(movie 50 is now related to person 123 through movies relation);批量挂接仅 Postgresconst numRelatedRows await Person.relatedQuery(movies) .for(123) .relate([50, 60, 70]); console.log(${numRelatedRows} rows were related);复合主键可以传标识符数组也可以用对象形式const numRelatedRows await Person.relatedQuery(movies) .for(123) .relate({ foo: 50, bar: 20, baz: 10 }); console.log(${numRelatedRows} rows were related);多对多关系中标记为 extras 的字段会自动写入 join 表。下面的someExtra字段会在关系映射extra数组包含someExtra时写入 join 表const numRelatedRows await Movie.relatedQuery(actors) .for(movieId) .relate({ id: 50, someExtra: Ill be written to the join table }); console.log(${numRelatedRows} rows were related);实现要点relate由 RelateOperation 实现并根据关系类型委托到不同实现——多对多走 lib/relations/manyToMany/relate/ManyToManyRelateOperation.js向 join 表插入行并处理extras列belongsToOne/hasOne/hasMany 则走对应的外键更新操作见 lib/relations/ 下各关系目录的 relate 操作。十四、unrelate()解除挂接queryBuilder queryBuilder.unrelate();unrelate移除两个条目之间的连接。它不会删除条目本身只移除连接对于多对多关系删除 join 表中的对应行对于其他关系类型把连接列置为 null。重要区别与relate不同不要给unrelate传任何参数。像使用delete一样使用unrelate用返回的查询构建器通过.where(...)过滤要解除的行。返回值是受影响条目的数量。返回值QueryBuilder。示例与生成的 SQLconst actor await Person.query().findById(100);select persons.* from persons where persons.id 100await actor .$relatedQuery(movies) .unrelate() .where(name, like, Terminator%);delete from persons_movies where persons_movies.personId 100 where persons_movies.movieId in ( select movies.id from movies where name like Terminator% )使用静态relatedQuery的等价写法await Person.relatedQuery(movies) .for(100) .unrelate() .where(name, like, Terminator%);delete from persons_movies where persons_movies.personId 100 and persons_movies.movieId in ( select movies.id from movies where name like Terminator% )下面的查询移除 Arnold Schwarzenegger 的全部《终结者》电影关联。注意这里没有 awaitarnold查询——它不会执行只是作为占位符在relatedQuery执行时用来构建子查询const arnold Person.query().findOne({ firstName: Arnold, lastName: Schwarzenegger }); await Person.relatedQuery(movies) .for(arnold) .unrelate() .where(name, like, Terminator%);delete from persons_movies where persons_movies.personId in ( select persons.id from persons where firstName Arnold and lastName Schwarzenegger ) and persons_movies.movieId in ( select movies.id from movies where name like Terminator% )unrelate返回受影响行数const person await Person.query().findById(123); const numUnrelatedRows await person .$relatedQuery(movies) .unrelate() .where(id, 50); console.log( movie 50 is no longer related to person 123 through movies relation );实现要点与relate对称unrelate由 UnrelateOperation 实现多对多场景委托给 lib/relations/manyToMany/unrelate/ 下的操作含 SQLite 专用变体ManyToManyUnrelateSqliteOperation单侧关系则把外键置空。同样地unrelate()传参会被 QueryBuilder.unrelate 拒绝并抛出错误。十五、knex 透传方法increment / decrement / truncate / onConflict / ignore / merge以下方法由 knex 提供objection 的 QueryBuilder 直接透传QueryBuilder 本质上是 knex QueryBuilder 的包装见 doc/api/query-builder/README.md使用时请查阅 knex 文档方法用途increment(column, amount)对数值列做增量更新decrement(column, amount)对数值列做减量更新truncate()清空表onConflict(columns)冲突处理常配合merge/ignore使用ignore()冲突时忽略插入merge()冲突时合并更新这些方法返回QueryBuilder可继续链式调用。注意与原生patch({ age: raw(age 1) })相比increment/decrement是 knex 直接提供的原子增量语法糖可按需选用。十六、总结与实践建议把本文的写操作 API 按场景归拢新增数据单条/多条用insertPostgres 批量插入最可靠需要回读用insertAndFetch或 Postgresreturning(*)整棵关联图用insertGraph/insertGraphAndFetch。更新数据默认用patch忽略required、只校验出现的字段整行全列更新才用update两者需要回读时分别对应patchAndFetch(ById)与updateAndFetch(ById)。删除数据delete()不带参数配合where过滤与deleteById(id)支持复合主键数组。关系维护relate(ids)挂接已存在条目Postgres 支持数组与 join 表 extrasunrelate()解除连接不传参、用where过滤。图级 upsertupsertGraph功能强大但务必先阅读文档中的严重警告仅在确实能大幅简化代码时使用并坚持“只更新最小数量的行和列”以规避多用户并发覆盖问题。如需进一步深入建议继续阅读完整的 QueryBuilder 文档、模型定义与校验、returning 技巧、多对多 extras 属性以及仓库中的集成测试用例如 tests/integration/insert.js、tests/integration/patch.js、tests/integration/relate.js、tests/integration/upsertGraph.js——这些测试覆盖了各写方法在多种数据库下的真实行为是验证本文所述细节的最佳实证材料。赞分享数据库后端【免费下载链接】objection.jsAn SQL-friendly ORM for Node.js项目地址https://gitcode.com/gh_mirrors/ob/objection.js点击查看免费下载相关推荐Squirrel数据操作INSERT、UPDATE、DELETE详解Squirrel数据操作INSERT、UPDATE、DELETE详解 本文详细介绍了Squirrel框架中InsertBuilder的多值插入与批量操作、Up后端数据库SQLAlchemy 数据操作实战从 INSERT、SELECT 到 UPDATE 与 DELETE 的完整指南SQLAlchemy 数据操作实战从 INSERT、SELECT 到 UPDATE 与 DELETE 的完整指南 本指南以 SQLAlchemy 官方教程 数据库后端ORMApache DataFusion 数据操纵语言DML完全指南COPY、INSERT、DELETE 与 UPDATEApache DataFusion 数据操纵语言DML完全指南COPY、INSERT、DELETE 与 UPDATE 本指南系统讲解 Apache Dat大数据数据分析后端上一篇深度解析DeepEval SummaC模型实战指南 - 5步构建专业级文本一致性检测系统下一篇终极指南如何用LunaTranslator轻松突破视觉小说语言障碍创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考