加载没有N + 1 Cartesian Product with JPA和Hibernate的递归对象图

Edv*_*yse 16 java hibernate jpa lazy-loading java-ee-7

将项目从Ibatis转换为JPA 2.1时,我遇到了一个问题,我必须为一组对象加载一个完整的对象图,而不是出于性能原因而没有按N + 1选择或使用笛卡尔积.

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

任务是层次结构的一部分.它可以有一个父任务,也可以有子.任务可以依赖于其他任务,由'dependencies'属性表示.任务可以有许多属性,由properties属性表示.

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

@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;
}
Run Code Online (Sandbox Code Playgroud)

给定任务的Task层次结构可以无限深,因此为了更容易获得整个图形,Task将通过"root"属性指向它的根任务.

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

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

让延迟加载做它的工作

  • 表现自杀,无需详细说明:)

JOIN FETCH子节点,JOIN FETCH依赖节点,JOIN FETCH属性

  • 这是有问题的,因为产生的笛卡尔积很大,而我的JPA实现(Hibernate)不支持List,只有在获取多个包时才设置.任务可能具有大量属性,使得笛卡尔产品无效.

Ad-hoc查询方式与我在ibatis中的方式相同

  • 我不能将子,依赖项和属性添加到Task对象上的Lazy初始化集合,因为Hibernate将尝试将它们添加为新对象.

一个可能的解决方案可能是创建不由JPA管理的新Task对象,并使用这些对象将我的层次结构缝合在一起,我想我可以忍受它,但它感觉不到非常"JPA",然后我不能使用JPA擅长 - 自动跟踪和持久更改我的对象.

任何提示将不胜感激.如果有必要,我愿意使用供应商特定扩展.我使用Hibernate 4.3.5.Final在Wildfly 8.1.0.Final(Java EE7 Full Profile)中运行.

Vla*_*cea 9

可用选项

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

  • 子选择提取将使用额外的子选择加载所有延迟实体,这是您第一次需要该给定类型的延迟关联.这个声音起初很吸引人,但它使你的应用程序易于获取额外的子选择实体的数量,并可能传播到其他服务方法.

  • 批量提取更容易控制,因为您可以在一个批处理中强制实施要加载的实体数量,并且可能不会影响太多其他用例.

  • 如果您的数据库支持,则使用递归公用表表达式.

未雨绸缪

最后,所有关于您计划对所选行进行的操作都是如此.如果它只是将它们显示在视图中,那么本机查询就足够了.

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

根据您的回复,我发现您需要发布一个EntityManager.merge()并且可能依赖级联来传播子状态转换(添加/删除).

因为我们正在讨论3个JPA查询,并且只要你没有获得笛卡尔积,那么你应该使用JPA.

结论

您应该争取最少量的查询,但这并不意味着您将始终必须只有一个查询.两个或三个查询根本不是问题.

只要您控制查询号码并且没有进入N + 1查询问题,您也可以使用多个查询.交易一个笛卡尔积(2个一对多的提取)进行一次连接和一次额外选择是一个很好的交易.

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