如何用Query(Criteria)获取嵌套文档的二级子节点数据?
问题分析与解决方案
首先明确需求:从MongoDB的嵌套菜单文档中,根据根菜单ID、一级子菜单ID、二级子节点ID,获取对应的二级子节点数据。
原文档结构
{ "title": "home", "active": false, "level": "1", "children": [ { "id": 1, "title": "clothing", "type": "sub", "active": false, "level": "2", "children": [ { "id": 1, "title": "New", "type": "sup", "active": false, "level": "3" } ] } ] }
原代码的核心问题
- 参数
SubMenu subMenu无意义:无需传入该对象,我们需要从数据库查询根菜单,而非依赖外部传入的实例 - 查询条件写法错误:
children.children.$[].id不符合MongoDB查询语法,且仅靠查询条件无法直接返回嵌套子节点 - 逻辑错误:循环传入的
subMenu子元素并返回查询结果,完全偏离了从数据库获取数据的目标 - 路径变量命名不规范:
{Id}与submenuId大小写不一致,易引发参数绑定错误
方案1:查询根文档后Java代码遍历提取(简单直观)
适合嵌套层级较少的场景,先获取完整根文档,再逐层遍历匹配目标子节点:
@GetMapping("/menus/{id}/submenu/{submenuId}/children/{childId}") public Children findChildrenById(@PathVariable("id") long id, @PathVariable("submenuId") long submenuId, @PathVariable("childId") long childId) { // 1. 查询根菜单文档(假设根文档对应的实体类为Menu) Query rootQuery = new Query(Criteria.where("id").is(id)); Menu rootMenu = template.findOne(rootQuery, Menu.class); if (rootMenu == null || rootMenu.getChildren() == null) { return null; } // 2. 遍历一级子菜单,匹配目标子菜单 for (SubMenu subMenu : rootMenu.getChildren()) { if (subMenu.getId() == submenuId && subMenu.getChildren() != null) { // 3. 遍历二级子节点,匹配目标节点后返回 for (Children child : subMenu.getChildren()) { if (child.getId() == childId) { return child; } } } } return null; }
方案2:MongoDB投影操作直接返回目标子节点
通过投影语法让数据库仅返回需要的子节点数据,减少数据传输:
@GetMapping("/menus/{id}/submenu/{submenuId}/children/{childId}") public Children findChildrenById(@PathVariable("id") long id, @PathVariable("submenuId") long submenuId, @PathVariable("childId") long childId) { Query query = new Query(); // 筛选根文档,并确保包含目标一级子菜单和二级子节点 query.addCriteria(Criteria.where("id").is(id) .and("children").elemMatch(Criteria.where("id").is(submenuId) .and("children").elemMatch(Criteria.where("id").is(childId)))); // 投影仅保留匹配的二级子节点 query.fields() .include("children.$.children.$") // $符号表示匹配的第一个数组元素 .exclude("_id"); // 查询后提取目标子节点 Menu result = template.findOne(query, Menu.class); if (result == null || result.getChildren() == null || result.getChildren().isEmpty()) { return null; } SubMenu matchedSubMenu = result.getChildren().get(0); if (matchedSubMenu.getChildren() == null || matchedSubMenu.getChildren().isEmpty()) { return null; } return matchedSubMenu.getChildren().get(0); }
方案3:聚合查询(灵活适配复杂嵌套场景)
针对多层嵌套的复杂查询,用聚合管道精准定位目标子节点:
@GetMapping("/menus/{id}/submenu/{submenuId}/children/{childId}") public Children findChildrenById(@PathVariable("id") long id, @PathVariable("submenuId") long submenuId, @PathVariable("childId") long childId) { Aggregation aggregation = Aggregation.newAggregation( // 匹配根文档 Aggregation.match(Criteria.where("id").is(id)), // 展开一级子菜单数组 Aggregation.unwind("children"), // 匹配目标一级子菜单 Aggregation.match(Criteria.where("children.id").is(submenuId)), // 展开二级子节点数组 Aggregation.unwind("children.children"), // 匹配目标二级子节点 Aggregation.match(Criteria.where("children.children.id").is(childId)), // 投影仅保留二级子节点字段 Aggregation.project("children.children").andExclude("_id") ); // 执行聚合查询("menu"为集合名称) AggregationResults<Document> results = template.aggregate(aggregation, "menu", Document.class); if (results.getMappedResults().isEmpty()) { return null; } // 将MongoDB Document转换为Children实体类 Document childDoc = results.getMappedResults().get(0) .get("children", Document.class) .get("children", Document.class); return template.getConverter().read(Children.class, childDoc); }
关键注意事项
- 确保
Menu、SubMenu、Children实体类的字段与MongoDB文档结构完全对应(包括嵌套关系和字段名) - 路径变量命名保持一致,避免大小写导致的参数绑定错误
- 使用MongoDB数组操作符(如
$elemMatch、$unwind)时,需确保MongoDB版本支持对应语法
内容的提问来源于stack exchange,提问作者tauheed Alzohar
相关产品推荐
相关产品推荐

