如何使用 EntityGraph 获取多个列表

Car*_*arí 5 java hibernate jpa spring-data spring-boot

我花了太多时间尝试做一些乍一看似乎很简单的事情:

假设我有一个Store具有两种@OneToMany关系的实体:

@Entity
public class Store {

    private String name;

    @OneToMany(mappedBy = "store")
    private List<Foo> foos;

    @OneToMany(mappedBy = "store")
    private List<Bar> bars;

}
Run Code Online (Sandbox Code Playgroud)

我想使用相同的查询获取两个列表。我想使用实体图,因为我需要在执行时决定要获取哪些子项。

@EntityGraph(attributePaths = {"foos", "bars"})
Store findByName(String name);
Run Code Online (Sandbox Code Playgroud)

这导致:

MultipleBagFetchException: cannot simultaneously fetch multiple bags.
Run Code Online (Sandbox Code Playgroud)

我不想使用Set代替List,因为这种方法的目的是避免 N+1 查询问题笛卡尔积问题(实体层次结构比示例大得多),所以 nolazy fetching或改为Listdo Sethelp这个案例。

此外,此处提出的第一个答案不适用于实体图。

有任何想法吗?

Eug*_*ene 16

实体图的主要目标是提高加载实体的相关关联和基本字段时的运行时性能。它正在解决N+1问题。Hibernate 在一个选择查询中加载所有图表,然后避免获取与更多SELECT查询的关联。

需要理解的是,通过当前的实现,实体图仅生成用于执行一个 SELECT查询,因为不可能为图中的所有属性路径指定除默认FetchMode.JOIN定义之外的其他获取模式。

它非常适合ManyToOneOneToOne和 single 等关系OneToMany,因为所有这些关系都可以通过一个查询加载。

但是,如果一个实体具有多个OneToMany关系,则由于笛卡尔连接,通过一个查询加载完整数据就会出现问题。如果我们有 50Store行与 20 行Foo和 10Bar行关联,则最终结果集将包含 10_000 条记录(例如,50 x 20 x 10)。这对性能来说太糟糕了!

这是主要原因MultipleBagFetchException。Hibernate 可以保护您免受笛卡尔连接的影响。

因此,我们只能通过单独的选择查询来加载两个集合。

目前,Entity Graph 不支持此类加载。

有一个悬而未决的问题讨论

Fetch Profiles也是如此。目前仅支持FetchMode.JOIN 。

查看很好的解释修复 Hibernate MultipleBagFetchException 的最佳方法

解决方案 1:两个 JPQL 查询

我们可以执行两个 JPQL 查询,而不是执行获取两个关联的单个查询:

public interface StoreRepository extends JpaRepository<Store, Long> {

    default List<Store> findStoresByNameWithCollections(String name) {
        List<Store> stores = findByName(name);
        if (stores != null && !stores.isEmpty()) {
            stores = loadStoreBars(stores);
        }
        return stores;
    }

