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 拉取数据的方式,但很难称其为成功的优化。