Assembling Hierarchical Tree Structures with Java Streams, Caching and Message Queues
This article demonstrates how to efficiently build department and regional hierarchical trees in Java by fetching all records in a single database query, using Stream API, Lambda expressions, in‑memory grouping, Redis caching and RocketMQ for asynchronous updates, achieving sub‑second response times even with tens of thousands of nodes.
In recent development work the author needed to return assembled department and region trees as List objects for the front‑end, and after analysis chose a Stream‑based, in‑memory approach rather than recursive database calls.
Core ideas :
Fetch all records with a single query (tens of thousands at most).
Use Java Stream, Lambda, and reference handling for fast in‑memory processing.
Cache the result with a Redis‑backed annotation; cache expires automatically.
Use RocketMQ to asynchronously refresh the cache when underlying data changes.
1. Department tree example (MySQL)
Entity classes :
@Data
public class PmTenant {
/** 主键Id */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 租户名称 */
private String tenantName;
/** 租户唯一编码,对外暴露 */
private String tenantCode;
/** 租户Id */
private String tenantId;
/** 租户状态,0可用,1禁用 */
private Integer status;
} @Data
public class PmDept {
@TableId(type = IdType.ASSIGN_ID)
private Integer id;
/** 父部门Id */
private Integer parentDeptId;
/** 部门id,全局唯一 */
private Integer deptId;
/** 部门名称 */
private String deptName;
/** 排序 */
private Integer orderNum;
/** 层级 */
private Integer depth;
/** 状态,0可用,1删除 */
private Integer status;
private String tenantId;
private String tenantCode;
}VO class (used by the front‑end):
@Data
public class DeptTreeNodeVO implements Serializable {
/** 子节点 list 集合,封装自己 */
private List
childrenNodeList;
protected Integer deptId;
protected Integer parentDeptId;
protected String deptName;
}Implementation (key steps are commented in the code):
@Resource
private PmTenantService pmTenantService;
@Resource
private PmDeptMapper pmDeptMapper;
@Override
@Cache(expiryTime = 300)
public List
assembleTree(){
// 1. Get tenant list (two tenants in example)
List
tenantList = this.pmTenantService.list();
// 2. For each tenant, convert department entities to VO list
List
resultList = tenantList.stream().map(tenant -> {
List
deptTreeNodeVOList = this.selectAllDeptByTenantCode(tenant.getTenantCode())
.stream().map(val -> val.convertExt(DeptTreeNodeVO.class)).collect(Collectors.toList());
// 3. Group by parentDeptId
Map
> listMap = deptTreeNodeVOList.parallelStream()
.collect(Collectors.groupingBy(DeptTreeNodeVO::getParentDeptId));
// 4. Attach children to each node
deptTreeNodeVOList.forEach(val -> val.setChildrenNodeList(listMap.get(val.getDeptId())));
// 5. Filter top‑level nodes (parentId == 0)
List
allChildrenList = deptTreeNodeVOList.stream()
.filter(val -> val.getParentDeptId().equals(NumberUtils.INTEGER_ZERO)).collect(Collectors.toList());
DeptTreeNodeVO node = new DeptTreeNodeVO();
node.setChildrenNodeList(allChildrenList);
node.setDeptName(tenant.getTenantName());
return node;
}).collect(Collectors.toList());
return Optional.of(resultList).orElse(null);
}
public List
selectAllDeptByTenantCode(String tenantCode) {
return pmDeptMapper.selectList(new LambdaQueryWrapper
()
.eq(PmDept::getTenantCode, tenantCode)
.eq(PmDept::getStatus, PmDeptStatus.DISABLE.getStatus()));
}The test shows that with up to two‑three‑thousand records the whole request/response completes in under one second when the server has sufficient memory.
2. Region tree example (MongoDB)
Entity :
@Data
public class Region {
@Id
public Long id;
public Long parentId;
public String name;
public String district;
public String province;
public String city;
public Long provinceId;
public Long cityId;
public Integer depth;
}VO (contains child list):
@Data
public class RegionCascadeVO extends RegionVO {
private List
childrenRegionList;
public Long id;
public String name;
public Integer depth;
public String province;
public String city;
public String district;
public Long parentId;
public Long provinceId;
public Long cityId;
}Implementation :
@Resource
private RegionRepository regionRepository;
@Override
@Cache(expiryTime = 300)
public List
quickAllTree() {
// 1. Load all regions and map to VO
List
regionCascadeVOList = this.regionRepository.findAll().stream()
.map(val -> val.convertExt(RegionCascadeVO.class))
.sorted((s1, s2) -> RegionSortUtil.citySort(s1.getName(), s2.getName()))
.sorted((s1, s2) -> RegionSortUtil.countySort(s1.getName(), s2.getName()))
.collect(Collectors.toList());
// 2. Group by parentId
Map
> listMap = regionCascadeVOList.parallelStream()
.collect(Collectors.groupingBy(RegionCascadeVO::getParentId));
// 3. Attach children
regionCascadeVOList.forEach(val -> val.setChildrenRegionList(listMap.get(val.getId())));
// 4. Return top‑level nodes (parentId == China root id)
return regionCascadeVOList.stream()
.filter(val -> RegionConstant.CHINA_ID.equals(val.getParentId()))
.collect(Collectors.toList());
}Testing with a few thousand region records also yields sub‑second response times (around 200 ms), confirming that a single database read plus in‑memory Stream processing is highly efficient.
The performance advantage stems from querying the database only once and performing all subsequent operations as reference‑based Stream manipulations in memory.
Conclusion
Using Java Stream to assemble complex parent‑child tree structures in memory provides a fast, scalable solution; the approach avoids deep recursion, reduces database load, and, when combined with Redis caching and RocketMQ for async updates, can serve large hierarchical data within a second.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.