    @Query(value = "select DISTINCT st from Store st LEFT JOIN FETCH st.foos where st.name like :name")
    @QueryHints(value = { @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false" )}, forCounting = false)
    List<Store> findByName(String name);

    @Query(value = "select DISTINCT st from Store st LEFT JOIN FETCH st.bars where st in (:stores)")
    @QueryHints(value = { @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false" )}, forCounting = false)
    List<Store> loadStoreBars(List<Store> stores);
}
Run Code Online (Sandbox Code Playgroud)

hibernate.query.passDistinctThrough我们用来指示 Hibernate 防止将 JPQL DISTINCT 关键字传递给底层 SQL 查询的 JPA 查询提示。父实体的唯一性将在服务器端执行。

生成的查询:

--Parent entity with Foos collection
    select
        store0_.id as id1_36_0_,
        foos1_.id as id1_15_1_,
        store0_.name as name2_36_0_,
        foos1_.store_id as store_id2_15_1_,
        foos1_.store_id as store_id2_15_0__,
        foos1_.id as id1_15_0__ 
    from
        store store0_ 
    left outer join
        foo foos1_ 
            on store0_.id=foos1_.store_id 
    where
        store0_.name like ?

--Bars collection
    select
        store0_.id as id1_36_0_,
        bars1_.id as id1_5_1_,
        store0_.name as name2_36_0_,
        bars1_.store_id as store_id2_5_1_,
        bars1_.store_id as store_id2_5_0__,
        bars1_.id as id1_5_0__ 
    from
        store store0_ 
    left outer join
        bar bars1_ 
            on store0_.id=bars1_.store_id 
    where
        store0_.id in (
            ? , ?
        )
Run Code Online (Sandbox Code Playgroud)

解决方案2:FetchMode.SUBSELECT

@Entity
public class Store {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = "store", fetch = FetchType.EAGER)
    @Fetch(FetchMode.SUBSELECT)
    private List<Foo> foos;

    @OneToMany(mappedBy = "store", fetch = FetchType.EAGER)
    @Fetch(FetchMode.SUBSELECT)
    private List<Bar> bars;
}
Run Code Online (Sandbox Code Playgroud)

Hibernate 将通过生成单个 SQL 语句来初始化Store先前获取的所有实体的所有集合来避免 N + 1 查询问题。Hibernate 不传递所有实体标识符,而是简单地重新运行先前获取实体的查询Store

生成的查询:

--Parent entities
    select
        store0_.id as id1_36_,
        store0_.name as name2_36_ 
    from
        store store0_ 
    where
        store0_.name like ?

--Foos collection
    select
        foos0_.store_id as store_id2_15_1_,
        foos0_.id as id1_15_1_,
        foos0_.id as id1_15_0_,
        foos0_.store_id as store_id2_15_0_ 
    from
        foo foos0_ 
    where
        foos0_.store_id in (
            select
                store0_.id 
            from
                store store0_ 
            where
                store0_.name like ?
        )

--Bars collection
    select
        bars0_.store_id as store_id2_5_1_,
        bars0_.id as id1_5_1_,
        bars0_.id as id1_5_0_,
        bars0_.store_id as store_id2_5_0_ 
    from
        bar bars0_ 
    where
        bars0_.store_id in (
            select
                store0_.id 
            from
                store store0_ 
            where
                store0_.name like ?
        )
Run Code Online (Sandbox Code Playgroud)

解决方案3:FetchMode.SUBSELECT + EntityGraph.EntityGraphType.LOAD

当该javax.persistence.loadgraph属性用于指定实体图时,由实体图的属性节点指定的属性将被视为FetchType.EAGER,未指定的属性将根据其指定或默认的 FetchType 进行处理。

图中未指定的集合将使用自己的获取模式。

public interface StoreRepository extends JpaRepository<Store, Long> {
    @EntityGraph(attributePaths = {"foos"}, type = EntityGraph.EntityGraphType.LOAD)
    List<Store> findByName(String name);
}

public class Store {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = "store")
    private List<Foo> foos;

    @OneToMany(mappedBy = "store", fetch = FetchType.EAGER)
    @Fetch(FetchMode.SUBSELECT)
    private List<Bar> bars;
}
Run Code Online (Sandbox Code Playgroud)

生成的查询:

    select
        store0_.id as id1_36_0_,
        foos1_.id as id1_15_1_,
        store0_.name as name2_36_0_,
        foos1_.store_id as store_id2_15_1_,
        foos1_.store_id as store_id2_15_0__,
        foos1_.id as id1_15_0__ 
    from
        store store0_ 
    left outer join
        foo foos1_ 
            on store0_.id=foos1_.store_id 
    where
        store0_.name=?

    select
        bars0_.store_id as store_id2_5_1_,
        bars0_.id as id1_5_1_,
        bars0_.id as id1_5_0_,
        bars0_.store_id as store_id2_5_0_ 
    from
        bar bars0_ 
    where
        bars0_.store_id in (
            select
                store0_.id 
            from
                store store0_ 
            left outer join
                foo foos1_ 
                    on store0_.id=foos1_.store_id 
            where
                store0_.name=?
        )
Run Code Online (Sandbox Code Playgroud)

解决方案4、BatchSize

然而,虽然@BatchSize比遇到 N+1 查询问题要好,但大多数情况下,解决方案 1 和解决方案 2 是更好的选择,因为它允许您以最少的查询次数获取所有所需的数据。

批量大小很棘手。它是一个系数,用于确定在一个查询中加载的集合计数,它不是精确的集合计数。

例如,如果您有 20 个父记录和批量大小为 50 的集合,hibernate 将生成 1 个用于加载父实体的查询,然后生成 2 个用于初始化集合的查询(第一个:12 个集合,第二个:8 个集合)。

public class Store {

    @Id
    private Long id;

    private String name;

    @BatchSize(size = 50)
    @OneToMany(mappedBy = "store")
    private List<Foo> foos;

    @BatchSize(size = 50)
    @OneToMany(mappedBy = "store")
    private List<Bar> bars;
}
Run Code Online (Sandbox Code Playgroud)

生成的查询:

--Parent entities
    select
        store0_.id as id1_36_,
        store0_.name as name2_36_ 
    from
        store store0_ 
    where
        store0_.name=?

--Bar collections
    select
        bars0_.store_id as store_id2_5_1_,
        bars0_.id as id1_5_1_,
        bars0_.id as id1_5_0_,
        bars0_.store_id as store_id2_5_0_ 
    from
        bar bars0_ 
    where
        bars0_.store_id in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )

    select
        bars0_.store_id as store_id2_5_1_,
        bars0_.id as id1_5_1_,
        bars0_.id as id1_5_0_,
        bars0_.store_id as store_id2_5_0_ 
    from
        bar bars0_ 
    where
        bars0_.store_id in (
            ?, ?, ?, ?, ?, ?, ?, ?
        )

--Foo collections
    select
        foos0_.store_id as store_id2_15_1_,
        foos0_.id as id1_15_1_,
        foos0_.id as id1_15_0_,
        foos0_.store_id as store_id2_15_0_ 
    from
        foo foos0_ 
    where
        foos0_.store_id in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )


    select
        foos0_.store_id as store_id2_15_1_,
        foos0_.id as id1_15_1_,
        foos0_.id as id1_15_0_,
        foos0_.store_id as store_id2_15_0_ 
    from
        foo foos0_ 
    where
        foos0_.store_id in (
            ?, ?, ?, ?, ?, ?, ?, ?
        )
Run Code Online (Sandbox Code Playgroud)