Laf 云数据库操作符完全指南从查询指令到更新指令的 database-ql 实战手册【免费下载链接】lafLaf is a vibrant cloud development platform that provides essential tools like cloud functions, databases, and storage solutions. It enables developers to quickly unleash their creativity and bring innovative ideas to life with ease.项目地址: https://gitcode.com/GitHub_Trending/la/lafLaf 云开发平台内置的database-ql数据库查询语言为云函数提供了一套声明式、可组合、贴近 MongoDB 语义的数据库操作符体系。本文以db.command即_为入口系统梳理查询逻辑、比较、字段、数组、地理位置与表达式操作符以及更新类字段、数组操作符的完整用法与底层编码原理帮助你在 Laf 云函数中写出精确、安全、高性能的数据读写代码。操作符概览与初始化Laf 云函数云开发平台的服务端环境内可直接通过cloud.database()获取数据库引用操作符统一挂载在db.command上惯例命名为_const db cloud.database() const _ db.command // 后续操作 用 _ 即可db数据库引用用于collection()、doc()、Geo等入口_命令集合用于构造查询条件与更新表达式。从源码结构看_的实际类型在 packages/database-ql/src/command.ts 中定义为Command对象它内部将操作符划分为三类命令类| 命令类 | 覆盖操作符 | 源码位置 | | :- | :- | :- | |LogicCommand逻辑 |and/or/not/nor| commands/logic.ts | |QueryCommand查询 |eq/neq/lt/lte/gt/gte/in/nin/all/elemMatch/exists/size/mod/geoNear/geoWithin/geoIntersects/like| commands/query.ts | |UpdateCommand更新 |set/remove/inc/mul/push/pull/pullAll/pop/shift/unshift/addToSet/rename/max/min/bit| commands/update.ts |这些命令类最终由序列化器QuerySerializer/UpdateSerializer见 serializer/query.ts 与 serializer/update.ts编码为以$前缀开头的 MongoDB 风格条件映射关系集中在 operator-map.ts默认规则是$ 操作符名例外包括neq - $ne、remove - $unset、shift - $pop、unshift - $push。查询·逻辑操作符逻辑操作符用于组合多个筛选条件是构建复杂where的基础。and逻辑与and表示需同时满足多个查询筛选条件有两种使用场景。1. 用在根查询条件跨字段传入多个完整查询条件对象要求同时满足const _ db.command let res await db.collection(todo).where(_.and([ { progress: _.gt(50) }, { tags: cloud } ])).get()但上述写法其实是非必要的——where传入的对象各字段隐式组成与关系等价于更简洁的写法const _ db.command let res await db.collection(todo).where({ progress: _.gt(50), tags: cloud }).get()通常需要显式使用and的场景是跨字段或跨操作的组合如配合_.or、_.nor做分组。2. 用在字段查询条件传入多个操作符或常量表示字段需同时满足给定条件。前置写法const _ db.command let res await db.collection(todo).where({ progress: _.and(_.gt(50), _.lt(100)) }).get()后置链式写法const _ db.command let res await db.collection(todo).where({ progress: _.gt(50).and(_.lt(100)) }).get()更进一步的简化QueryCommand默认链式调用其他Command时即表示与关系源码见 commands/query.tsgt/gte/lt/lte等方法均通过this.and(command)组合因此上述代码可精简为const _ db.command let res await db.collection(todo).where({ progress: _.gt(50).lt(100) }).get()从序列化角度链式_.gt(50).lt(100)会先被编码为$and: [{$gt: 50}, {$lt: 100}]再经mergeConditionAfterEncode合并为同一字段下的{$gt: 50, $lt: 100}参见 serializer/query.ts 的注释示例。or逻辑或or表示或关系支持字段值的或与跨字段的或两种用法。字段值的或操作指定字段值为多个值之一即可。筛选进度大于 80 或小于 20 的 todo// 流式写法 const _ db.command let res await db.collection(todo).where({ progress: _.gt(80).or(_.lt(20)) }).get() // 前置写法 let res await db.collection(todo).where({ progress: _.or(_.gt(80), _.lt(20)) }).get() // 前置写法传数组 let res await db.collection(todo).where({ progress: _.or([_.gt(80), _.lt(20)]) }).get()跨字段的或操作相当于传入多个where语句满足其中一个即可。筛选进度大于 80 或已标记完成的 todoconst _ db.command let res await db.collection(todo).where(_.or([ { progress: _.gt(80) }, { done: true } ])).get()需要说明的是and/or/nor/not在 command.ts 中均支持接收单个数组参数或可变参数两种形式源码通过isArray(arguments[0])判断所以上面数组写法与多参数写法完全等价。not逻辑非not表示需不满足指定条件。筛选进度不等于 100 的 todoconst _ db.command let res await db.collection(todo).where({ progress: _.not(_.eq(100)) }).get()not可搭配其他逻辑指令使用包括and、or、nor、not。例如排除进度小于 50 或等于 100 的记录const _ db.command let res await db.collection(todo).where({ progress: _.not(_.or([_.lt(50), _.eq(100)])) }).get()nor逻辑都不nor表示需不满足指定的所有条件若记录中不存在对应字段则默认满足条件这是与and/or组合时最容易踩的坑。示例 1筛选进度既不小于 20 又不大于 80 的 todoconst _ db.command let res await db.collection(todo).where({ progress: _.nor([_.lt(20), _.gt(80)]) }).get()以上会同时筛选出不存在progress字段的记录。若要求progress字段必须存在可叠加exists指令const _ db.command let res await db.collection(todo).where({ progress: _.exists(true).nor([_.lt(20), _.gt(80)]) // 等价于以下非链式调用的写法 // progress: _.exists(true).and(_.nor([_.lt(20), _.gt(80)])) }).get()示例 2筛选progress不小于 20 且tags数组不包含miniprogram的记录const _ db.command db.collection(todo).where(_.nor([{ progress: _.lt(20), }, { tags: miniprogram, }])).get()该查询会筛选出满足以下条件之一的记录progress不小于 20 且tags数组不包含miniprogramprogress不小于 20 且tags字段不存在progress字段不存在且tags数组不包含miniprogramprogress不小于 20 且tags字段不存在原文记录项从nor语义看为所有条件均不满足的兜底情形。如果要求progress与tags字段都必须存在则在nor结果之上叠加and与existsconst _ db.command let res await db.collection(todo).where( _.nor([{ progress: _.lt(20), }, { tags: miniprogram, }]) .and({ progress: _.exists(true), tags: _.exists(true), }) ).get()查询·比较操作符比较操作符用于对字段值做大小、相等、包含等判定。文档中eq与neq的接受参数说明为number、boolean、string、object、array、Date。eq等于eq接受一个字面量literal用于字段等于某个值。除直接传对象外也可以用指令形式例如按_openid筛选自己的文章const _ db.command const openID xxx let res await db.collection(articles).where({ _openid: _.eq(openID) }).get()eq相比直接传对象有更大的灵活性——它可以表示字段等于某个对象的完整匹配。以下两种写法语义不同// 这种写法表示匹配 stat.publishYear 2018 且 stat.language zh-CN部分字段匹配 let res await db.collection(articles).where({ stat: { publishYear: 2018, language: zh-CN } }).get() // 这种写法表示 stat 对象整体等于 { publishYear: 2018, language: zh-CN }全等匹配 const _ db.command let res await db.collection(articles).where({ stat: _.eq({ publishYear: 2018, language: zh-CN }) }).get()从编码实现看eq会被序列化为$eqoperator-map.ts并以$eq形式下发给数据库执行。neq不等于neq表示字段不等于某个值用法与eq相反const _ db.command let res await db.collection(articles).where({ _openid: _.neq(openID) }).get()编码映射上neq会转换为 MongoDB 的$ne见 operator-map.ts。lt / lte / gt / gte大小比较四个比较操作符分别表示小于、小于等于、大于、大于等于均可传入Date对象做日期比较const _ db.command // 进度小于 50 let res await db.collection(todos).where({ progress: _.lt(50) }).get() // 进度小于或等于 50 let res await db.collection(todos).where({ progress: _.lte(50) }).get() // 进度大于 50 let res await db.collection(todos).where({ progress: _.gt(50) }).get() // 进度大于或等于 50 let res await db.collection(todos).where({ progress: _.gte(50) }).get()in / nin数组包含in要求值在给定数组内nin要求值不在给定数组内const _ db.command // 进度为 0 或 100 let res await db.collection(todos).where({ progress: _.in([0, 100]) }).get() // 进度不是 0 也不是 100 let res await db.collection(todos).where({ progress: _.nin([0, 100]) }).get()序列化时in/nin与其他操作符不同直接以整个数组作为$in/$nin的操作数见 commands/query.ts 的toJSON与 serializer/query.ts。查询·字段操作符exists字段存在性判断字段是否存在true为存在false为不存在const _ db.command // 找出存在 tags 字段的记录 let res await db.collection(todos).where({ tags: _.exists(true) }).get()该操作符常用于nor等场景中收紧匹配范围见上文逻辑操作符示例。mod取模给定除数divisor和余数remainder要求字段作为被除数时value % divisor remainderconst _ db.command // 找出进度为 10 的倍数的记录 let res await db.collection(todos).where({ progress: _.mod(10, 0) }).get()查询·数组操作符all包含全部元素all用于数组字段要求数组中包含给定数组的所有元素。普通数组找出tags同时包含cloud和database的记录const _ db.command let res await db.collection(todos).where({ tags: _.all([cloud, database]) }).get()对象数组数组元素是对象时可用_.elemMatch匹配对象的部分字段。假设字段places的元素结构为{ type: string area: number age: number }找出数组中至少同时包含一个满足area 大于 100 且 age 小于 2的元素以及一个满足type 为 mall 且 age 大于 5的元素const _ db.command let res await db.collection(todos).where({ places: _.all([ _.elemMatch({ area: _.gt(100), age: _.lt(2), }), _.elemMatch({ type: mall, age: _.gt(5), }), ]), }) .get()elemMatch数组内条件匹配elemMatch要求数组中包含至少一个满足给定所有条件的元素。对象数组场景假设集合示例数据{ _id: a0, city: x0, places: [{ type: garden, area: 300, age: 1 }, { type: theatre, area: 50, age: 15 }] }找出places中至少存在一个area 大于 100 且 age 小于 2的元素const _ db.command let res await db.collection(todos).where({ places: _.elemMatch({ area: _.gt(100), age: _.lt(2), }) }).get()注意如果不使用elemMatch而直接以对象形式指定条件语义会变为places中至少有一个元素的area大于 100且places中至少有一个元素的age小于 2两个条件可能分别命中不同元素const _ db.command let res await db.collection(todos).where({ places: { area: _.gt(100), age: _.lt(2), } }).get()普通数据类型数组场景假设数据{ _id: a0, scores: [60, 80, 90] }找出scores中至少一个元素大于 80 且小于 100const _ db.command let res await db.collection(todos).where({ scores: _.elemMatch(_.gt(80).lt(100)) }).get()size数组长度要求数组字段长度为给定值const _ db.command // 找出 tags 数组字段长度为 2 的所有记录 let res await db.collection(todos).where({ places: _.size(2) }).get()查询·地理位置操作符地理操作符与db.Geo系列类型Point、LineString、Polygon等见 geo配合使用。使用前需对查询字段建立地理位置索引。geoNear距离附近查询按从近到远的顺序找出字段值在给定点附近、指定距离范围内的记录const _ db.command // 找出离给定位置 1 公里到 5 公里范围内的记录 let res await db.collection(restaurants).where({ location: _.geoNear({ geometry: new db.Geo.Point(113.323809, 23.097732), minDistance: 1000, maxDistance: 5000, }) }).get()从源码看geoNear有严格的参数校验geometry必须是Point类型maxDistance/minDistance必须是数字否则抛出TypeErrorcommands/query.ts序列化时被编码为$nearSphereserializer/query.ts。geoWithin区域内查询无排序找出字段值在指定区域内的记录无排序。区域必须是多边形Polygon或多边形集合MultiPolygon源码中对此有类型校验commands/query.ts。示例 1给定多边形const _ db.command const { Point, LineString, Polygon } db.Geo let res await db.collection(restaurants).where({ location: _.geoWithin({ geometry: new Polygon([ new LineString([ new Point(0, 0), new Point(3, 2), new Point(2, 3), new Point(0, 0) ]) ]), }) }).get()示例 2给定圆形可不传geometry改用centerSphere构建圆形。其值定义为[ [经度, 纬度], 半径 ]半径以弧度计如需要 10km 半径用距离除以地球半径 6378.1km 得到弧度值const _ db.command let res await db.collection(restaurants).where({ location: _.geoWithin({ centerSphere: [ [-88, 30], 10 / 6378.1, ] }) }).get()geoIntersects几何相交查询找出与给定地理位置图形相交的记录。geometry支持Point、LineString、Polygon、MultiPoint、MultiLineString、MultiPolygon六种类型源码校验见 commands/query.tsconst _ db.command const { Point, LineString, Polygon } db.Geo let res await db.collection(restaurants).where({ location: _.geoIntersects({ geometry: new Polygon([ new LineString([ new Point(0, 0), new Point(3, 2), new Point(2, 3), new Point(0, 0) ]) ]), }) }).get()查询·表达式操作符expr聚合表达式查询expr用于在查询语句中引入聚合表达式接收一个参数该参数必须为聚合表达式通过_.aggregate构建。使用场景在聚合match流水线阶段中引入聚合表达式若聚合match阶段在lookup阶段内expr表达式中可使用lookup中let参数定义的变量详见lookup的指定多个连接条件示例在普通查询语句where中引入聚合表达式。示例 1比较同一记录中的两个字段。假设items集合结构为{ _id: string, inStock: number, // 库存量 ordered: number // 被订量 }找出被订量大于库存量的记录const _ db.command const $ _.aggregate let res await db.collection(items).where(_.expr($.gt([$ordered, $inStock]))).get()示例 2与条件语句组合使用。假设items集合结构为{ _id: string, price: number }假设价格小于等于 10 的打 8 折、大于 10 的打 5 折让数据库返回打折后价格小于等于 8 的记录const _ db.command const $ _.aggregate let res await db.collection(items).where( _.expr( $.lt([ $.cond({ if: $.gte([$price, 10]), then: $.multiply([$price, 0.5]), else: $.multiply([$price, 0.8]), }) , 8 ]) )).get()从实现上看expr直接返回{ $expr: values }command.ts而_.aggregate提供了完整的聚合运算符集合涵盖算数add、multiply、divide、mod、sqrt等 15 个、数组、布尔、比较、条件cond、ifNull、switch、日期、字符串、分组sum、avg、push等与let变量声明等上百个运算符command.ts它们统一被包装为{$名称: 参数}形式的AggregationOperator。聚合流水线最终由 aggregate.ts 通过ActionType.aggregate发送到服务端执行。更新·字段操作符更新操作符用于update()调用中doc(doc-id)指定目标文档。set设定字段值set用于设定字段等于指定值。相比直接传纯 JS 对象它的优势是能够指定字段等于一个对象// 以下方法只会更新 style.color 为 red而不是将 style 更新为 { color: red }即不影响 style 中的其他字段 let res await db.collection(todos).doc(doc-id).update({ style: { color: red } }) // 以下方法更新 style 为 { color: red, size: large }整体替换 let res await db.collection(todos).doc(doc-id).update({ style: _.set({ color: red, size: large }) })从序列化实现看普通对象更新默认也会被包装为$setserializer/update.ts区别在于普通对象会先经flattenQueryObject扁平化形成对嵌套字段如style.color的部分更新而_.set则以整体值直接作为$set的操作数。remove删除字段用于删除某个字段编码时映射为$unset见 operator-map.tsconst _ db.command // 删除 style 字段 let res await db.collection(todos).doc(todo-id).update({ style: _.remove() })inc / mul原子自增 / 原子自乘inc与mul都是原子操作多个用户同时写时数据库对每个请求都执行自增/自乘不会出现后来者覆写前者的丢失更新问题。const _ db.command // 将一个 todo 的进度自增 10 let res await db.collection(todos).doc(todo-id).update({ progress: _.inc(10) }) // 将一个 todo 的进度自乘 10 let res await db.collection(todos).doc(todo-id).update({ progress: _.mul(10) })典型场景点赞数_.inc(1)、库存扣减_.inc(-1)、金额翻倍_.mul(2)等计数器类更新借助原子性天然避免并发覆盖。min / max条件更新min给定一个值仅当该值小于字段当前值时才更新即把字段压低max给定一个值仅当该值大于字段当前值时才更新即把字段抬高const _ db.command // 如果字段 progress 50则更新到 50 let res await db.collection(todos).doc(doc-id).update({ progress: _.min(50) }) // 如果字段 progress 50则更新到 50 let res await db.collection(todos).doc(doc-id).update({ progress: _.max(50) })典型场景max用于记录历史最高分min用于记录历史最低价。rename字段重命名rename用于字段重命名。嵌套深层字段需用点路径表示法不能对嵌套在数组里的对象的字段做重命名。示例 1重命名顶层字段const _ db.command let res await db.collection(todos).doc(doc-id).update({ progress: _.rename(totalProgress) })示例 2重命名嵌套字段两种写法等价const _ db.command // 写法一对象层级写法 let res await db.collection(todos).doc(doc-id).update({ someObject: { someField: _.rename(someObject.renamedField) } }) // 写法二点路径写法 let res await db.collection(todos).doc(doc-id).update({ someObject.someField: _.rename(someObject.renamedField) })更新·数组操作符数组更新操作符用于对值为数组的字段做增删改。其中push支持一组可选的修饰参数position、sort、sliceaddToSet是原子操作pull/pullAll支持按条件批量移除。push尾部/指定位置插入向数组添加一个或多个值若字段原为空则创建该字段并设为传入数组。其参数说明如下position要求必须同时存在each参数。非负数代表从数组头部数的位置从 0 开始若大于等于数组长度则视为尾部添加负数代表从数组尾部倒数如 -1 代表倒数第二个元素位置若绝对值大于等于数组长度则视为从头部添加。sort要求必须同时存在each参数。1升序-1降序数组元素是对象时用{ 字段: 1 | -1 }指定排序字段。slice要求必须同时存在each参数。取值说明值说明0将字段更新为空数组正数数组只保留前 n 个元素负数数组只保留后 n 个元素示例 1尾部添加元素const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push([mini-program, cloud]) })示例 2从第二个位置开始插入const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push({ each: [mini-program, cloud], position: 1, }) })示例 3排序——插入后对整个数组排序const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push({ each: [mini-program, cloud], sort: 1, }) })不插入、只对数组排序each传空数组const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push({ each: [], sort: 1, }) })对象数组可按元素字段排序const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push({ each: [ { name: miniprogram, weight: 8 }, { name: cloud, weight: 6 }, ], sort: { weight: 1, }, }) })示例 4截断保留——插入后只保留后 2 个元素const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push({ each: [mini-program, cloud], slice: -2, }) })示例 5组合使用——在指定位置插入、然后排序、最后只保留前 2 个元素const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.push({ each: [mini-program, cloud], position: 1, slice: 2, sort: 1, }) })从实现看push在传入含each的对象时会把position/sort/slice映射为$position/$sort/$slice修饰符command.ts若传入数组则直接作为$each处理serializer/update.ts。unshift在编码时等价于push且$position: 0。pop / shift删除尾部/头部元素pop删除数组尾部元素仅末尾一个shift删除数组头部元素const _ db.command // 删除尾部元素 let res await db.collection(todos).doc(doc-id).update({ tags: _.pop() }) // 删除头部元素 let res await db.collection(todos).doc(doc-id).update({ tags: _.shift() })实现上两者都映射为 MongoDB 的$pop仅方向不同pop - 1尾shift - -1头见 serializer/update.ts。unshift头部插入往数组头部添加一个或多个值字段原为空则创建字段并设为传入值const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.unshift([mini-program, cloud]) })pull / pullAll按条件移除元素pull给定一个值或一个查询条件移除数组中所有匹配的元素pullAll与pull的区别是只能指定常量值且传入的是数组。示例 1按常量匹配移除const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.pull(database) })示例 2按查询条件匹配移除const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.pull(_.in([database, cloud])) })示例 3对象数组按查询条件匹配移除。假设places数组元素结构为{ type: string area: number age: number }const _ db.command let res await db.collection(todos).doc(doc-id).update({ places: _.pull({ area: _.gt(100), age: _.lt(2), }) })示例 4有嵌套对象的对象数组。假设cities数组元素结构为{ name: string places: Place[] }其中Place结构为{ type: string area: number age: number }可用elemMatch匹配嵌套在对象数组里面的对象数组字段placesconst _ db.command let res await db.collection(todos).doc(doc-id).update({ cities: _.pull({ places: _.elemMatch({ area: _.gt(100), age: _.lt(2), }) }) })pullAll按常量数组批量移除const _ db.command // 从 tags 中移除所有 database 和 cloud 字符串 let res await db.collection(todos).doc(doc-id).update({ tags: _.pullAll([database, cloud]) })addToSet去重添加原子操作addToSet是原子操作给定一个或多个元素除非数组中已存在该元素否则添加进数组。示例 1添加一个元素const _ db.command // 如果 tags 数组中不包含 database则添加进去 let res await db.collection(todos).doc(doc-id).update({ tags: _.addToSet(database) })示例 2添加多个元素需传入一个对象其中字段each或$each的值为数组const _ db.command let res await db.collection(todos).doc(doc-id).update({ tags: _.addToSet({ $each: [database, cloud] }) })典型场景标签去重、关注者集合维护等原子性保证并发场景下不会插入重复元素。进阶操作符的序列化与执行链路理解操作符如何落地执行有助于排查条件写错导致的意外结果。从database-ql源码看完整链路为命令构造_上的每个操作符command.ts返回对应命令类的实例——LogicCommand逻辑、QueryCommand查询比较、UpdateCommand更新实例上记录了operator操作符名与operands操作数字段名绑定命令实例被放进where/update对象后序列化器在编码前通过_setFieldName将命令与所在字段名绑定见 serializer/query.ts否则会抛出 Cannot encode a comparison command with unset field name 错误操作符映射operatorToString依据 operator-map.ts 将操作符名映射为$前缀的数据库原语如neq - $ne、remove - $unset、shift - $pop、unshift - $push四个特例查询扁平化与合并flattenQueryObject将嵌套对象拍平为点路径mergeConditionAfterEncode把同一字段上的多个条件合并如_.gt(50).lt(100)合并为{ $gt: 50, $lt: 100 }并把根级逻辑指令合并为$and/$or数组请求发送where(...).get()走 query.ts 的senddoc(...).update(...)走 document.ts聚合表达式经 aggregate.ts 以ActionType.aggregate提交最终由 Laf 服务端见 server转发至底层数据库执行。这份操作符手册对应的完整文档位于 docs/zh/cloud-database/database-ql/command.md建议配合 database-ql 包源码 与 查询where文档、更新update文档、聚合aggregate文档 一起阅读形成从查询、更新到聚合的完整能力闭环。【免费下载链接】lafLaf is a vibrant cloud development platform that provides essential tools like cloud functions, databases, and storage solutions. It enables developers to quickly unleash their creativity and bring innovative ideas to life with ease.项目地址: https://gitcode.com/GitHub_Trending/la/laf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考