【问题标题】:Fetch several OneToMany relations in Hibernate (opposed to Ebean fetch)在 Hibernate 中获取多个 OneToMany 关系(与 Ebean fetch 相对)
【发布时间】:2021-05-26 02:15:12
【问题描述】:

我有一个生产项目,它使用非常旧的 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 个数据库查询 - 一个用于 A 类,两个用于关系;然后 Ebean 会自动组合这些查询的结果。

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

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

【问题讨论】:

  • 这能回答你的问题吗? How to use multiple JOIN FETCH in one JPQL query
  • 我在尝试解决这个问题时确实发现了这个 SO 问题 - 不幸的是,手动执行 N 个查询似乎并不方便 - 我特意提到我们有巨大的实体图,手动管理它们会很麻烦.我认为这个问题有一个通用的解决方案,不是吗?如果没有这样的解决方案 - 那么我会关闭这个问题。无论如何,谢谢您的回答。
  • 您可以简单地将List 类型替换为Set 类型,它会起作用,但会导致笛卡尔积问题。见this question
  • 我明白了。我在我的问题中也提到了这一点。问题是关于 Ebean 功能的任何替代方案,它可以通过不止一个查询来急切地获取所有需要的数据。使用Set 而不是List 不会进行多次数据库查询;而 Ebean 会。
  • 这是 JPQL 的一个基本限制,也是最初创建 Ebean 的关键原因之一。 hibernate 不仅倾向于生成笛卡尔积,而且它也不支持 SQL 中的 maxRows,因此分页发生在客户端上。这两个都是 JPQL 的设计限制。 JPA(和 Hibernate)只是通过引入 FetchGroup 才开始解决这个限制,这有点接近我们在 Ebean 中所拥有的,但 Ebean 给了我们更多的控制权(获取、获取查询、获取缓存、获取惰性)。注意:我是 Ebean 的创建者。

标签: java hibernate ebean


【解决方案1】:

首先,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,因为它避免了与 mutating beans 相关的 equals/hashcode 问题。将关系转换为对象时的重复数据删除是持久化上下文的工作,同样适用于使用 Ebean 的 List 和 Set。

Ebean 具有不同的架构 wrt 脏值,这意味着实体 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 的部分对象和复杂查询的位置,他们有一些工作要做。

【讨论】:

    【解决方案2】:

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

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

    我创建了该库以允许在 JPA 模型和自定义接口或抽象类定义模型之间轻松映射,例如 Spring Data Projections on steroids。这个想法是您按照自己喜欢的方式定义目标结构(域模型),并通过 JPQL 表达式将属性(getter)映射到实体模型。

    使用 Blaze-Persistence Entity-Views 的用例的 DTO 模型可能如下所示:

    @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 Data 集成让您可以像使用 Spring Data Projections 一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

    Page<ADto> findAll(Pageable pageable);
    

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

    【讨论】:

    • 感谢您的回答。几个问题:1)是否暗示,我需要为每个实体图构建相应的 DTO 图?如果我不想获取“bs”,而只想获取“cs”——我可以重用现有的 DTO 类吗? 2) 多选查询是否可读?如果整个图(尤其是非常深的)包含在单个查询中 - 这听起来不可读。 3)大实体图很罕见吗?没有它们我无法想象我们的产品,这可能是设计问题,但我认为至少有至少 2 个 OneToMany 关系是很常见的。
    • 1) 由于 Blaze-Persistence Entity-Views 允许使用支持多重继承的接口,您可以获得很好的重用,但是是的,这个想法是,对于每个用例,您定义您需要通过 java 类型的数据图。相信我,这些 DTO 中不存在方法这一事实将使您免于 LazyInitializationExceptions。 2)它们是可读的。获取的 SQL 只是从主查询移动到子查询。 3)“大”是主观的,但问问自己,你可以期望用户一次消化多少数据。大多数情况下,您不需要一次获得所有数据。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多