加载递归对象图,不带 N+1 笛卡尔积,带 JPA 和休眠

2022-09-03 02:50:20

在将项目从 Ibatis 转换为 JPA 2.1 时,我遇到了一个问题,即我必须为一组对象加载一个完整的对象图,而无需命中 N+1 选择或出于性能原因使用笛卡尔积。

用户查询将生成 List<Task>,我需要确保在返回任务时,它们已填充所有属性,包括父项项、依赖项属性。首先让我解释一下所涉及的两个实体对象。

任务是层次结构的一部分。它可以有一个父任务,也可以有一个子任务。任务可以依赖于其他任务,由“依赖关系”属性表示。一个任务可以有许多属性,由属性属性表示。

示例对象已尽可能简化,并删除了样板代码。

@Entity
public class Task {
    @Id
    private Long id;

    @ManyToOne(fetch = LAZY)
    private Task parent;

    @ManyToOne(fetch = LAZY)
    private Task root;

    @OneToMany(mappedBy = "task")
    private List<TaskProperty> properties;

    @ManyToMany
    @JoinTable(name = "task_dependency", inverseJoinColumns = { @JoinColumn(name = "depends_on")})
    private List<Task> dependencies;

    @OneToMany(mappedBy = "parent")
    private List<Task> children;
}

@Entity
public class TaskPropertyValue {
    @Id
    private Long id;

    @ManyToOne(fetch = LAZY)
    private Task task;

    private String name;
    private String value;
}

给定任务的 Task 层次结构可以无限深,因此为了更轻松地获取整个图形,Task 将通过“root”属性具有指向其根任务的指针。

在 Ibatis 中,我只需获取根 ID 的不同列表的所有 Tasks,然后使用“task_id IN ()”查询对所有属性和依赖项进行即席查询。当我拥有这些时,我使用Java代码向所有模型对象添加属性,子级和依赖项,以便图形完整。对于任何大小的任务列表,我只会做3个SQL查询,我正在尝试对JPA做同样的事情。由于“parent”属性指示在何处添加子级,因此我甚至不必查询这些子级。

我尝试了不同的方法,包括:

让延迟加载完成它的工作

  • 表演自杀,无需詳細:)

加入 FETCH 子级,加入 FETCH 依赖项,加入 FETCH 属性

  • 这是有问题的,因为生成的笛卡尔积是巨大的,而我的JPA实现(Hibernate)不支持List,只在获取多个袋子时支持Set。一个任务可以有大量的属性,使笛卡尔积无效。

临时查询,就像我在 ibatis 中所做的那样

  • 我无法将子项,依赖项和属性添加到任务对象上的懒惰初始化集合中,因为Hibernate将尝试将它们添加为新对象。

一种可能的解决方案是创建不由JPA管理的新任务对象,并使用这些对象将我的层次结构缝合在一起,我想我可以接受这一点,但它感觉不是很“JPA”,然后我不能使用JPA来做它擅长的事情 - 自动跟踪和持久保存对我的对象的更改。

任何提示将不胜感激。如有必要,我愿意使用供应商专用扩展。我在Wildfly 8.1.0.Final(Java EE7完整配置文件)中运行,Hibernate 4.3.5.Final。


答案 1

可用选项

有一些策略可以实现您的目标:

  • 子选择提取将使用额外的子选择加载所有惰性实体,这是您第一次需要该给定类型的惰性关联时。这听起来很吸引人,但它会使你的应用容易受到要获取的其他子选择实体数量的影响,并可能传播到其他服务方法。

  • 批处理提取更易于控制,因为您可以强制在一个批处理中加载实体的数量,并且可能不会对其他用例产生太大影响。

  • 使用递归公用表表达式(如果您的数据库支持)。

提前计划

最后,这完全取决于您计划对所选行执行的操作。如果只是将它们显示在视图中,那么本机查询就足够了。

如果需要跨多个请求(首先是视图部分,第二个是更新部分)保留实体,则实体是更好的方法。

从您的回复中,我看到您需要发布一个,并且可能依靠级联来传播儿童的状态转换(添加/删除)。EntityManager.merge()

由于我们谈论的是3个JPA查询,只要你没有得到笛卡尔积,那么你应该对JPA没问题。

结论

您应该努力实现最少的查询量,但这并不意味着您将始终必须有一个且只有一个查询。两三个查询根本不是问题。

只要您控制查询号并且不陷入N + 1查询问题],您就可以使用多个查询。无论如何,交易一个笛卡尔积(2个一对多获取)进行一个连接和一个额外的选择是一笔好交易。

最后,您应该始终检查 EXPLAIN ANALYZE 查询计划并加强/重新考虑您的策略。


答案 2

推荐