Spring Data的findAll()方法不会主动获取关联数据。

10

我有两个实体,它们之间有单向的一对多关系。

@Entity
public class Basket {

    @Id
    @GeneratedValue
    private Long id;

    private int capacity;
}

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    private Basket basket;
}

我保存了几个对象:

    Basket basket1 = new Basket(100);
    Basket basket2 = new Basket(200);
    Basket basket3 = new Basket(300);
    basketRepository.save(asList(basket1, basket2, basket3));

    Item item1 = new Item("item1", basket1);
    Item item11 = new Item("item11", basket1);
    Item item2 = new Item("item2", basket2);
    Item item22 = new Item("item22", basket2);
    Item item3 = new Item("item3", basket3);
    Item item33 = new Item("item33", basket3);
    itemRepository.save(asList(item1, item11, item2, item22, item3, item33));

    // Loading one item. Basket fetched eagerly.
    itemRepository.findOne(1L);

    // Loading many items. Baskets are not loaded (n+1 select problem).
    itemRepository.findAll();

@ManyToOne 注解默认使用 eager fetch。 当我使用 findOne() 加载一个 Item 时,Hibernate 会生成带有 left outer join 的查询,并在同一查询中获取 Basket。 然而,当我使用 findAll() 时,Hibernate 先获取所有的 Items,然后执行 N selects(每个 Basket 一个),导致出现了所谓的 (n+1) select 问题。为什么 Hibernate 不会在 findAll() 方法中及时获取 Basket 对象,如何修复这个问题?

2个回答

13

根据 JPA 2.0 规范,@ManyToOne 默认是 EAGER。

现在,当您使用 findAll() 时,它相当于执行 JPQL 查询,例如 entityManager.createQuery(...),并默认先加载 items,接着对于每个 item 加载 basket 实体,导致 N+1 问题。

您可以遵循以下两种方法之一:

  1. 通过在 findAll 方法上指定@Query 注释来覆盖默认查询,并使用类似 select i from Item i left join fetch i.basket 的带有连接的查询。

  2. Item 类上使用名称为 basket@NamedEntityGraph,并指定需要急切加载的 Item 图的哪个部分。在 findAll 方法中,使用 @EntityGraph(value = "basket")请注意,根据spring jpa entity graph,我们还可以使用 attributePath 通过 @EntityGraph 定义临时实体图,而无需显式地将 @NamedEntityGraph 添加到您的域类型中。


或者覆盖 'findAll' 方法,只需添加 @EntityGraph(attributePaths = "basket") Spring 注释即可使方法急切地加载 'basket'。 - Cepr0
哦,是的...它是@EntityGraph(value = "basket", type = EntityGraphType.FETCH)。我在Spring仓库之外尝试了一下,所以在entityManager上手动使用setHint。更新答案。 - Madhusudana Reddy Sunnapu
是的,但默认情况下它是FETCH。我现在无法使用Spring Repo尝试它是值还是属性路径。 - Madhusudana Reddy Sunnapu
现在明白了。根据 https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-graph,我们可以使用 attributePath 通过 @EntityGraph 定义临时实体图,而无需显式添加 @NamedEntityGraph 到您的域类型中。 - Madhusudana Reddy Sunnapu
这个答案更具体并提供了更多的解决方案。@EntityGraph绝对是正确的选择,你不应该使用@Query来覆盖默认的查找实现。 - Gonzalo

4
您可以通过在仓库中使用@Query注释来覆盖findAll方法。以下是示例代码。
public interface ItemRepository extends CrudRepository<Item, Long> {
    @Override
    @Query("select item from Item item left join fetch item.basket")
    Iterable<Item> findAll();
}

然后您可以记录您的SQL查询,以查看只执行了一个查询。
Hibernate: select item0_.id as id1_1_0_, basket1_.id as id1_0_1_, item0_.basket_id as basket_i3_1_0_, item0_.name as name2_1_0_, basket1_.capacity as capacity2_0_1_ from item item0_ left outer join basket basket1_ on item0_.basket_id=basket1_.id

之前是这样的

2018-03-09 13:26:52.269  INFO 4268 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select item0_.id as id1_1_, item0_.basket_id as basket_i3_1_, item0_.name as name2_1_ from item item0_
Hibernate: select basket0_.id as id1_0_0_, basket0_.capacity as capacity2_0_0_ from basket basket0_ where basket0_.id=?
Hibernate: select basket0_.id as id1_0_0_, basket0_.capacity as capacity2_0_0_ from basket basket0_ where basket0_.id=?
Hibernate: select basket0_.id as id1_0_0_, basket0_.capacity as capacity2_0_0_ from basket basket0_ where basket0_.id=?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接