如何多级联级抓取XmlObject树形结构及末级XmlPeriod?
以下几个方案可以解决你的问题,不需要切换EAGER或改用Set,也能避免笛卡尔积和重复结果:
方案1:递归CTE+HQL分步抓取
步骤:
递归获取完整树形结构
用Hibernate 5.2+支持的递归CTE查询所有XmlObject节点,只查询实体本身不关联XmlPeriod,完全规避笛卡尔积:WITH RECURSIVE xml_obj AS ( SELECT o FROM XmlObject o WHERE o.parent IS NULL UNION ALL SELECT child FROM XmlObject child JOIN xml_obj parent ON child.parent = parent ) SELECT o FROM xml_obj o给
childObjects集合加上@BatchSize(size = 50)注解,Hibernate会批量加载所有层级的子节点,避免懒加载带来的N+1问题。单独抓取末级节点的XmlPeriod
执行第二次HQL查询,只抓没有子节点的末级对象,并关联XmlPeriod:SELECT o FROM XmlObject o JOIN FETCH o.period WHERE o.childObjects IS EMPTY由于操作在同一个Session内,查询到的XmlObject实例和第一步的树形实例是同一个,Hibernate会自动把XmlPeriod关联到对应的末级节点上,无需手动赋值。
方案2:实体图+批量加载+手动遍历
步骤:
添加批量加载配置
在childObjects集合字段上添加@BatchSize(size = 50),访问懒加载集合时,Hibernate会批量加载同层级的所有子节点,大幅减少查询次数。用实体图加载根节点
定义包含childObjects的实体图,加载根节点:EntityGraph<XmlObject> graph = entityManager.createEntityGraph(XmlObject.class); graph.addSubgraph("childObjects"); XmlObject root = entityManager.find(XmlObject.class, rootId, Collections.singletonMap("javax.persistence.loadgraph", graph));遍历树形触发批量加载
写一个简单的递归方法遍历整个树形结构(仅需访问每个节点的childObjects),Hibernate会自动批量加载所有层级的子节点。抓取末级节点的XmlPeriod
同方案1的第二步,单独查询末级节点关联的XmlPeriod,自动关联到树形实例中。
方案3:原生SQL递归查询+结果转换
如果数据库支持递归CTE(如MySQL 8+、PostgreSQL),可以用原生SQL一次性查出所有节点和对应的末级Period,再手动转换成树形结构:
步骤:
- 编写递归SQL
WITH RECURSIVE xml_tree AS ( SELECT o.id, o.parent_id, o.name, p.id AS period_id, p.start_date, p.end_date, 1 AS node_level FROM xml_object o LEFT JOIN xml_period p ON o.id = p.xml_object_id AND (SELECT COUNT(*) FROM xml_object c WHERE c.parent_id = o.id) = 0 WHERE o.parent_id IS NULL UNION ALL SELECT child.id, child.parent_id, child.name, p.id AS period_id, p.start_date, p.end_date, parent.node_level + 1 AS node_level FROM xml_object child JOIN xml_tree parent ON child.parent_id = parent.id LEFT JOIN xml_period p ON child.id = p.xml_object_id AND (SELECT COUNT(*) FROM xml_object c WHERE c.parent_id = child.id) = 0 ) SELECT * FROM xml_tree - 结果转换
手动编写代码或用Hibernate的ResultTransformer把平级结果转换成树形:- 先把所有节点按ID存入Map,避免重复
- 遍历节点,根据
parent_id找到父节点并添加到childObjects集合 - 给末级节点设置对应的XmlPeriod对象
各方案优缺点对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 方案1 | HQL语法兼容好,步骤清晰,完全规避笛卡尔积 | 需要两次查询,依赖Hibernate 5.2+ |
| 方案2 | 代码侵入性小(仅需加@BatchSize),无需复杂SQL | 需要手动遍历树形触发加载 |
| 方案3 | 一次性获取所有数据,查询逻辑灵活可控 | 需要手动处理结果转换,代码量较大 |
注意事项
- 所有操作必须在同一个事务/Session内完成,避免懒加载异常
@BatchSize的size值根据实际数据量调整,平衡性能和内存占用- 若数据库不支持递归CTE(如MySQL 5.x),可改用Java递归遍历+
@BatchSize的方案
内容的提问来源于stack exchange,提问作者Alex A

