我遇到了在合理的时间内从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)实现这一点。
我尝试了以下方法:
使用获取加入,例如from User u join fetch u.oranges ...
。这太糟糕了。结果集为30 * 30 * 30 * 30,执行时间为10秒。请求数量= 3.我尝试没有葡萄,葡萄你会得到x10大小的结果集。
只需使用延迟加载。这是此示例中的最佳结果(使用@ Fetch = 葡萄的SUBSELECT)。但在这种情况下,我需要手动迭代每个元素集合。此外,subselect fetch太全局设置,所以我想有一些可以在查询级别工作的东西。结果集和时间接近理想。 6个查询和43毫秒。
使用实体图表加载。与获取连接相同,但它也要求每种葡萄都能获得葡萄藤。但是,结果时间更好(6秒),但仍然很糟糕。请求数> 30。
我试图在单独的查询中通过“手动”加载实体来欺骗JPA。像:
SELECT u FROM User where id=1; SELECT a FROM Apple where a.user_id=1;
延迟加载有点糟糕,因为它需要对每个集合进行两次查询:第一次查询到手动加载实体(我完全控制这个查询,包括加载相关实体),第二次查询到延迟加载Hibernate本身使用相同的实体(这是由Hibernate自动执行的)
执行时间为52,查询数量= 10(用户为1,葡萄为1,每个水果收集为4 * 2)
实际上,“手动”解决方案结合SUBSELECT fetch允许我使用“简单”提取连接在一个查询中加载必要的实体(如@OneToOne
实体)所以我将使用它。但我不喜欢我必须执行两个查询来加载集合。
有什么建议吗?
答案 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做一些它不能做的事情,而且当它不能做的时候,你想要一个远非最佳的黑客解决方案。为什么不放松限制并获得有效的东西?你为什么一开始就有这些限制?
一些一般性指示:
user.getOranges().size()
或Hibernate.initialize(user.getOranges())
。对于葡萄藤,你必须迭代以初始化所有的葡萄。通过适当的数据库设计和在正确位置的延迟加载,除了以下任何内容之外不应该需要:
em.find(User.class, userId);
如果延迟加载需要花费很多时间,那么可能是一个连接获取查询。
根据我的经验,加速Hibernate的最重要因素是数据库中的搜索索引。