递归JPA查询?

17

JPA 2是否有运行递归查询的机制?

这是我的情况:我有一个实体E,其中包含一个名为x的整数字段。 它还可能拥有类型为E的子级,通过@OneToMany进行映射。 我想做的是通过主键找到一个E,并获取其x值及所有后代的x值。 是否有办法在单个查询中完成此操作?

我正在使用Hibernate 3.5.3,但我不希望显式依赖于Hibernate API。


编辑:根据文章,Hibernate没有此功能,至少在三月份没有。 因此,JPA似乎也不会有这个功能,但我想确保。

4个回答

30

使用简单的邻接模型,其中每一行包含对其父项的引用,该引用将引用同一表中的另一行,与JPA不协作良好。这是因为JPA不支持使用Oracle CONNECT BY子句或SQL标准WITH语句生成查询。如果没有这两个子句中的任何一个,那么就无法真正地发挥邻接模型的用途。

但是,还有一些其他方法可以应用于这个问题的建模。第一种方法是材料化路径模型。这是将节点的完整路径展开到单个列中的模型。表定义如下:

CREATE TABLE node (id INTEGER,
                   path VARCHAR, 
                   parent_id INTEGER REFERENCES node(id));

插入一棵节点树的代码大致如下:

INSERT INTO node VALUES (1, '1', NULL);  -- Root Node
INSERT INTO node VALUES (2, '1.2', 1);   -- 1st Child of '1'
INSERT INTO node VALUES (3, '1.3', 1);   -- 2nd Child of '1'
INSERT INTO node VALUES (4, '1.3.4', 3); -- Child of '3'

要获取节点'1'及其所有子级,查询语句如下:

SELECT * FROM node WHERE id = 1 OR path LIKE '1.%';

要将此映射到JPA,只需将“path”列作为持久化对象的属性。但是您将需要进行簿记以保持“path”字段的最新状态。 JPA / Hibernate不会为您执行此操作。例如,如果将节点移动到不同的父级,则必须同时更新父级引用并从新父对象确定新路径值。

另一种方法称为“嵌套集模型”,但稍微复杂一些。可能最好由其发明者进行描述(而不是由我直接添加)。

还有第三种方法称为嵌套间隔模型,但是这种方法对于实现具有重大依赖性存储过程。

关于此问题的更完整解释,请参阅The Art of SQL的第7章。


这比我寻找的要复杂得多,但感谢您提供的信息。材料化路径模型还有一个更简单的替代方案 - 每个节点存储其所属树的根节点的ID。这意味着您只能使用根节点作为查询的起点,但在我的特定情况下,这不是问题 - 所以这可能是我要做的事情。 - Mike Baranczak
你可以使用通过JPA管理的邻接表,但是使用本地查询来使用递归SQL功能。这可能有点棘手,而且不可移植,但它将是两全其美的最佳选择之一。嵌套集合是可移植的,但有些常见的查询类型性能不太好。 - Tom Anderson
JPA 2.0 有什么变化吗?例如,我已经使用了本地 NamedQuery 在我的查询中使用 'CONNECT BY' 子句。我已经尝试过它,它确实返回了子项,但不是以分层列表形式...只是平面子项。有什么想法吗? - SoftwareSavant
我将使用JPA持久化上下文http://www.tikalk.com/java/load-a-tree-with-jpa-and-hibernate,需要一个虚假的根节点。 - Juan Rojas
嗨,你能更新一下“嵌套集模型”的链接吗? - Ivan Baranuk

9
这篇文章中最好的答案对我来说似乎是一个巨大的解决方案。我已经处理过数据模型,其中优秀的工程师们决定将树形结构编码为文本,例如“Europe|Uk|Shop1|John”,并且这些表中包含大量的数据。毫不奇怪,查询类型为MyHackedTreeField LIKE 'parentHierharchy%'的性能表现很差。 解决这种问题最终需要创建内存缓存,以及其他许多方面。
如果您需要运行递归查询并且数据量不大...请简化您的生活,只需加载您需要运行计划的数据库字段。并在Java中编写您的递归代码。除非您有充分的理由进行操作,否则不要在数据库中进行。
即使您拥有大量数据,您也很可能可以将问题分成独立的递归树批次,并逐个处理这些问题,而无需一次性加载所有数据。

3

我知道这个问题很久了,但因为它被链接到另一个问题中,所以我想就此问题进行更新。现在Blaze-Persistence支持在JPA模型上使用递归CTE。

Blaze-Persistence是一个建立在JPA之上的查询构建器,支持许多高级的DBMS功能。要建模CTEs或递归CTEs(即您在此处所需的内容),首先需要引入一个CTE实体来建模CTE的结果类型。

@CTE
@Entity
public class GroupCTE {
  @Id Integer id;
}

一条获取组织层级结构的查询可能如下所示:
List<Group> groups = criteriaBuilderFactory.create(entityManager, Group.class)
  .withRecursive(GroupCTE.class)
    .from(Group.class, "g1")
    .bind("id").select("g1.id")
    .where("g1.parent").isNull()
  .unionAll()
    .from(Group.class, "g2")
    .innerJoinOn(GroupCTE.class, "cte")
      .on("cte.id").eq("g2.parent.id")
    .end()
    .bind("id").select("g2.id")
  .end()
  .from(Group.class, "g")
  .fetch("groups")
  .where("g.id").in()
    .from(GroupCTE.class, "c")
    .select("c.id")
  .end()
  .getResultList();

这将渲染为SQL,类似于以下内容。
WITH RECURSIVE GroupCTE(id) AS (
    SELECT g1.id
    FROM Group g1
    WHERE g1.parent_group_id IS NULL
  UNION ALL
    SELECT g2.id
    FROM Group g2
    INNER JOIN GroupCTE cte ON g2.parent_group_id = cte.id
)
SELECT *
FROM Group g
LEFT JOIN Group gsub ON gsub.parent_group_id = g.id
WHERE g.id IN (
  SELECT c.id
  FROM GroupCTE c
)

您可以在文档中了解有关递归CTE的更多信息:https://persistence.blazebit.com/documentation/core/manual/en_US/index.html#recursive-ctes


1

我曾遇到过这样的问题,需要从一个表中查询菜单节点。解决方法是:假设我们有一个名为Node的类,创建了一个单向一对多的关联,如下所示:

    @OneToMany(  fetch = FetchType.EAGER)
    @JoinColumn(name = "parent_id", referencedColumnName = "id")
    private List<Node> subNodeList;

还可以在实体中添加一个名为 boolean isRoot 的字段,以说明此节点是否为根菜单项,然后通过查询 isRoot 为 true 的节点,我们就可以获得顶级节点,因为使用了 FetchType.EAGER ,我们也会获取到列表中的子节点。 这将导致多个查询,但对于小型菜单之类的东西来说,这应该没问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接