【MongoDB】多级嵌套数组的操作 含Mongo Shell 和 MongoTemplate的增删改细节
创始人
2024-05-23 17:53:43
0

文章目录

  • 1.前言
  • 2.数据准备
  • 3.Mongo Shell操作实践
    • 3.1.第一层数组操作
      • 3.1.1.新增元素
      • 3.1.2.修改元素
        • 3.1.2.1.批量修改元素中的坑
      • 3.1.3.使用`$[]`做批量修改
      • 3.1.4.移除元素
    • 3.2.第二层数组操作
      • 3.2.1.新增与移除元素
      • 3.2.2.修改元素中的字段值
      • 3.2.2.1.易错点
  • 4.Mongo Template 操作实践
    • 4.1.准备工作
      • 4.1.1.数据库模型定义
      • 4.1.2.数据准备
    • 4.2.实现代码
  • 5.结语

1.前言

最近遇到了一个需求,需要在系统中配置用户的自定义字段,考虑到后续维护的灵活性,选择了使用MongoDB来持久化自定义字段数据,在做这个需求的过程中,遇到了同一个数据中有多层嵌套数组的增删改问题,在网上查阅了大部分的资料说的不是很全,于是在处理好这个需求之后,整理并记录一下处理中使用到的一些技术细节,以供大家参考。

本文包含了一级和二级嵌套数组的操作,篇幅较长,如果只想了解在生产服务中如何应用,可以直接查看目录中的 4.Mongo Template 操作实践

2.数据准备

首先是数据结构准备,这里我处理掉了和公司有关的信息,下面展示的是一个简单的Demo结构,可以看到的是在自定义字段 userDefinedFields 是一个数组类型的数据,表示同一个用户可以有多个自定义字段。自定义字段的类型type如果为 select ,则还会包含下列框的选项字段 options ,也是一个数组,此时就出现了两层的嵌套数组了。

{"_id":1,"userId":"1","userDefinedFields":[{"key":"111","name":"自定义字段1","type":"input"},{"key":"222","name":"自定义字段2","type":"select","options":[{"optionKey":"11","optionValue":"选项1"},{"optionKey":"22","optionValue":"选项2"}]}]
}

现在遇到的问题就是,不想一个大JSON来对这条数据做全量操作,只想针对数组中的某一个元素中的字段做修改,如何才能实现呢?

3.Mongo Shell操作实践

查询MongoDB的官方文档中与数组更新相关的部分:《Array Update Operators》,可以看到如下的一些操作方式。
在这里插入图片描述
接下来就按照文档的指引,完成下面的操作。

3.1.第一层数组操作

3.1.1.新增元素

userDefinedFields中添加数据,即新建一个自定义字段。可以使用 $push$addToSet,两者的区别是 $addToSet 添加的元素不能在数组中已存在,而$push没有限制,此处用$push演示。

db.user_defined_field.updateMany({_id:1},{$push:{"userDefinedFields":{"key":"333","name":"自定义字段3","type":"input"}}}
);

此时的数据会如下:

{"_id":1,"userId":"1","userDefinedFields":[{"key":"111","name":"自定义字段1","type":"input"},{"key":"222","name":"自定义字段2","type":"select","options":[{"optionKey":"11","optionValue":"选项1"},{"optionKey":"22","optionValue":"选项2"}]},{"key":"333","name":"自定义字段3","type":"input"}]
}

3.1.2.修改元素

修改元素需要使用到 $$[],两者的区别在于 $ 只修改条件匹配的第一个元素 $[]是修改条件匹配的全部元素。

// 第一层数组修改
db.user_defined_field.updateMany({_id:1,"userDefinedFields.type":"input"},{$set:{"userDefinedFields.$.name":"测试$"}}
);

此时只修改了第一个结果(减少篇幅,后续的数据从数组开始展示,外层的id不展示了):
在这里插入图片描述

3.1.2.1.批量修改元素中的坑

再尝试批量修改元素:

db.user_defined_field.updateMany({_id:1,"userDefinedFields.type":"input"},{$set:{"userDefinedFields.$[].name":"测试$[]"}}
);

在这里插入图片描述
如上图所示,typeselect 的元素也被修改了,也就是说"userDefinedFields.type":"input"并没有生效,这显然是不符合要求的。

