在 Hibernate 中获取几个 OneToMany 关系(与 Ebean 获取相反)

Fetch several OneToMany relations in Hibernate (opposed to Ebean fetch)

我有一个生产项目,它使用相当古老的 Ebean ORM(来自 Play Framework)。 Out 团队决定寻求迁移到更新的工具。 在我们的代码中,我们有很多 ORM 模型,并且通常有巨大的实体图(在一个“嵌套级别”上最多有 20 个 OneToMany 关系,每个嵌套最多 3 层深,这是很多关系,那应该急切地获取以避免 N+1 问题)。 我们当前的框架允许我们编写非常简洁的代码来获取 OneToMany 关系,假设示例:

@Entity
public class A {
   @OneToMany
   private List<B> bs;

   @OneToMany
   private List<C> cs;
}

查询代码:

Ebean.find(A.class)
     .fetch("bs", new FetchConfig().query())
     .fetch("cs", new FetchConfig().query())
     ... etc

该代码将产生 3 个数据库查询 - 一个用于 class A,两个用于关系;然后 Ebean 会自动组合这些查询的结果。

我尝试使用 JPA Criteria API 和 NamedEntityGraphs 在 Hibernate ORM 中生成这种代码,但未能成功 - 似乎 Hibernate 不喜欢同时获取多个 OneToMany 关系(通过产生类似 MultipleBagFetchException 的东西)。我明白为什么会引发此异常(笛卡尔积),但我找不到框架的一部分,它可以在多个数据库查询中拆分一个实体图。

是否可以在 Hibernate 中执行?如果不是,是否有任何第三方依赖项可以这样做? Hibernate 用户如何处理大实体图?

根据我的经验,大型实体图(主要用于用户无法消化大量数据的网络应用程序)相当罕见,但大多数时候您可以配置适当的批处理大小或使用 @Fetch(SUBSELECT) 以提高选择多个集合时的性能。 ListSet 的问题具体在于列表可以允许重复且无序的事实,即您无法区分第一个和第二个重复项。当您 join fetch a bag 然后 join fetch another bag 时,您会在 JDBC 结果集级别获得两个包中行的组合,这样您就无法再区分对象,这可能会导致错误的基数。要解决这个问题,您可以使用 Set 来确保没有重复项,或者定义一个索引列 @OrderColumn 来区分重复项。

除此之外,我认为这是 Blaze-Persistence Entity Views and its MULTISET fetch strategy 的完美用例,它就像是非常高效的连接抓取和子选择抓取的混合体。

我创建了库以允许在 JPA 模型和自定义接口或抽象 class 定义的模型之间轻松映射,类似于 Spring 类固醇数据投影。这个想法是您按照自己喜欢的方式定义目标结构(领域模型),并通过 JPQL 表达式将属性(getter)映射到实体模型。

您的用例的 DTO 模型与 Blaze-Persistence 实体视图类似:

@EntityView(A.class)
public interface ADto {
    @IdMapping
    Long getId();
    String getName();
    @Mapping(fetch = MULTISET)
    List<BDto> getBs();
    @Mapping(fetch = MULTISET)
    List<CDto> getCs();

    @EntityView(B.class)
    interface BDto {
        @IdMapping
        Long getId();
        String getName();
    }
    @EntityView(C.class)
    interface CDto {
        @IdMapping
        Long getId();
        String getName();
    }
}

查询就是将实体视图应用于查询,最简单的就是通过 id 进行查询。

ADto a = entityViewManager.find(entityManager, ADto.class, id);

Spring 数据集成让您几乎可以像 Spring 数据投影一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<ADto> findAll(Pageable pageable);

最棒的是,它只会获取实际需要的状态!

首先,JPQL 的一个基本限制是它并不真正支持创建查询来构建复杂的图形[JPQL FETCH JOIN 不会削减它,Hibernate 通过生成 sql 笛卡尔坐标来解决这个问题产品等]。这也是Ebean存在的根本原因之一。

JPA 后来添加了 FetchGroup,这让您更接近 Ebean ORM 查询语言的功能。您将需要尝试将 FetchGroup 与 JPQL 查询一起使用,以查看您的用例有多接近。

Hibernate 可能遇到的具体问题包括:

  • 获取 2 ToMany 路径时生成 SQL 笛卡尔积
  • 不遵守 SQL 中的 maxRows,而是执行客户端分页(因此我们不再让数据库针对最大行数优化查询)
  • 没有对大型查询的等效支持 - Ebean 的 findEach() 管理持久性上下文中持有的 bean 的数量
  • 不支持 filterMany 表达式(谓词在 ToMany 路径而不是根路径上)
  • 不支持部分对象(需要转换为 DTO 查询)

补充说明:

List vs Set:这是一个特定于 Hibernate 的实现设计,其中 Hibernate 为 Set 提供了“包语义”(更好的 sql 实现)。对于 Ebean,我们同样可以使用 Set 或 List 并推荐 List,因为它避免了与变异 bean 相关的 equals/hashcode 问题。将关系转换为对象时的重复数据删除是持久性上下文的工作,同样适用于 Ebean 的 List 和 Set。

Ebean 具有不同的体系结构和脏值,这意味着实体 bean 查询的成本非常接近 DTO 查询的成本。 Hibernate 还不支持部分对象并且存储“旧值”的成本要高得多,这意味着 Hibernate 人员出于性能原因提倡使用 DTO 查询。由于我们的架构方法(Ebean 存储旧值),我们对 Ebean 没有同样的需求。

LazyInitializationException

这是另一个 Hibernate 特有的行为。 Ebean 用户根本不需要处理这个。此外,如果我们想停止映射代码调用的延迟加载,Ebean 不会产生 N+1 加上 Ebean 也有 query.setDisableLazyLoading(true)。如果您使用 Hibernate,这些是您需要处理的 3 件事。

Hibernate“成熟而强大”

是的,但它目前对 ORM 的含义有不同的看法,而且可能永远如此。具体围绕对部分对象和复杂查询的支持,但您还可以包括 sql2011 年历史支持和软删除支持。

Ebean 自 2006 年以来一直是开源的(所以 15 年了,而且还在增加)。您还可以将 Ebean github 问题与 Hibernate JIRA 问题进行比较。有许多不同的方式来查看“成熟”等。正如我所看到的,要让 Hibernate 到达 Ebean 在 wrt 部分对象和复杂查询中的位置,他们还有一些工作要做。