MongoDB 数组查询($elemMatch)、更新操作(占位符$)详解
前言和官方文档
前言:
MongoDB中涉及到数组字段的查询和更新很常用,抽空把自己开发工作中常遇到的场景拿出来并结合官方文档小结一下。
有说的不对的地方,欢迎指出交流探讨,也希望这篇笔记能够帮到你。
可以转载,但请注明出处。
之前自己写的SpringBoot整合MongoDB的聚合查询操作,感兴趣的可以点击查阅。
https://www.cnblogs.com/zaoyu/p/springboot-mongodb.html
官方文档:
$elemMatch: https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/
$update: https://www.mongodb.com/docs/manual/reference/operator/update/positional/#mongodb-update-up.-
测试环境: MongoDB 5.0.9
一、Array(数组)相关的Query(查询)
官方定义和语法格式
数组的查询多数情况结合$elemMatch操作符一起查询,也可以不使用。 下面分别是两种情况的演示说明。
1.1 是直接查询,不使用$elemMatch, 1.2是带$elemMatch的查询。
具体语法格式见1.1 和1.2开头。
1.1 直接查询 (普通的find)
就是直接 db.collection.find({queryExpression})
以官方提供的Demo来说明
// 1. 插入多条数据 db.inventory.insertMany([ { item: "journal", qty: 25, tags: ["blank", "red"], dim_cm: [ 14, 21 ] }, { item: "notebook", qty: 50, tags: ["red", "blank"], dim_cm: [ 14, 21 ] }, { item: "paper", qty: 100, tags: ["red", "blank", "plain"], dim_cm: [ 14, 21 ] }, { item: "planner", qty: 75, tags: ["blank", "red"], dim_cm: [ 22.85, 30 ] }, { item: "postcard", qty: 45, tags: ["blue"], dim_cm: [ 10, 15.25 ] } ]);
// 2.1 数组元素完全匹配查询(值、值的个数、顺序,都要完全一致才返回)。 // 这个语句就是查找tags数组下只且只有red/blank两个值的元素,且顺序要为red、blank。 db.inventory.find( { tags: ["red", "blank"] } ) // 结果可见下图
// 2.2 数组元素部分匹配查询(只要数组元素中的值,部分值匹配要查询的条件,就可以返回,无关顺序), 要使用 $all 操作符 // 这个语句意思是说,查找tags数组中,只要元素值里面有red和blank(注意,是多个条件同时存在),就返回,无关顺序。 db.inventory.find( { tags: { $all: ["red", "blank"] } } ) // 结果可见下图
// 2.3 数组元素单个值匹配和范围查找 // 比如查找元素中包含某个值的文档,或者元素中存在位于查找范围区间的值的文档。 // 2.3.1 这个是查找元素中存在red值的文档,注意,这里不像上面的完全匹配或者部分匹配时使用中括号,而是直接把值带进去。 db.inventory.find( { tags: "red" } ) // 2.3.2 这个是查找数组元素中存在符合区间范围的值的元素(类似部分匹配),一般会传入$gt/$lt/$gte/$lte/$ne/$eq之类的匹配范围操作符 // 这里是查找 dim_cm数组中,存在大于25的值的元素。 db.inventory.find( { dim_cm: { $gt: 25 } } ) 2.3.1和2.3.2结果见下图 当然也可以使用多条件匹配查询,就是下文要讲到的$elemMatch查询。 大概格式和说明如下,比如 db.inventory.find( { dim_cm: { $elemMatch: { $gt: 22, $lt: 30 } } } ) 说明要查找dim_cm中存在元素值大于22小于30的文档。 具体看下文。
// 2.1的查询结果
// 2.2的查询结果
// 2.3.1 的查询结果
// 2.3.2 的查询结果
以上就是基本的数组查询,如果涉及到嵌套数组(就是数组里面嵌套着对象),无非是在查询条件那里使用对象形式或者多层级字段来查询。
比如存在这样的数据:{ item: "journal", instock: [ { warehouse: "A", qty: 5 }, { warehouse: "C", qty: 15 } ] } .... 这里为了省事,就列一个。
instock数组字段存储着对象
// 示例查询语句,意思是说查询instock数组中的元素存在等于 { warehouse: "A", qty: 5 } 的文档。 db.inventory.find( { "instock": { warehouse: "A", qty: 5 } } ) // 意思是说查询instock数组中元素对象中的qty 存在大于等于20的文档。 db.inventory.find( { 'instock.qty': { $lte: 20 } } )
官方的说明、Demo地址: https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/
1.2 使用$elemMatch操作符查询,本文侧重该方式。
官方说明:The $elemMatch
operator matches documents that contain an array field with at least one element that matches all the specified query criteria.
就是说$elemMatch是用来查询数组字段的,如果数组字段中有至少1个元素匹配查询规则,则查询出来这个数组。
// 官方标准语法 { <field>: { $elemMatch: { <query1>, <query2>, ... } } } // field是数组字段名,在$elemMatch中传入查询条件,可多个,用逗号隔开。
举例说明
// 先插入数据 db.scores.insertMany( [ { _id: 1, results: [ 82, 85, 88 ] }, { _id: 2, results: [ 75, 88, 89 ] } ] ) // 来个最基本的$elemMatch使用演示 // 语句说明,查找 results数组中存在大于等于80且小于85的元素的文档。(只要元素中有一个匹配,那么这个元素所在的数组的文档就会返回) db.scores.find( { results: { $elemMatch: { $gte: 80, $lt: 85 } } } ) // 返回结果, { "_id" : 1, "results" : [ 82, 85, 88 ] } // 说明: _id=2的数据,没有任何一个元素值在80~85之间,所以不返回。
上面是简单的数组结构的查询。
下面演示下元素为对象的数据的查询。
// 1. 插入数据 db.survey.insertMany( [ { "_id": 1, "results": [ { "product": "abc", "score": 10 }, { "product": "xyz", "score": 5 } ] }, { "_id": 2, "results": [ { "product": "abc", "score": 8 }, { "product": "xyz", "score": 7 } ] }, { "_id": 3, "results": [ { "product": "abc", "score": 7 }, { "product": "xyz", "score": 8 } ] }, { "_id": 4, "results": [ { "product": "abc", "score": 7 }, { "product": "def", "score": 8 } ] } ] ) // 查询演示, 这里是查询results中元素含有 { product: "xyz", score: { $gte: 8 } --- 也就是product = xyz, score ≥ 8的元素。 只要数组中含有匹配的元素,该数组所在的文档返回。 db.survey.find( { results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } } ) // 查询结果 --- 可以看到 product = xyz, score≥8,所以这条 id=3的文档被返回。 { "_id" : 3, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "xyz", "score" : 8 } ] } // 补充,只要数组中的元素有至少一个匹配查询规则($elemMatch中的条件),那么该数组所在的文档就返回,不管数组中其他的元素怎样。
带$elemMatch和不带的查询对比
// 不带$elemMatch 查找results.product不等于xyz的文档,是要全部元素做匹配,如果有一个元素不匹配,那条数据所在的文档就不返回。 db.survey.find( { "results.product": { $ne: "xyz" } } ) // 返回结果 { "_id" : 4, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "def", "score" : 8 } ] } // 带$elemMatch db.survey.find( { "results": { $elemMatch: { product: { $ne: "xyz" } } } } ) // 返回结果,说明,因为是要查找数组results中存在元素product != xyz的文档,只要有一个元素的product != xyz,那么这个元素所在的数组的文档就会返回。 下面 id=1\2\3的第一个元素都为abc, != xyz, 所以返回,哪怕第二个元素的product == xyz。 { "_id" : 1, "results" : [ { "product" : "abc", "score" : 10 }, { "product" : "xyz", "score" : 5 } ] } { "_id" : 2, "results" : [ { "product" : "abc", "score" : 8 }, { "product" : "xyz", "score" : 7 } ] } { "_id" : 3, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "xyz", "score" : 8 } ] } { "_id" : 4, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "def", "score" : 8 } ] }
可以看到对于数组的查询,带$elemMatch和不带,区别很大。 通常情况下,一般会用$elemMatch,但有时候也会视实际需求来选择。
看完了如何查询,现在可以进入第二步——如何更新,因为update()里面有两个主要参数,一个是query, 一个是set。
db.collection.updateOne(<filter>, <update>, <options>) filter就是query语句, update就是set语句,还有一个参数配置。
二、Array(数组)相关的Update(更新)
1. 官方定义和语法格式
// 官方标准语法定义 db.collection.update( <query>, // 要更新的文档的查询语句 <update>, // 要更新的内容 { upsert: <boolean>, // 可选,true时开启,存在则更新,否则新增,默认为False。 multi: <boolean>, // 可选, true时开启,批量更新符合条件的文档,否则只更新第一条符合条件的文档,默认false。 writeConcern: <document>, // 可选,写入策略。比如writeConcern:{w:1}, w:1 是默认的writeConcern,表示数据写入到Primary就向客户端发送确认。 这个一般可以先不管,除非有需要再去专门看,否则不带此参数。 collation: <document>, // 可选,根据不同的语言定制排序规则,比如{collation: {locale: "zh"}} 代表文档处理时,按照中文拼音排序规则来排序处理。 默认情况下,是按照字段值的普通的二机制字符串来排序。 可以先忽略。 arrayFilters: [ <filterdocument1>, ... ], // 可选,这个对于数组字段的更新很有用,尤其是只需要更新数组中的符合条件的个别元素。 等下下文会有使用演示。 hint: <document|string>, // 可选, 4.2 版本后新增的东西 强制某个字段使用索引。 尽管mongodb会自动优化处理,但为了避免某个字段没有使用索引,可以强制指定。 let: <document> // 可选, 5.0 版本后新增的东西 一个变量定义选项,可以让命令的可读性得到提升, } ) // 上面的let 选项,如下是一个简易示例。 假设有这几条数据 db.cakeFlavors.insertMany( [ { _id: 1, flavor: "chocolate" }, { _id: 2, flavor: "strawberry" }, { _id: 3, flavor: "cherry" } ] ) // 执行下述语句,用了let 分别定义了 targetFlavor变量,值为cherry, newFlavor变量,值为 orange。 在前面的 query/update 中,对应的匹配值和新的set的值,是用了let中定义的变量名来占位。 db.cakeFlavors.update( { $expr: { $eq: [ "$flavor", "$$targetFlavor" ] } }, [ { $set: { flavor: "$$newFlavor" } } ], { let : { targetFlavor: "cherry", newFlavor: "orange" } } ) 上面的意思就是查找flalvor等于targetFlavor(let中定义的变量)的值(也就是cherry)的文档,然后把flavor的值更新成 newFlavor(let中定义的变量)的值(即 orange)。
除了上面标准语法,还有updateOne--更新符合条件的第一条;updateMany--更新多条,replaceOne--替换符合条件的一条。 参数和上面的一样,不赘述。
- db.collection.updateOne(<filter>, <update>, <options>)
- db.collection.updateMany(<filter>, <update>, <options>)
- db.collection.replaceOne(<filter>, <update>, <options>)
2. 数组更新操作符(Array Update Operators)
要正确、熟悉地实现针对数组的更新,需要了解学习以下几个数组更新操作符。
- $ 占位符,只更新符合条件的文档的数组字段中的第一个匹配的元素。 下文有demo。
- $[] 占位符,和$的区别是更新符合条件的文档的数组字段中的所有元素。
- $[<identifier>] 也是占位符,但是只更新符合条件的文档的数组中的指定元素(符合某个条件)。 要和update中的第三个参数中的可选项 arrayFilters配合使用。
- $addToSet,添加元素到一个数组,确保不重复(set)。如果数组中没有一模一样的元素,可以插入,如果有,则无法插入。
- $pop 删除数组第一个或者最后一个元素。
- $pull 删除数组中所有符合指定条件的元素。
- $push 添加一个元素到数组中。
- $pullAll 删除数组中的所有元素。
3. 举例
基于上述的数组字段的查询,以及update语法,我们可以开始做基于数组的更新操作演示了。非数组字段的更新,请读者自行找官方文档参考学习,此处不赘述。
3.1 $ 占位符
用于只更新符合条件的文档中的第一个匹配的元素。
// 让我们先插入一些数据 db.students.insertMany( [ { "_id" : 1, "grades" : [ 85, 80, 80 ] }, { "_id" : 2, "grades" : [ 88, 90, 92 ] }, { "_id" : 3, "grades" : [ 85, 100, 90 ] } ] ) // 注意,这里用的updateOne,只更新一条。 // 查找_id=1, grades数组中有80的文档。把grades中第一个匹配的元素(就是值为80)替换成 82 db.students.updateOne( { _id: 1, grades: 80 }, { $set: { "grades.$" : 82 } } ) // 更新完毕后,可以看到id=1的数据中的grades,第一个80变成82了,后面的80没变。 这就是$,代表占位匹配的第一个元素位置(不是数组的第一个元素)。 { "_id" : 1, "grades" : [ 85, 82, 80 ] } { "_id" : 2, "grades" : [ 88, 90, 92 ] } { "_id" : 3, "grades" : [ 85, 100, 90 ] } // 有人说,如果我要更新所有grades含有 90的数组的第一个匹配元素呢?请看下面语句。 // 使用update,开启批量更新( {multi:true})。 查找grades存在90的文档,并把每个文档中的第一个90替换成82. db.students.update( { grades: 90 }, { $set: { "grades.$" : 82 } }, {multi:true} ) // 执行后的结果 { "_id" : 1, "grades" : [ 85, 82, 80 ] } { "_id" : 2, "grades" : [ 88, 82, 92 ] } { "_id" : 3, "grades" : [ 85, 100, 82] }
上面是比较简单的普通数组,如果数组存储的是对象呢? 同理的,看代码
// 先执行一下把Students的数据清空 db.students.remove({}) // 插入带对象的数组数据 db.students.insertMany( [ {"_id" : 4, "grades" : [ { "grade" : 80, "mean" : 75, "std" : 8 }, { "grade" : 85, "mean" : 90, "std" : 6 }, { "grade" : 85, "mean" : 85, "std" : 8 } ] }, { "_id": 5, "grades": [ { "grade": 80, "mean": 75, "std": 8 }, { "grade": 85, "mean": 90, "std": 5 }, { "grade": 90, "mean": 85, "std": 3 } ] } ]) // 执行updateOne db.students.updateOne( { _id: 4, "grades.grade": 85 }, { $set: { "grades.$.std" : 10 } } )
可以看到,成功更新1条。
查看结果如下图。 对象数组的操作也一样的。 只更新第一个匹配的元素。
上面的例子,没有使用$elemMatch作为匹配查询条件,你要用也可以。 自己尝试。
3.2 $[] 占位符
和$的区别是更新符合条件的文档的数组字段中的所有元素。
// 还是一样,先清空下数据 db.students.remove({}) // 插入演示数据 db.students.insertMany( [ { "_id" : 1, "grades" : [ { "grade" : 80, "mean" : 75, "std" : 8 }, { "grade" : 85, "mean" : 90, "std" : 6 }, { "grade" : 85, "mean" : 85, "std" : 8 } ] }, { "_id" : 2, "grades" : [ { "grade" : 90, "mean" : 75, "std" : 8 }, { "grade" : 87, "mean" : 90, "std" : 5 }, { "grade" : 85, "mean" : 85, "std" : 6 } ] } ] ) // 更新所有grades数组中含有grade大于80的文档,使用grades.$[].std表示更新每个匹配文档中的所有元素的std字段值。 这里是统一改成10。 db.students.updateMany( {"grades":{$elemMatch:{"grade": {$gt:80}}}}, { $set: { "grades.$[].std" : 10 } }, ) 结果看下图
假如你要处理的数组字段不是一个对象,只是字符串或者数字,$[]后面不需要接下级字段。 如下参考代码。
db.students.updateMany( {"grades":{$elemMatch:{"grade": {$gt:80}}}}, { $set: { "grades.$[]" : "随便你写个值" } }, // 这里grades 假设是一个字符串数组。 )
写到这里,我们已经分别用$、$[] 实现对第一个匹配的元素、符合条件的所有文档下的所有元素做更新。
有读者可能会问,那么我想只更新文档下的部分元素呢?比如我只想把数组中的grade大于等于87的元素的std换成 22呢?
好问题,对于这种需求,要使用第三种占位符 $[<identifier>]。
3.3 $[<identifier>]
注意:$[<identifier>] 通常情况下是要和 arrayFilters 一起使用的。语法格式如下
db.collection.updateMany( { <query conditions> }, // 查询条件 { <update operator>: { "<array>.$[<identifier>]" : value } }, // 更新内容 { arrayFilters: [ { <identifier>: <condition> } ] } // 数组过滤条件 )
请看代码。
// 先清空下数据 db.students.remove({}) // 插入数据 db.students.insertMany( [ { "_id" : 1, "grades" : [ 95, 92, 90 ] }, { "_id" : 2, "grades" : [ 98, 100, 102 ] }, { "_id" : 3, "grades" : [ 95, 110, 100 ] } ] ) // 使用$[<identifier>] 更新 // 查询所有,把grades中≥100的值全部换成333。 db.students.updateMany( { }, { $set: { "grades.$[element]" : 333 } }, { arrayFilters: [ { "element": { $gte: 100 } } ] } ) 注意: identifier的名称可以是任意,但是$set和arrayFilters中的名称要一致。 执行后的数据结果如下。
可以看到,上面只更新了匹配的文档中的符合arrayFilters条件的元素。
有人又问,你这是一个简单的数组,可以演示下对象数组吗? 可以,看代码。
// 还是先清空数据 db.students.remove({}) // 插入数据 db.students.insertMany( [ { "_id" : 1, "grades" : [ { "grade" : 80, "mean" : 75, "std" : 5 }, { "grade" : 85, "mean" : 100, "std" : 4 }, { "grade" : 85, "mean" : 100, "std" : 5 } ] }, { "_id" : 2, "grades" : [ { "grade" : 90, "mean" : 100, "std" : 5 }, { "grade" : 87, "mean" : 100, "std" : 3 }, { "grade" : 85, "mean" : 100, "std" : 4 } ] } ] ) // 查询所有,把所有grades中的grade ≥ 85的元素中的mean更新为111 db.students.updateMany( { }, { $set: { "grades.$[elem].mean" : 111 } }, { arrayFilters: [ { "elem.grade": { $gte: 85 } } ] } ) 注意: arrayFilters的elem要和set中的$[elem]中的elem一致。 是一个标识符。 可以任意,但要一致。 执行后结果如下图
讲到这里,对于数组字段中的元素编辑基本上可以满足开发需求,再小结下。
- $ : 更新文档中匹配的第一个元素
- $[] : 更新文档中所有元素
- $[<identifier>] : 条件更新
除了上面3个占位符,mongodb 数组中的更新还有几个操作符($addToSet, $pop, $pull, $push, $pullAll),下面逐一介绍。
$addToSet
AddToSet
// 语法格式:
{ $addToSet: { <field1>: <value1>, ... } } // 字段名:值
// 插入一条演示数据 db.inventory.insertOne( { _id: 1, item: "polarizing_filter", tags: [ "electronics", "camera" ] } ) // 使用addToSet 添加一个元素到Tags中。 db.inventory.updateOne( { _id: 1 }, { $addToSet: { tags: "accessories" } } )
可以看到 accessories 作为元素追加到了tags数组中
当要插入的元素已经存在,可以看到modified是0,也就是没更新。
$pop
// 作用,删除数组中的第一个或者最后一个元素。
// 语法格式 { $pop: { <field>: <-1 | 1>, ... } } 其中 -1, 1 分别代表数组的第一个元素和最后一个元素 // 插入数据 db.students.insertOne( { _id: 1, scores: [ 8, 9, 10 ] } ) // 删除scores数组的第一个元素(-1) db.students.updateOne( { _id: 1 }, { $pop: { scores: -1 } } ) // 再次查看 结果如下 { _id: 1, scores: [ 9, 10 ] } 可以看到第一个元素 8 已经被删掉。
$push
// 作用: 把一个元素加入到数组中。
// 语法
{ $push: { <field1>: <value1>, ... } }
// 插入数据
db.students.insertMany( [
{ _id: 2, scores: [ 45, 78, 38, 80, 89 ] } ,
{ _id: 3, scores: [ 46, 78, 38, 80, 89 ] } ,
{ _id: 4, scores: [ 47, 78, 38, 80, 89 ] }
] )
// 批量更新,对每个文档都往scores中追加一个元素 95.
db.students.updateMany(
{ },
{ $push: { scores: 95 } }
)
// 再次查询 结果
[
{ _id: 1, scores: [ 44, 78, 38, 80, 89, 95 ] },
{ _id: 2, scores: [ 45, 78, 38, 80, 89, 95 ] },
{ _id: 3, scores: [ 46, 78, 38, 80, 89, 95 ] },
{ _id: 4, scores: [ 47, 78, 38, 80, 89, 95 ] }
]
$pull
// 作用:删除数组中的指定元素(通过查询条件),
// 注意和 $pullAll的区别, pullAll是删除所有指定值元素, pull是传入查询条件,删除符合条件的元素。
// 语法格式 { $pull: { <field1>: <value|condition>, <field2>: <value|condition>, ... } } // 插入数据 db.stores.insertMany( [ { _id: 1, fruits: [ "apples", "pears", "oranges", "grapes", "bananas" ], vegetables: [ "carrots", "celery", "squash", "carrots" ] }, { _id: 2, fruits: [ "plums", "kiwis", "oranges", "bananas", "apples" ], vegetables: [ "broccoli", "zucchini", "carrots", "onions" ] } ] ) // 删掉fruits中所有apples, oranges元素,删掉vegatables中所有carrots元素。 db.stores.updateMany( { }, { $pull: { fruits: { $in: [ "apples", "oranges" ] }, vegetables: "carrots" } } ) // 执行后结果 { _id: 1, fruits: [ 'pears', 'grapes', 'bananas' ], vegetables: [ 'celery', 'squash' ] }, { _id: 2, fruits: [ 'plums', 'kiwis', 'bananas' ], vegetables: [ 'broccoli', 'zucchini', 'onions' ] }
$pullAll
请注意和$pull的区别
// 作用,传入指定值,删除数组中元素为指定值的所有元素,和$pull的区别是,pull是依赖于传入的查询条件,删除匹配查询条件的元素。 // 语法格式 { $pullAll: { <field1>: [ <value1>, <value2> ... ], ... } } // 插入数据 db.survey.insertOne( { _id: 1, scores: [ 0, 2, 5, 5, 1, 0 ] } ) // 执行 db.survey.updateOne( { _id: 1 }, { $pullAll: { scores: [ 0, 5 ] } } ) // 删除 0,5的元素,再次查询,结果如下 { "_id" : 1, "scores" : [ 2, 1 ] }
希望这篇文章能帮到大家,有错漏之处,欢迎指正。
完。