查阅了MongoDB文档,这种更新需要使用 $[] 来标识待查询的条件,$[]具体应该怎么用呢?

3.1.3.使用$[]做批量修改

先看一下语法:

db.collection.updateMany({  },{ : { ".$[]" : value } },{ arrayFilters: [ { :  } ] }
)

  • 这里一般指的是非数组元素的查询条件,如上面数据中的id,userId


  • 想要做的更新操作是什么,这里指的是针对 数组中的元素对象 的操作,也就是字段修改,例如:$set,$inc,$setOnInsert等,参考官方文档《Field Update Operators》

  • .$[]
    指的是数组的字段名,$[] 指的是给前面的数组一个别名作为标识符,有点类似于 MySQL 中的 as

  • arrayFilters
    即数组过滤查询条件,用上面定义的别名作为 ,需要查询的值作为


综上,一个只修改类型为 "type":"input" 的脚本可以写为:

db.user_defined_field.updateMany({_id:1},{$set:{"userDefinedFields.$[out].name":"我只想修改input"}},{arrayFilters:[{"out.type":"input"}]}
);

在这里插入图片描述

3.1.4.移除元素

移除元素与添加元素的语法类似:

db.user_defined_field.updateMany({_id:1},{$pull:{"userDefinedFields":{"key":"333"}}}
);

删除成功,只剩下两个元素了:
在这里插入图片描述

3.2.第二层数组操作

上面提到 $$[] 均只能对第一层数组进行操作,要操作第二层的数组,需要使用 $[] 对数据进行检索,也就是在上面 3.1.3中的使用方式,在看一下这个语法,加深下印象:

db.collection.updateMany({  },{ : { ".$[]" : value } },{ arrayFilters: [ { :  } ] }
)

现在我们需要操作的是 userDefinedFields 中的某个对象中的 options字段,则 .$[] 可以写成:

// 如果不需要使用的 options 中的字段作为条件
"userDefinedFields.$[out].options"
// 如果需要使用到 options 中的字段作为条件
"userDefinedFields.$[out].options.$[inner]"

定义了标识符 outinner 之后,就可以做增删改的操作了

3.2.1.新增与移除元素

  • 新增元素
    添加一个选项,key33,新增不需要使用到options中的字段作为条件。
db.user_defined_field.updateMany({_id:1},{$push:{"userDefinedFields.$[out].options":{"optionKey": "33", "optionValue": "选项3"}}},{arrayFilters:[{"out.key":"222"}]}
);

在这里插入图片描述

  • 移除元素
    移除 key22 的选项
db.user_defined_field.updateMany({_id:1},{$pull:{"userDefinedFields.$[out].options":{"optionKey": "22"}}},{arrayFilters:[{"out.key":"222"}]}
);

在这里插入图片描述
这里的移除元素使用到了 options 中的字段作为条件,为什么也没有用到 .$[inner] 的别名呢?

其实可以理解,这里的 $pull 操作,在 userDefinedFields.$[out].options": 后面总得接点什么东西吧,这里接的就是需要被删除的对象的字段条件,所以不需要在 arrayFilters 中指定条件。

3.2.2.修改元素中的字段值

不再废话,直接上脚本:

db.user_defined_field.updateMany({_id:1},{$set:{"userDefinedFields.$[out].options.$[inner].optionValue":"修改后的选项1"}},{arrayFilters:[{"out.key":"222"},{"inner.optionKey":"11"}]}
);

在这里插入图片描述

3.2.2.1.易错点

如果脚本中的arrayFilters写成了下面这个样子:

db.user_defined_field.updateMany({_id:1},{$set:{"userDefinedFields.$[out].options.$[inner].optionValue":"修改后的选项1"}},{arrayFilters:[{"out.key":"222","inner.optionKey":"11"}]}
);

这里会抛出异常caused by :: Expected a single top-level field name, found 'out' and 'inner'',这是因为定义两个变量outinner,但在arrayFilters的数组中却只对应了一个对象,注意区分:

[{"out.key":"222","inner.optionKey":"11"}] // 错误
[{"out.key":"222"},{"inner.optionKey":"11"}] // 正确

