Hibernate和Spring Data JPA中N+1问题

Spring JPA 和 Hibernate为无缝数据库通信提供了强大的工具。但是,由于客户端将更多控制权委托给框架,因此生成的查询结果可能远非最佳。

在本教程中,我们将回顾使用 Spring JPA 和 Hibernate 时常见的N +1 问题。我们将检查可能导致问题的不同情况。

什么是N + 1问题
N +1 问题是指对于单个请求(例如获取Users ),我们向每个User发出额外请求以获取其信息的情况。尽管此问题通常与延迟加载有关,但情况并非总是如此。

我们可以在任何类型的关系中遇到这个问题。然而,它通常产生于多对多或一对多关系。

为了更好地形象化问题,我们需要概述实体之间的关系。我们以一个简单的社交网络平台为例。只有用户和帖子.

1.延迟获取
首先我们来看看延迟加载是如何导致N +1问题的。我们将考虑以下示例:

@Entity
public class User {
    @Id
    private Long id;
    private String username;
    private String email;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "author")
    protected List<Post> posts;
   
// constructors, getters, setters, etc.
}

用户与帖子具有一对多的关系。这意味着每个User有多个Posts。我们没有明确确定字段的获取策略。该策略是从注释中推断出来的。正如前面提到的,@OneToMany默认情况下具有延迟获取:

@Target({METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface OneToMany {
    Class targetEntity() default void.class;
    CascadeType cascade() default {};
    FetchType fetch() default FetchType.LAZY;
    String mappedBy() default "";
    boolean orphanRemoval() default false;
}

如果我们尝试获取所有Users,则延迟获取不会获取比我们访问的更多的信息:

@Test
void givenLazyListBasedUser_WhenFetchingAllUsers_ThenIssueOneRequests() {
    getUserService().findAll();
    assertSelectCount(1);
}

因此,为了获取所有Users,我们将发出一个请求。让我们尝试访问帖子。 Hibernate 将发出额外的请求,因为未事先获取信息。对于单个User,这意味着总共有两个请求:

@ParameterizedTest
@ValueSource(longs = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
void givenLazyListBasedUser_WhenFetchingOneUser_ThenIssueTwoRequest(Long id) {
    getUserService().getUserByIdWithPredicate(id, user -> !user.getPosts().isEmpty());
    assertSelectCount(2);
}

getUserByIdWithPredicate (Long, Predicate )方法过滤用户,但其测试的主要目标是触发加载。我们将有 1+1 请求,但如果我们扩展它,我们将遇到N +1 问题:

@Test
void givenLazyListBasedUser_WhenFetchingAllUsersCheckingPosts_ThenIssueNPlusOneRequests() {
    int numberOfRequests = getUserService().countNumberOfRequestsWithFunction(users -> {
        List<List<Post>> usersWithPosts = users.stream()
          .map(User::getPosts)
          .filter(List::isEmpty)
          .toList();
        return users.size();
    });
    assertSelectCount(numberOfRequests + 1);
}

我们应该小心延迟获取。在某些情况下,延迟加载对于减少从数据库获取的数据是有意义的。但是,如果我们在大多数情况下访问延迟获取的信息,我们可能会增加请求量。为了做出最佳判断,我们必须仔细研究访问上下文情况。

2.急切获取
大多数情况下,急切加载可以帮助我们解决N +1问题。然而,结果取决于我们实体之间的关系。让我们考虑一个类似的User类,但具有显式设置的急切获取:

@Entity
public class User {
    @Id
    private Long id;
    private String username;
    private String email;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "author", fetch = FetchType.EAGER)
    private List<Post> posts;
   
// constructors, getters, setters, etc.
}

如果我们获取单个用户,获取类型将强制 Hibernate 在单个请求中加载所有数据:

@ParameterizedTest
@ValueSource(longs = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
void givenEagerListBasedUser_WhenFetchingOneUser_ThenIssueOneRequest(Long id) {
    getUserService().getUserById(id);
    assertSelectCount(1);
}

同时,获取所有用户的情况也发生了变化。无论我们是否要使用帖子,我们都会立即得到N +1 :

@Test
void givenEagerListBasedUser_WhenFetchingAllUsers_ThenIssueNPlusOneRequests() {
    List<User> users = getUserService().findAll();
    assertSelectCount(users.size() + 1);
}

虽然 eager fetch 改变了 Hibernate 拉取数据的方式,但很难称其为成功的优化。