Hibernate:复杂对象的初始化

9
我在合理的时间内,使用合理的查询数量,无法完整加载一些非常复杂的对象。我的对象有许多嵌套实体,每个实体都引用另一个实体,而另一个实体又引用另一个实体,以此类推(因此,嵌套级别为6)。我创建了一个示例来演示我的要求:https://github.com/gladorange/hibernate-lazy-loading
我有一个用户。用户有喜欢的橘子、苹果、葡萄和桃子集合。每个葡萄树都有一组葡萄。每个水果都是另一个只有一个字符串字段的实体。我创建了一个拥有每种类型30个喜爱水果的用户,并且每个葡萄树有10个葡萄。所以,在数据库中我有421个实体- 30×4个水果,100×30个葡萄和一个用户。
我的目标是:使用不超过6个SQL查询来加载它们。每个查询不应生成大的结果集(对于这个例子,大于200条记录就算是大结果集了)。
我的理想解决方案如下:
  • 6个请求。第一个请求返回关于用户的信息,结果集大小为1。

  • 第二个请求返回此用户喜欢的苹果的信息,结果集大小为30。

  • 第三、四、五个请求返回与第二个请求相同的信息(结果集大小为30),但是分别返回葡萄树,橘子和桃子的信息。

  • 第六个请求返回所有葡萄树的葡萄信息。

在SQL世界中非常简单,但我无法使用JPA(Hibernate)实现这样的目标。我尝试了以下方法:
  1. 使用fetch join,例如from User u join fetch u.oranges ...。这种方式效果很差。结果集大小为30*30*30*30,执行时间为10秒。请求数量=3。我尝试了没有用葡萄的情况,但加上葡萄会使结果集大小增加10倍。

  2. 只需使用lazy loading即可。在此示例中,这是最好的结果(对于带有@Fetch=SUBSELECT的葡萄)。但在这种情况下,我需要手动遍历每个元素的集合。而且,子选择提取太过全局化,所以我想要一些可以在查询级别上工作的东西。结果集和时间接近理想状态。6个查询和43毫秒。

  3. 使用实体图加载。与fetch join相同,但它还会为每个葡萄请求其葡萄藤。然而,结果时间更好(6秒),但仍然很差。请求数量>30。

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

    SELECT u FROM User where id=1;
    SELECT a FROM Apple where a.user_id=1;
    

这比lazy loading略差一些,因为对于每个集合,它需要两个查询:第一个查询手动加载实体(我完全控制此查询,包括加载关联实体),第二个查询由Hibernate自己懒加载相同的实体(Hibernate会自动执行此操作)。

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

实际上,“手动”解决方案与SUBSELECT fetch结合使用,允许我使用“简单”的fetch join在一次查询中加载所需实体(例如@OneToOne实体)。因此我打算使用它。但我不喜欢必须执行两个查询来加载集合的方式。

有什么建议吗?


我会手动完成5个查询:首先使用急切的@OneToMany将第一个和第二个组合,然后按照描述的请求3-6每个执行一个查询,最后在Java代码中组装一个对象。当然,这并不太优雅,但不需要@OneToMany乘法或懒加载。 - Roman Puchkovskiy
@RomanPuchkovskiy 是的,这很有道理。但我还需要在加载和编辑后保存实例的能力。所以,它可能有效,但有时会出现诸如“未找到异常”之类的异常(因为它尝试在旧集合中搜索实体)。所以,我决定不使用手动对象构造。但我认为,你的建议应该适用于只读操作。 - EvilOrange
3个回答

7

我通常通过使用批量获取来覆盖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自动生成的相同查询)。

4

我将向您建议另一种方法来懒惰地获取Grapevine中的葡萄集合:

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

这里使用in (?, ?, 等等)来一次性获取多个Grape集合,而不是使用子查询。传递的是?葡萄藤ID。这与一次查询1个List<Grape>集合相反。

这只是你工具库中的另一种技术。


请谨慎使用,如果您已经加载了30个“葡萄藤”,但只对其中一个“葡萄藤”的“葡萄”感兴趣,那么加载这些“葡萄”也将导致其他29个“葡萄藤”的“葡萄”被加载。如果您想查看的“葡萄藤”有3个“葡萄”,而另一个“葡萄藤”有100,000个“葡萄”,那么您最终将加载比实际需要更多的内容。 - Tobb

0

我不太明白你在这里的要求。在我看来,你想让Hibernate做一些它没有设计做的事情,当它无法做到时,你想要一个远非最佳的黑客解决方案。为什么不放松限制,获得可行的东西呢?为什么你一开始就有这些限制呢?

以下是一些一般性的指导:

  1. 使用Hibernate/JPA时,您无法控制查询。您也不应该这样做(有一些例外情况)。查询的数量、执行顺序等几乎超出了您的控制范围。如果您想完全控制查询,请跳过JPA并改用JDBC(例如Spring JDBC)。
  2. 理解延迟加载对于在这种情况下做出决策至关重要。当获取拥有实体时,不会获取延迟加载的关系,而是Hibernate在实际使用它们时返回到数据库并获取它们。这意味着如果您不总是使用属性,则延迟加载会产生回报,但每次实际使用它时都会有一个惩罚。(使用Fetch join来急切获取延迟关系。不适用于从数据库中常规加载。)
  3. 使用Hibernate进行查询优化不应该是您的首选行动。始终从您的数据库开始。它是否正确建模,具有主键和外键,正常形式等?您是否在适当的位置(通常是在外键上)拥有搜索索引?
  4. 在非常有限的数据集上进行性能测试可能不会得到最佳结果。连接等方面可能会有开销,这将比实际运行查询所花费的时间更大。此外,可能会有一些随机故障,这些故障会耗费几毫秒的时间,这将导致可能具有误导性的结果。
  5. 从查看您的代码得出的小提示:永远不要为实体中的集合提供setter。如果在事务内实际调用,Hibernate将抛出异常。
  6. tryManualLoading可能比您想象的要多。首先,它获取用户(使用延迟加载),然后获取每个水果,然后再通过延迟加载获取水果。(除非Hibernate理解查询与延迟加载时相同。)
  7. 您实际上不必遍历整个集合才能启动延迟加载。您可以执行user.getOranges().size()Hibernate.initialize(user.getOranges())。对于葡萄树,您需要迭代以初始化所有葡萄。

通过适当的数据库设计和在正确的位置进行延迟加载,除了以下内容外,不应该需要任何其他东西:

em.find(User.class, userId);

如果懒加载需要很长时间,那么可以考虑使用联合抓取查询。

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


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