【MongoDB】多级嵌套数组的操作 含Mongo Shell 和 MongoTemplate的增删改细节

news2025/1/11 20:52:03

文章目录

  • 1.前言
  • 2.数据准备
  • 3.Mongo Shell操作实践
    • 3.1.第一层数组操作
      • 3.1.1.新增元素
      • 3.1.2.修改元素
        • 3.1.2.1.批量修改元素中的坑
      • 3.1.3.使用`$[<identifier>]`做批量修改
      • 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文档,这种更新需要使用 $[<identifier>] 来标识待查询的条件,$[]具体应该怎么用呢?

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

先看一下语法:

db.collection.updateMany(
   { <query conditions> },
   { <update operator>: { "<array>.$[<identifier>]" : value } },
   { arrayFilters: [ { <identifier>: <condition> } ] }
)
  • <query conditions>
    这里一般指的是非数组元素的查询条件,如上面数据中的id,userId

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

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

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


综上,一个只修改类型为 "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.第二层数组操作

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

db.collection.updateMany(
   { <query conditions> },
   { <update operator>: { "<array>.$[<identifier>]" : value } },
   { arrayFilters: [ { <identifier>: <condition> } ] }
)

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

// 如果不需要使用的 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 {
  
    @Field
    private String optionKey;
   
    @Field
    private 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 {

    @Field
    private String key;

    @Field
    private String name;

    @Field
    private String type;

    @Field
    private List<Option> options;

}
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  {

    @Field
    private Long id;

    @Field
    private String userId;

    @Field
    private List<FieldData> 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 {

    @Resource
    private 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 的两个特性:

  • $[<identifier>]
  • arrayFilters

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

db.collection.updateMany(
   { <query conditions> },
   { <update operator>: { "<array>.$[<identifier>]" : value } },
   { arrayFilters: [ { <identifier>: <condition> } ] }
)

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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/331897.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于yolo的小球位置实时检测

基于yolo的小球位置实时检测 Yolo安装 操作系统&#xff1a;ubuntu 安装cuda和opencv git clone https://github.com/pjreddie/darknet.git cd darknet 修改Makefile文件&#xff0c;使GPU1&#xff0c;OPENCV1 make 2. 数据集处理 2.1 制作数据集 将小球放在摄像头前…

分布式高级篇2 —— 商城业务 (1)

一、商品上架 一、商品上架1、sku 在 ES 中存储模型分析2、nested 嵌套类型3、商品上架业务代码&#xff08;1&#xff09;构建 sku 基本属性&#xff08;2&#xff09;构造 sku 检索属性&#xff08;3&#xff09;远程调用查询库存&#xff08;4&#xff09; 远程调用上架接口…

【干货】Python:time库的用法

【干货】Python&#xff1a;time库的用法1. time库概述2. time库的运用2.1 时间处理2.1.1 time()2.1.2 gmtime()2.1.3 localtime()2.1.4 ctime()2.2 时间格式化2.2.1 mktime()2.2.2 strftime()2.2.3 strptime()2.3 计时3. 习题4. 本文代码编译环境及版本5. 更新日志6. 参考1. t…

【CSS面试题】2023前端最新版css模块,高频15问

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;博主收集的CSS面试题 目录 一、CSS必备面试题 1.CSS3新特性 2.CSS实现元素两个盒子垂…

内网穿透-frp

frp的作用 1.利用处于内网或防火墙后的机器&#xff0c;对外网环境提供 http 或 https 服务。 2.对于 http, https 服务支持基于域名的虚拟主机&#xff0c;支持自定义域名绑定&#xff0c;使多个域名可以共用一个80端口。 3.利用处于内网或防火墙后的机器&#xff0c;对外网环…

【Python】Python学习笔记(二)基本输入输出

Python娘来源&#xff1a;https://next.rikunabi.com/tech/docs/ct_s03600.jsp?p002412 目录print()函数不进行自动换行的print()函数打印输出多个字符串只进行换行input()函数使用format方法格式化字符串字符串与数值转换字符串转换为数值数值转换为字符串总结参考资料print(…

SpringCloud(13)— 分布式缓存(Redis集群)

分布式缓存(Redis集群) 前言 单节点Redis的问题 1.数据丢失 Redis基于内存存储&#xff0c;服务器重启可能会导致数据丢失 2.并发能力 单节点Redis的并发能力虽然已经很不错&#xff0c;但是依然无法满足大型的高并发场景 3.故障恢复 如果Redis宕机&#xff0c;则服务将不…

C# 调用Python

一、简介 IronPython 是一种在 NET 和 Mono 上实现的 Python 语言&#xff0c;由 Jim Hugunin&#xff08;同时也是 Jython 创造者&#xff09;所创造。 Python是一种跨平台的计算机程序设计语言。 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python是…

电子器件系列31:ULN2003 芯片详解

主体转自&#xff1a; uln2003驱动电路_身在江湖的郭大侠的博客-CSDN博客_uln2003 一、uln2003有什么作用 ULN2003是大电流驱动阵列&#xff0c;多用于单片机、智能仪表、PLC、数字量输出卡等控制电路中。可直接驱动继电器等负载。 输入5VTTL电平&#xff0c;输出可达500mA/…

167. 两数之和 II - 输入有序数组

给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递减顺序排列 &#xff0c;请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] &#xff0c;则 1 < index1 < index2 < numbers…

每天一道大厂SQL题【Day07】教育领域SQL实战

每天一道大厂SQL题【Day07】教育领域SQL实战 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典SQL题&#…

C进阶实战通讯录

C语言实战通讯录C语言实战通讯录前言整理逻辑整体框架初始化通讯录添加联系人显示联系人删除联系人查找联系人修改联系人销毁通讯录保存联系人信息加载联系人信息所有源码&#xff1a;test.c&#xff1a;Contact.h:Contact.c:C语言实战通讯录 前言 这次用C语言实现通讯录是一…

Nginx_4

Nginx负载均衡 负载均衡概述 早期的网站流量和业务功能都比较简单&#xff0c;单台服务器足以满足基本的需求&#xff0c;但是随着互联网的发展&#xff0c;业务流量越来越大并且业务逻辑也跟着越来越复杂&#xff0c;单台服务器的性能及单点故障问题就凸显出来了&#xff0c…

VHDL语言基础-组合逻辑电路-基本逻辑门电路

数字电路中的四种基本操作是与、或、非及触发器操作&#xff0c;前三种为组合电路&#xff0c;后一种为时序电路。与非 、或非和异或的操作仍然是与、或、非的基本操作。与、或、非、与非、或非和异或等基本逻辑门电路为常用的门电路。 二输入与非门是一种常用的简单逻辑电路&a…

当下最流行的 ChatGPT :前世今生

GPT 不是凭空而出&#xff0c;它是经过了很多人的努力&#xff0c;以及很长一段时间的演化得来的。因此&#xff0c;梳理一下 GPT 的庞大 “家族” 还是很有必要的&#xff0c;看看他继承了什么&#xff0c;学习了什么&#xff0c;又改进了什么&#xff0c;这样也能更好地理解 …

C++设计模式(11)——桥接模式

亦称&#xff1a; Bridge 意图 桥接模式是一种结构型设计模式&#xff0c; 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构&#xff0c; 从而能在开发时分别使用。 问题 抽象&#xff1f; 实现&#xff1f; 听上去挺吓人&#xff1f; 让我们慢慢来&…

linux笔记 diff及patch的制作与使用

相关命令展示 为方便查阅博客使用&#xff0c;预先展示相关命令 diff命令 diff -uN old.txt new.txt > patch_test.patch单个文件&#xff0c;不需要使用-r参数 diff 选项参数 旧文件&#xff08;夹&#xff09; 新文件&#xff08;夹&#xff09; > 补丁diff命令的常…

PS快速入门系列

01-界面构成 1菜单栏 2工具箱 3工县属性栏 4悬浮面板 5画布 ctr1N新建对话框&#xff08;针对画布进行设置&#xff09; 打开对话框&#xff1a;ctrl0&#xff08;字母&#xff09; 画布三种显示方式切换&#xff1a;F 隐藏工具箱&#xff0c;工具属性栏&#xff0c;悬浮面板…

Linux perf probe 的使用(三)

文章目录前言一、Dynamic Tracing二、kprobes2.1 perf kprobe 的使用2.2 kprobe Arguments3.3 tcp_sendmsg()3.3.1 Kernel: tcp_sendmsg()3.3.2 Kernel: tcp_sendmsg() with size3.3.2 Kernel: tcp_sendmsg() line number and local variable三、uprobes的使用3.1 perf uprobe …

PTA L1-043 阅览室

前言&#xff1a;内容包括四大模块&#xff1a;题目&#xff0c;代码实现&#xff0c;大致思路&#xff0c;代码解读 题目&#xff1a; 天梯图书阅览室请你编写一个简单的图书借阅统计程序。当读者借书时&#xff0c;管理员输入书号并按下S键&#xff0c;程序开始计时&#xf…