Database Schema and Test Data Preparation
To evaluate the processing efficiency of tree structures at scale, a relational model is established first.
Table Structure Definition
The schema utilizes a self-referencing foreign key to denote parent-child relationships.
CREATE TABLE system_organization (
`id` bigint NOT NULL AUTO_INCREMENT,
`parent_id` bigint DEFAULT NULL,
`org_name` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Bulk Data Generation
A stored procedure iterates one million times. It creates a linear dependency where every 100th ID acts as a parent for the preceding 99 IDs. The initial record has no parent reference.
DELIMITER $$
CREATE PROCEDURE SeedOrganizationData()
BEGIN
DECLARE counter INT DEFAULT 1;
WHILE counter <= 1000000 DO
INSERT INTO system_organization (parent_id, org_name)
VALUES (
IF(MOD(counter, 100) = 1, NULL, counter - MOD(counter, 100)),
CONCAT('Org ', counter)
);
SET counter = counter + 1;
END WHILE;
END$$
DELIMITER ;
CALL SeedOrganizationData();
Java Implementation Strategies
We utilize Spring Boot with a standard ORM framework. The entity class separates persistent fields from transient tree associations.
Domain Model
public class TreeNode {
private Long nodeId;
private Long parentId;
private String name;
@Transient
private List<TreeNode> subNodes;
// Getters and Setters omitted for brevity
}
Approach A: Stream Collection Grouping
This method relies on parallel stream operations to group children by their parent ID immediately after fetching all records.
@GetMapping("/strategy-stream")
public List<TreeNode> assembleByStream() {
List<TreeNode> rawData = repository.findAll();
Map<Long, List<TreeNode>> hierarchyMap = rawData.stream()
.collect(Collectors.groupingBy(TreeNode::getParentId));
return rawData.stream()
.filter(node -> node.getParentId() == null || node.getParentId().equals(0L))
.peek(node -> {
List<TreeNode> children = hierarchyMap.get(node.getNodeId());
if (children != null) node.setSubNodes(children);
})
.collect(Collectors.toList());
}
Approach B: Iterative Hash Map Lookup
A traditional loop-based approach that populates maps before traversing the list to atttach references.
@GetMapping("/strategy-loop")
public List<TreeNode> assembleByLoop() {
List<TreeNode> rawData = repository.findAll();
Map<Long, List<TreeNode>> indexMap = new HashMap<>();
List<TreeNode> rootList = new ArrayList<>();
for (TreeNode node : rawData) {
indexMap.computeIfAbsent(node.getParentId(), k -> new ArrayList<>())
.add(node);
if (node.getParentId() == null) rootList.add(node);
}
for (TreeNode node : rawData) {
node.setSubNodes(indexMap.get(node.getNodeId()));
}
return rootList;
}
Approach C: Third-Party Utility Library
Utilizing an external library abstraction to handle the tree construction logic automatically.
import cn.hutool.core.tree.TreeUtil;
import cn.hutool.core.tree.TreeNode;
@GetMapping("/strategy-lib")
public List<? extends TreeNode> assembleWithLibrary() {
List<DeptDTO> depts = repository.findAll();
List<TreeStructureNode> structureList = new ArrayList<>();
for (DeptDTO dto : depts) {
TreeStructureNode temp = new TreeStructureNode();
temp.setId(dto.getNodeId());
temp.setParentId(dto.getParentId());
temp.setName(dto.getName());
structureList.add(temp);
}
return TreeUtil.build(structureList, 0L);
}
Approach D: Recursive Traversal
This strategy employs Depth First Search logic to traverse parents down to descendents, requiring efficient child lookup.
@GetMapping("/strategy-recursive")
public List<TreeNode> assembleRecursively() {
List<TreeNode> allNodes = repository.findAll();
Map<Long, List<TreeNode>> cache = allNodes.stream()
.collect(Collectors.groupingBy(n -> n.getParentId() != null ? n.getParentId() : 0));
List<TreeNode> result = new ArrayList<>();
for (TreeNode node : allNodes) {
if (node.getParentId() == null) {
recursivePopulate(node, cache);
result.add(node);
}
}
return result;
}
private void recursivePopulate(TreeNode currentNode, Map<Long, List<TreeNode>> cache) {
List<TreeNode> children = cache.get(currentNode.getNodeId());
if (children == null) return;
currentNode.setSubNodes(children);
for (TreeNode child : children) {
recursivePopulate(child, cache);
}
}
Performance Observations
Execution times vary based on system load and JVM warmup, but trends remain consistent across dataset sizes ranging from 10k to 1M nodes.
- Low Volume (10,000 Records): All four methods execute within acceptable latency windows under 100ms. Differences are negligible for small datasets.
- Medium Volume (50,000 - 100,000 Records): The iterative and stream approaches maintain stability. The library utility remains competitive due to optimized internal caching. Recursive solutions begin showing noticeable overhead due to function call stack depth.
- High Volume (> 1 Million Records): Memory pressure increases significantly. The recursive implementation consumes more stack space and CPU cycles per node compared to the flat-map approaches. Stream grouping generally offers the best balance between readability and throughput for massive payloads. The pure recursive method without pre-caching becomes impractical at this scale.