当需要来自多个表的字段时,如何避免使用 Hibernate 在 JPA 中联接'explosions'?



假设我在 JpaRepository 中有以下方法:

@EntityGraph(value = "Subject.allJoins", type = EntityGraphType.FETCH)
@Query("select s from Subject s" + FIND_QUERY_WHERE)
Page<Subject> findInProject(@Param("projectId") UUID projectId, <additional params>

如您所见,我已经使用了一个带有所需连接的实体图。 Hibernate生成的SQL查询如下所示(其中大部分省略(

select
subject0_.id,
<all kinds of fields including some duplicates>
from
subject subject0_
left outer join project project1_ on subject0_.project_id = project1_.id
left outer join subject_property_value properties2_ on subject0_.id = properties2_.subject_id
left outer join property_value propertyva3_ on properties2_.property_value_id = propertyva3_.id
left outer join ingestion_id_mapping ingestedme4_ on subject0_.id = ingestedme4_.subject_id
where
subject0_.project_id = '123'
order by
subject0_.name asc

由于此处的所有联接都将结果乘以联接的结果,因此即使主题总数只有几百,结果集也会爆炸成数十万行。

请注意,我将进行投影,这已经避免选择某些字段,但仍需要连接。

我能做些什么来优化它?

请注意,我确实需要所有数据才能立即序列化到客户端,因此仅通过获取模型实体并为每个关联使用 Getter 方法将其留给 Hibernate 需要比这更长的时间。

我目前的想法是,对于每个单独的连接,我必须使用相同的位置多次进行查询,然后将结果合并到单个对象中。如果我在后续查询中读取更多或更少的行,因为原始表中的行添加或删除了,这并不是世界末日,因为我可以只获取主题 ID 的最小子集并从中得出结果。

但是,还有比这更聪明和/或更简单的事情吗?

我将以一个拥有国家、体育场和球员列表的足球俱乐部为例。

第一个查询只应用于筛选数据库中所需的行。 在这种情况下,您还可以获取 1:1 关系,但不能获取 1:n。 因此,在我的示例中,第一个查询应:

  • 筛选所有符合标准的俱乐部
  • 获取所有 1:1 关系(每个俱乐部的国家和体育场(。

然后,您可以为每个子列表创建一个专用列表。 仍然在我的示例中,您将选择其俱乐部在您提供的列表中作为查询参数的每个球员(即第一次查询的结果(。 查询将如下所示:

String jpql = "select p from Player p where p.club in :clubs";

这样做,您还可以提供实体图来加载玩家的属性。 当您使用分页处理时,这很有效(第一个查询的结果并不重要(。

弗拉德·米哈尔恰(Vlad Mihalcea(很好地描述了这种方式: 修复休眠MultipleBagFetchException的最佳方法

我强烈建议您看一看。

问题是,获取联接对每个相关实体/表进行子选择。相反,您应该只联接具有 1:1 关系的实体。然后,在首次访问其他实体时提取它们。这会导致每个主题一行,以及一个选择,每个不在初始选择中的实体都有 n 行。

如果子选择花费的时间过长,请尝试将记录数最少的实体添加到选择中。

这是 Blaze-Persistence 实体视图的完美用例。

我创建了该库,以允许JPA模型与自定义接口或抽象类定义的模型之间的轻松映射,类似于类固醇上的Spring Data Projections。这个想法是你按照你喜欢的方式定义你的目标结构(域模型(,并通过JPQL表达式将属性(getter(映射到实体模型。由于属性名称用作默认映射,因此您通常不需要显式映射,因为 80% 的用例是具有作为实体模型子集的 DTO。

有趣的是,您可以指定应该使用的获取策略。示例模型可能如下所示:

@EntityView(Subject.class)
public interface SubjectView {
@IdMapping
Integer getId();
ProjectView getProject();
@Mapping(fetch = SUBSELECT)
Set<PropertyValueView> getProperties();
Set<IngestionMappingView> getMappings();
}
@EntityView(Project.class)
public interface ProjectView {
@IdMapping
Integer getId();
String getName();
}
@EntityView(PropertyValue.class)
public interface PropertyValueView {
@IdMapping
Integer getId();
String getName();
}
@EntityView(IngestionMapping.class)
public interface IngestionMappingView {
@IdMapping
Integer getId();
String getName();
}

查询是将实体视图应用于查询的问题,最简单的只是按 id 进行查询。

SubjectView p = entityViewManager.find(entityManager, SubjectView.class, id);

Spring 数据集成允许您像 Spring 数据投影一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features 即拥有类似于以下内容的存储库

@Repository
public interface SubjectRepository {
Page<SubjectView> findByProjectId(@Param("projectId") UUID projectId, <additional params>);
}

您可以在实体视图文档中阅读有关支持的获取策略的更多信息,如果可能,我通常建议您使用MULTISET提取策略,因为这通常可提供最佳性能。

最新更新