至此,Mongo Shell的操作就告一段落,下面是在Mongo Template的实践

4.Mongo Template 操作实践

4.1.准备工作

4.1.1.数据库模型定义

从内到外创建3个类,分别对应,第二层的数组,第一层外层的数组以及外层的对象。

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.mongodb.core.mapping.Field;/*** 第二层数组*/
@Getter
@Setter
public class Option {@Fieldprivate String optionKey;@Fieldprivate String optionValue;
}
package com.eqxiu.crm.system.dao.mongo.test;import lombok.Getter;
import lombok.Setter;
import org.springframework.data.mongodb.core.mapping.Field;import java.util.List;/*** 第一层数组*/
@Getter
@Setter
public class FieldData {@Fieldprivate String key;@Fieldprivate String name;@Fieldprivate String type;@Fieldprivate List
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;import java.util.List;/*** 外层对象*/
@Getter
@Setter
@Document(collection = "user_defined_field")
public class UserDefinedField  {@Fieldprivate Long id;@Fieldprivate String userId;@Fieldprivate List customFields;}

4.1.2.数据准备

为了演示的方便,重新初始化以下数据。

{"_id":1,"userId":"1","userDefinedFields":[{"key":"111","name":"自定义字段1","type":"input"},{"key":"222","name":"自定义字段2","type":"select","options":[{"optionKey":"11","optionValue":"选项1"},{"optionKey":"22","optionValue":"选项2"}]}]
}

4.2.实现代码

import com.mongodb.BasicDBObject;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;import javax.annotation.Resource;@Component
public class UserDefineFieldDao {@Resourceprivate MongoTemplate mongoTemplate;/*** 第一层新增元素*/public void pushLevelOne() {FieldData fieldData = new FieldData();fieldData.setKey("333");fieldData.setName("自定义字段3");fieldData.setType("input");Query query = Query.query(Criteria.where("_id").is(1));Update update = new Update().push("userDefinedFields", fieldData);mongoTemplate.updateMulti(query, update, UserDefinedField.class);}/*** 第一层移除元素*/public void pullLevelOne() {Query query = Query.query(Criteria.where("_id").is(1));Update update = new Update().pull("userDefinedFields", new BasicDBObject("key", "111"));mongoTemplate.updateMulti(query, update, UserDefinedField.class);}/*** 第一层更新元素*/public void updateLevelOne() {Query query = Query.query(Criteria.where("_id").is(1));Update update = new Update();update.set("userDefinedFields.$[out].name", "通过MongoTemplate更新");update.filterArray("out.key", "222");mongoTemplate.updateMulti(query, update, UserDefinedField.class);}/*** 第二层新增元素*/public void pushLevelTwo() {Option option = new Option();option.setOptionKey("33");option.setOptionValue("选项3");Query query = Query.query(Criteria.where("_id").is(1));Update update = new Update();update.push("userDefinedFields.$[out].options", option);update.filterArray("out.key", "222");mongoTemplate.updateMulti(query, update, UserDefinedField.class);}/*** 第二层移除元素*/public void pullLevelTwo() {Query query = Query.query(Criteria.where("_id").is(1));Update update = new Update();update.pull("userDefinedFields.$[out].options", new BasicDBObject("optionKey", "11"));update.filterArray("out.key", "222");mongoTemplate.updateMulti(query, update, UserDefinedField.class);}/*** 第二层修改元素*/public void updateLevelTwo() {Query query = Query.query(Criteria.where("_id").is(1));Update update = new Update();update.set("userDefinedFields.$[out].options.$[inner].optionValue", "通过MongoTemplate修改选项2");update.filterArray("out.key", "222");update.filterArray("inner.optionKey", "22");mongoTemplate.updateMulti(query, update, UserDefinedField.class);}}

上述的代码都亲自进行了验证,请放心食用~,避免本篇文章又臭又长,下面就不一一放出执行结果啦,以一张最终的数据图来结尾吧。
在这里插入图片描述

5.结语

要操作多级嵌套的数组,需要使用到 MongoDB 的两个特性:

