Hibernate:复杂对象的初始化

时间:2017-11-30 13:06:46

标签: java hibernate jpa optimization

我遇到了在合理的时间内从DB中完全加载非常复杂的对象并且查询数量合理的问题。

我的对象有很多嵌入式实体,每个实体都引用另一个实体,另一个实体引用另一个实体等等(所以,嵌套级别为6)

所以,我已经创建了一个示例来演示我想要的东西: https://github.com/gladorange/hibernate-lazy-loading

我有用户。

用户拥有最喜欢的橙子,苹果,葡萄藤和桃子@OneToMany个收藏品。每个Grapevine都有@OneToMany个葡萄收藏。每个水果都是另一个只有一个String字段的实体。

我正在创造用户,每种类型有30种最喜欢的水果,每种葡萄都有10种葡萄。所以,我在DB中有421个实体--30 * 4个水果,100 * 30个葡萄和一个用户。

我想要的是:我想使用不超过6个SQL查询来加载它们。 并且每个查询都不应该产生大的结果集(大的是一个结果集,该例子中有超过200条记录)。

我理想的解决方案如下:

  • 6个请求。第一个请求返回有关用户和结果集大小的信息为1.

  • 有关此用户的苹果的第二个请求返回信息以及结果集的大小为30。

  • 第三,第四和第五个请求返回相同,第二个(结果集大小= 30),但对于Grapevines,Oranges和Peaches。

  • 第六个请求返回所有葡萄藤的葡萄

这在SQL世界中非常简单,但我无法用JPA(Hibernate)实现这一点。

我尝试了以下方法:

  1. 使用获取加入,例如from User u join fetch u.oranges ...。这太糟糕了。结果集为30 * 30 * 30 * 30,执行时间为10秒。请求数量= 3.我尝试没有葡萄,葡萄你会得到x10大小的结果集。

  2. 只需使用延迟加载。这是此示例中的最佳结果(使用@ Fetch = 葡萄的SUBSELECT)。但在这种情况下,我需要手动迭代每个元素集合。此外,subselect fetch太全局设置,所以我想有一些可以在查询级别工作的东西。结果集和时间接近理想。 6个查询和43毫秒。

  3. 使用实体图表加载。与获取连接相同,但它也要求每种葡萄都能获得葡萄藤。但是,结果时间更好(6秒),但仍然很糟糕。请求数> 30。

  4. 我试图在单独的查询中通过“手动”加载实体来欺骗JPA。像:

    SELECT u FROM User where id=1;
    SELECT a FROM Apple where a.user_id=1;
    
  5. 延迟加载有点糟糕,因为它需要对每个集合进行两次查询:第一次查询到手动加载实体(我完全控制这个查询,包括加载相关实体),第二次查询到延迟加载Hibernate本身使用相同的实体(这是由Hibernate自动执行的)

    执行时间为52,查询数量= 10(用户为1,葡萄为1,每个水果收集为4 * 2)

    实际上,“手动”解决方案结合SUBSELECT fetch允许我使用“简单”提取连接在一个查询中加载必要的实体(如@OneToOne实体)所以我将使用它。但我不喜欢我必须执行两个查询来加载集合。

    有什么建议吗?

3 个答案:

答案 0 :(得分:5)

对于实体和集合,我通常使用batch fetching来覆盖99%的此类用例。如果您在读取它们的同一事务/会话中处理获取的实体,那么您不需要另外执行任何操作,只需导航到处理逻辑所需的关联,生成的查询将是非常最佳的。如果要将已获取的实体作为分离返回,则手动初始化关联:

User user = entityManager.find(User.class, userId);
Hibernate.initialize(user.getOranges());
Hibernate.initialize(user.getApples());
Hibernate.initialize(user.getGrapevines());
Hibernate.initialize(user.getPeaches());
user.getGrapevines().forEach(grapevine -> Hibernate.initialize(grapevine.getGrapes()));

请注意,最后一个命令将实际执行每个小道消息的查询,因为初始化时会初始化多个grapes集合(直到指定的@BatchSize)第一。您只需迭代所有这些以确保所有这些都已初始化。

这种技术类似于您的手动方法,但更有效(查询不会针对每个集合重复),并且在我看来更易读和可维护(您只需调用Hibernate.initialize而不是手动编写与Hibernate相同的查询自动生成)。

答案 1 :(得分:3)

我将建议另一个关于如何在Grapevine中懒惰地获取Grapes集合的选项:

@OneToMany
@BatchSize(size = 30)
private List<Grape> grapes = new ArrayList<>();

不是进行子选择,而是使用in (?, ?, etc)一次获取许多Grape个集合。而是将传递? Grapevine ID。这与一次查询1 List<Grape>集合相反。

这只是你的武器库的另一种技术。

答案 2 :(得分:0)

我不太明白你的要求。在我看来,你希望Hibernate做一些它不能做的事情,而且当它不能做的时候,你想要一个远非最佳的黑客解决方案。为什么不放松限制并获得有效的东西?你为什么一开始就有这些限制?

一些一般性指示:

  1. 使用Hibernate / JPA时,您无法控制查询。你不应该(除了少数例外)。有多少查询,它们执行的顺序等等,几乎无法控制。如果要完全控制查询,只需跳过JPA并使用JDBC(例如Spring JDBC)。
  2. 了解延迟加载是在这种情况下做出决策的关键。获取拥有实体时,延迟加载的关系获取,而是Hibernate返回数据库并在实际使用时获取它们。这意味着,如果您不是每次都使用该属性,那么延迟加载会得到回报,但实际使用它时会受到惩罚。 (获取连接用于急切获取惰性关系。不适用于数据库中的常规加载。)
  3. 使用Hibernate查询优化不应该是您的第一行动作。始终从数据库开始。它是否正确建模,主键和外键,普通表格等?您是否在适当的位置(通常在外键上)有搜索索引?
  4. 在非常有限的数据集上测试性能可能无法获得最佳结果。可能会有连接等的开销,这将超过实际运行查询所花费的时间。此外,可能存在花费几毫秒的随机hickup,这将产生可能误导的结果。
  5. 查看代码的小提示:永远不要为实体中的集合提供setter。如果在事务中实际调用,Hibernate将抛出异常。
  6. tryManualLoading可能比您想象的更多。首先,它获取用户(使用延迟加载),然后获取每个水果,然后通过延迟加载再次获取水果。 (除非Hibernate知道查询与延迟加载时相同。)
  7. 您实际上不必遍历整个集合以启动延迟加载。您可以执行此操作user.getOranges().size()Hibernate.initialize(user.getOranges())。对于葡萄藤,你必须迭代以初始化所有的葡萄。
  8. 通过适当的数据库设计和在正确位置的延迟加载,除了以下任何内容之外不应该需要:

    em.find(User.class, userId);
    

    如果延迟加载需要花费很多时间,那么可能是一个连接获取查询。

    根据我的经验,加速Hibernate的最重要因素是数据库中的搜索索引