【MongoDB】多级嵌套数组的操作 含Mongo Shell 和 MongoTemplate的增删改细节
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
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":"测试$[]"}}
);
如上图所示type
为 select
的元素也被修改了也就是说"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]"
定义了标识符 out
与 inner
之后就可以做增删改的操作了
3.2.1.新增与移除元素
- 新增元素
添加一个选项key
为33
新增不需要使用到options
中的字段作为条件。
db.user_defined_field.updateMany(
{_id:1},
{$push:{"userDefinedFields.$[out].options":{"optionKey": "33", "optionValue": "选项3"}}},
{arrayFilters:[{"out.key":"222"}]}
);
- 移除元素
移除key
为22
的选项
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''
这是因为定义两个变量out
和inner
但在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.数据准备
为了演示的方便重新初始化以下数据。
[
{
"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 的版本号
如果觉得本文对你有所帮助可以帮忙点点赞哦你的支持是我更新最大的动力