  • $[]
  • arrayFilters

事不过三,最后在回顾一次语法吧:

db.collection.updateMany({  },{ : { ".$[]" : value } },{ arrayFilters: [ { :  } ] }
)

:只有在MongoDB 3.6 以上的版本才可以正常使用,可以通过 db.version() 查看当前 MongoDB 的版本号
在这里插入图片描述


如果觉得本文对你有所帮助,可以帮忙点点赞哦!你的支持是我更新最大的动力!

相关内容

热门资讯

安卓系统怎么关钥匙,轻松掌握钥... 手机里的安卓系统,是不是有时候让你觉得有点儿头疼?比如,当你想关掉手机,却发现钥匙在哪里呢?别急,今...
安卓系统有隐私空间,打造安全私... 你知道吗?在智能手机的世界里,安卓系统可是个超级明星呢!它不仅功能强大,而且现在还悄悄地给你准备了一...
安卓系统设置角标,打造专属通知... 你有没有发现,手机上的安卓系统设置里有个神奇的小功能——角标?这个小东西虽然不起眼,但作用可大了去了...
安卓系统定位信息查询,揭秘移动... 你有没有想过,你的手机里藏着多少秘密?尤其是那个安卓系统,它可是个超级侦探,随时随地都在帮你定位。今...
安卓刷入系统恢复,轻松实现设备... 手机系统崩溃了?别慌!安卓刷入系统恢复大法来啦! 手机,这个我们生活中不可或缺的小伙伴,有时候也会闹...
安卓系统限制无法录音,探索无法... 你有没有遇到过这种情况?手机里明明装了录音软件,却突然发现,哎呀妈呀,竟然无法录音了!这可真是让人头...
怎么降级手机系统安卓,操作指南... 手机系统升级了,新功能层出不穷,但有时候,你可能会觉得,这系统太卡了,想回到那个流畅如丝的年代。别急...
米oa系统是安卓系统吗,深入解... 亲爱的读者,你是否曾好奇过,米OA系统是不是安卓系统的一员?这个问题,就像是一颗好奇的种子,悄悄地在...
手机刷安卓车载系统,手机刷机后... 你有没有发现,现在开车的时候,手机和车载系统之间的互动越来越紧密了呢?想象当你驾驶着爱车,一边享受着...
vivo安卓怎么降系统,viv... 手机用久了,是不是觉得系统越来越卡,运行速度大不如前?别急,今天就来教你怎么给vivo安卓手机降降级...
nova 4刷安卓系统,体验全... 最近手机界可是热闹非凡呢!听说华为nova 4要刷安卓系统了,这可真是让人兴奋不已。你有没有想过,你...
如果当初没有安卓系统,科技世界... 想象如果没有安卓系统,我们的生活会是怎样的呢?是不是觉得有点不可思议?别急,让我们一起穿越时空,探索...
安卓电视装win系统,系统转换... 亲爱的读者们,你是否曾想过,在你的安卓电视上装一个Windows系统,让它瞬间变身成为一台功能强大的...
安卓手机还原系统好处,重拾流畅... 你有没有遇到过安卓手机卡顿、运行缓慢的情况?别急,今天就来给你揭秘一下安卓手机还原系统的那些好处,让...
安卓系统能跑win吗,探索跨平... 你有没有想过,你的安卓手机里能不能装上Windows系统呢?这听起来是不是有点像科幻电影里的情节?别...
安卓车载系统蓝牙设置,畅享智能... 你有没有发现,现在开车的时候,手机和车载系统之间的互动越来越频繁了呢?这不,今天就来给你详细说说安卓...
奥利奥安卓系统,探索新一代智能... 你有没有想过,一块小小的奥利奥饼干竟然能和强大的安卓系统扯上关系?没错,今天就要来聊聊这个跨界组合,...
微信使用安卓系统,功能解析与操... 你有没有发现,现在用微信的人越来越多了呢?尤其是安卓系统的用户,简直就像潮水一样涌来。今天,就让我带...
体验最新原生安卓系统,极致体验... 你有没有想过,手机系统就像是我们生活的调味品,有时候换一种口味,生活都会变得有趣起来呢?最近,我体验...
安卓系统能玩原神,尽享奇幻冒险... 你有没有想过,在安卓系统上也能畅玩《原神》这样的热门游戏呢?没错,就是那个画面精美、角色丰富、玩法多...