You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hibernate中如何将递归查询结果自动映射为完整Category树形实体

问题描述

我们有一个Category实体,每个分类可包含0个或多个子分类(对应subcategories集合字段)。通过**公共表表达式(CTE)**编写的递归查询已能正确返回指定根节点下的全部分类列表,但希望找到一种优雅的方式,将查询结果自动映射为包含所有层级子分类的单个根Category实体,而非手动处理结果。已知@SqlResultSetMapping或实体图无法实现此需求。

简化代码示例

分类实体类

@Entity
@Table(name = "CATEGORY")
public class CategoryEntity {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    @Column(name = "C_I_IDF")
    protected Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="C_PARENTCATEGORY")
    private CategoryEntity parentCategory;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parentCategory", cascade = { CascadeType.MERGE, CascadeType.REMOVE })
    @Fetch(value = FetchMode.SELECT)
    @BatchSize(size=10)
    private List<CategoryEntity> subCategories = new ArrayList<>();

    // 省略getter/setter方法
}

递归查询语句

WITH categoryrecursive (id) AS (
    SELECT C_I_IDF
    FROM CATEGORY parent
    WHERE parent.C_I_IDF = <根节点ID>
    UNION ALL
    SELECT sub.C_I_IDF
    FROM categoryrecursive, CATEGORY sub
    WHERE sub.C_PARENTCATEGORY = categoryrecursive.id
)
SELECT cat.*
FROM categoryrecursive rc
INNER JOIN CATEGORY cat ON cat.C_I_IDF = rc.id

优雅解决方案

方案1:利用JPA持久化上下文自动关联

JPA的持久化上下文(EntityManager)会自动维护实体间的关联关系。当你通过查询加载所有分类实体后,上下文会根据parentCategory的映射自动填充subCategories集合:

  1. 执行CTE查询获取全部分类:
List<CategoryEntity> allCategories = entityManager.createNativeQuery(
    "WITH categoryrecursive (id) AS (...)" // 填入完整CTE语句
    , CategoryEntity.class
)
.setParameter("rootId", rootId)
.getResultList();
  1. 从结果中筛选根节点:
CategoryEntity root = allCategories.stream()
    .filter(cat -> cat.getParentCategory() == null || cat.getId().equals(rootId))
    .findFirst()
    .orElseThrow(() -> new IllegalArgumentException("根节点不存在"));

此时根节点的subCategories已自动组装好完整层级结构,无需手动处理。

注意:需确保实体的equals/hashCode实现正确,且查询全程使用同一个EntityManager实例。

方案2:自定义Hibernate ResultTransformer

如果使用Hibernate作为JPA实现,可以自定义结果转换器直接组装树形结构:

public class CategoryTreeTransformer implements ResultTransformer {

    private final Map<Long, CategoryEntity> categoryMap = new HashMap<>();
    private CategoryEntity root;

    @Override
    public Object transformTuple(Object[] tuple, String[] aliases) {
        CategoryEntity category = (CategoryEntity) tuple[0];
        categoryMap.put(category.getId(), category);
        
        // 关联子分类到父分类
        if (category.getParentCategory() != null) {
            CategoryEntity parent = categoryMap.get(category.getParentCategory().getId());
            if (parent != null) {
                parent.getSubCategories().add(category);
            }
        } else {
            root = category; // 无父分类的即为根节点
        }
        return category;
    }

    @Override
    public List transformList(List collection) {
        return Collections.singletonList(root);
    }
}

使用方式:

CategoryEntity root = (CategoryEntity) entityManager.createNativeQuery(
    "WITH categoryrecursive (id) AS (...)"
    , CategoryEntity.class
)
.setParameter("rootId", rootId)
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new CategoryTreeTransformer())
.getSingleResult();

方案3:使用Blaze-Persistence实现自动树形映射

Blaze-Persistence是增强JPA的工具库,支持将递归CTE查询直接映射为树形实体结构,无需手动组装:

CTE<CategoryEntity> recursiveCte = cte(CategoryEntity.class)
    .withRecursive("categoryrecursive")
    .from(CategoryEntity.class, "parent")
    .where("parent.id").eq(rootId)
    .select("parent.id")
    .unionAll()
    .from(CategoryEntity.class, "sub")
    .innerJoinOn("categoryrecursive", "rc").on("sub.parentCategory.id").eqExpression("rc.id")
    .select("sub.id")
    .end();

CategoryEntity root = criteriaBuilder.from(CategoryEntity.class, "cat")
    .innerJoinOn(recursiveCte, "rc").on("cat.id").eqExpression("rc.id")
    .fetch("subCategories", JoinType.LEFT)
    .where("cat.id").eq(rootId)
    .getSingleResult();

内容的提问来源于stack exchange,提问作者Bruno Vandekerkhove

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 11:05:18