使用JPA和Hibernate加载没有N + 1 Cartesian产品的递归对象图 [英] Load recursive object graph without N+1 Cartesian Product with JPA and Hibernate

查看:93
本文介绍了使用JPA和Hibernate加载没有N + 1 Cartesian产品的递归对象图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

将一个项目从Ibatis转换到JPA 2.1时,我遇到了一个问题,那就是我必须为一组对象加载一个完整的对象图,而不会因性能原因而击中N + 1选择或使用笛卡尔产品。 / p>

用户查询将产生一个List< Task>,并且我需要确保当我返回这些任务时,它们具有所有填充的属性,包括 parent 儿童依赖关系属性。首先,让我解释涉及的两个实体对象。

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



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

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

@ManyToOne(fetch = LAZY)
私人任务父母;

@ManyToOne(fetch = LAZY)
私人任务根;

@OneToMany(mappedBy =task)
private List< TaskProperty>性能;

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

@OneToMany(mappedBy =parent)
private List< Task>儿童;
}

@实体
公共类TaskPropertyValue {
@Id
私人长ID;

@ManyToOne(fetch = LAZY)
私人任务任务;

私人字符串名称;
私有字符串值;

$ / code>

给定任务的任务层次结构可以是无限深的,所以要做到这一点更容易获得整个图,任务将通过'root'属性指向它的根任务。



在Ibatis中,我简单地提取了所有任务根id的列表,然后使用task_id IN()查询对所有属性和依赖项进行临时查询。当我有这些时,我使用Java代码为所有模型对象添加属性,子对象和依赖关系,以使图形完整。对于任何大小的任务列表,我只会做3个SQL查询,并且我正在尝试对JPA执行相同的操作。由于'parent'属性指示要添加子项的位置,因此我甚至不必查询这些子项。



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



让懒加载做它的工作




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



JOIN FETCH子项,JOIN FETCH依赖项,JOIN FETCH属性




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



即席查询的方式与我在ibatis




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



一种可能的解决方案是创建不由JPA管理的新Task对象,并使用这些对象来缝制我的层次结构,我想我可以忍受这一点,但它不会感觉到JPA,然后我不能使用JPA来处理它的优点 - 自动跟踪和保存对对象的更改。



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

解决方案

可用选项



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


  • 子选择抓取会使用附加的子选择加载所有懒惰实体,这是您第一次需要该给定类型的惰性关联。这听起来很有吸引力,但它使您的应用程序易于获取其他子选择实体的数量,并可能会传播到其他服务方法。

  • a href =https://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch20.html#performance-fetching-batch =nofollow noreferrer>批量抓取更容易控制,因为您可以在一个批次中强制实施要加载的实体数量,并且可能不会影响太多的其他使用情况。 使用 如果您的数据库支持它,请递归公用表表达式

    提前计划



    最后,所有关于什么您计划对选定的行进行操作。如果只是将它们显示在视图中,则比原生查询是绰绰有余的



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



    从你的回应中我发现你需要发行一个 EntityManager.merge() 依靠级联传播孩子状态转换(添加/删除)。

    由于我们谈论的是3个JPA查询,并且只要您没有获得笛卡儿积,您应该可以使用JPA。

    结论



    您应该争取最少的查询量,但并不意味着您将永远拥有只有一个查询。两三个查询根本不是问题。

    只要你控制查询号码,不要进入 N + 1查询问题你也可以使用多个查询。交易笛卡尔产品(2个一对多提取)用于一个连接和一个额外的选择是一件好事。



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

    When converting a project from Ibatis to JPA 2.1, I'm faced with a problem where I have to load a complete object graph for a set of objects, without hitting N+1 selects or using cartesian products for performance reasons.

    A users query will yield a List<Task>, and I need to make sure that when I return the tasks, they have all properties populated, including parent, children, dependencies and properties. First let me explain the two entity objects involved.

    A Task is part of a hierarchy. It can have a parent Task and it can also have children. A Task can be dependent on other tasks, expressed by the 'dependencies' property. A task can have many properties, expressed by the properties property.

    The example objects have been simplified as much as possible and boilerplate code is removed.

    @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;
    }
    

    The Task hierarchy for a given task can be infinitely deep, so to make it easier to get the whole graph, a Task will have a pointer to it's root task via the 'root' property.

    In Ibatis, I simply fetched all Tasks for the distinct list of root id's, and then did ad-hoc queries for all properties and dependencies with a "task_id IN ()" query. When I had those, I used Java code to add properties, children and dependencies to all model objects so that the graph was complete. For any size list of tasks, I would then only do 3 SQL queries, and I'm trying to do the same with JPA. Since the 'parent' property indicates where to add the children, I didn't even have to query for those.

    I've tried different approaches, including:

    Let lazy loading do it's job

    • Performance suicide, no need to elaborate :)

    JOIN FETCH children, JOIN FETCH dependences, JOIN FETCH properties

    • This is problematic because the resulting cartesian products are huge, and my JPA implementation (Hibernate) doesn't support List, only Set when fetching multiple bags. A task can have a huge number of properties, making the cartesian products ineffective.

    Ad-hoc queries the same way I did in ibatis

    • I cannot add children, dependencies and properties to the Lazy initialized collections on the Task objects, because Hibernate will then try to add them as new objects.

    One possible solution could be to create new Task objects that are not managed by JPA and sew my hierarchy together using those, and I guess I can live with that, but it doesn't feel very "JPA", and then I couldn't use JPA for what it's good at - tracking and persisting changes to my objects automatically.

    Any hints would be greatly appreciated. I'm open to using vendor spesific extensions if necessary. I'm running in Wildfly 8.1.0.Final (Java EE7 Full Profile) with Hibernate 4.3.5.Final.

    解决方案

    Available options

    There are some strategies to achieve your goals:

    • sub-select fetching would load all lazy entities with an additional sub-select, the very first time you need a lazy association of that given type. This sound appealing at first, but it makes your app fragile to the number of additional sub-select entities to fetch and may propagate to other service methods.

    • batch fetching is easier to control, since you can enforce the number of entities to be loaded in one batch and might not affect too much other use cases.

    • using a recursive common table expression if your db supports it.

    Plan ahead

    In the end, it's all about what you plan on doing with the selected rows. If it's just about displaying them into a view, than a native query is more than enough.

    If you need to retain the entities across multiple requests (first the view part, the second for the update part) than entities are a better approach.

    From your response I see you need to issue an EntityManager.merge() and probably rely on cascading to propagate children state transitions (add/remove).

    Since we are talking about 3 JPA queries, and as long as you don't get a Cartesian Product than you should be fine with JPA.

    Conclusion

    You should strive for the minimum amount of queries but it doesn't mean you will always have to have one and only one query. Two or three queries is not an issue at all.

    As long as you control the queries number and don't get into a N+1 query issue you are fine with more than one query too. Trading a Cartesian Product (2 one-to-many fetches) for one join and one additional select is a good deal anyway.

    In the end you should always check the EXPLAIN ANALYZE query plan and reinforce/rethink your strategy.

    这篇关于使用JPA和Hibernate加载没有N + 1 Cartesian产品的递归对象图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