N+1 问题是对象关系映射中的一个性能问题,它为应用程序层的单个选择查询在数据库中触发多个选择查询(确切地说是 N+1,其中 N = 表中的记录数)。休眠和弹簧数据JPA提供了多种方法来捕获和解决此性能问题。
为了理解N+1问题,让我们考虑一个场景。假设我们有一个映射到数据库中表的对象集合,并且每个用户都有使用联接表的集合或映射到表。在 ORM 级别,a 与 具有许多到许多关系。User
t_users
Role
t_roles
t_user_roles
User
Role
- @Entity
- @Table(name = "t_users")
- public class User {
-
- @Id
- @GeneratedValue(strategy=GenerationType.AUTO)
- private Long id;
- private String name;
-
- @ManyToMany(fetch = FetchType.LAZY)
- private Set
roles; - //Getter and Setters removed for brevity
- }
-
- @Entity
- @Table(name = "t_roles")
- public class Role {
-
- @Id
- @GeneratedValue(strategy= GenerationType.AUTO)
- private Long id;
-
- private String name;
- //Getter and Setters removed for brevity
- }
一个用户可以有多个角色。角色加载缓慢。 |
现在,假设我们要从此表中提取所有用户并打印每个用户的角色。非常幼稚的对象关系实现可以是 -
- public interface UserRepository extends CrudRepository
{ -
- List
findAllBy(); - }
由 ORM 执行的等效 SQL 查询将是:
首先获取所有用户 (1)
Select * from t_users;
然后获取每个用户执行 N 次的角色(其中 N 是用户数)
Select * from t_user_roles where userid = ;
因此,我们需要为“用户”选择一个,并为每个用户获取角色选择 N 个附加选项,其中 N 是用户总数。这是 ORM 中一个经典的 N+1 问题。
休眠提供跟踪选项,用于在控制台/日志中启用 SQL 日志记录。使用日志,您可以轻松查看休眠是否正在为给定呼叫发出 N+1 查询。
- spring:
- jpa:
- show-sql: true
- database-platform: org.hibernate.dialect.MySQL8Dialect
- hibernate:
- ddl-auto: create
- use-new-id-generator-mappings: true
- properties:
- hibernate:
- type: trace
在跟踪中启用 SQL 日志记录。 | |
我们也必须启用此功能,以便在日志中显示sql查询。 |
- 2017-12-23 07:42:30.923 INFO 11657 --- [ main] hello.UserService : Customers found with findAll():
- Hibernate: select user0_.id as id1_1_, user0_.name as name2_1_ from user user0_
- Hibernate: select roles0_.user_id as user_id1_2_0_, roles0_.roles_id as roles_id2_2_0_, role1_.id as id1_0_1_, role1_.name as name2_0_1_ from user_roles roles0_ inner join role role1_ on roles0_.roles_id=role1_.id where roles0_.user_id=?
- Hibernate: select roles0_.user_id as user_id1_2_0_, roles0_.roles_id as roles_id2_2_0_, role1_.id as id1_0_1_, role1_.name as name2_0_1_ from user_roles roles0_ inner join role role1_ on roles0_.roles_id=role1_.id where roles0_.user_id=?
- Hibernate: select roles0_.user_id as user_id1_2_0_, roles0_.roles_id as roles_id2_2_0_, role1_.id as id1_0_1_, role1_.name as name2_0_1_ from user_roles roles0_ inner join role role1_ on roles0_.roles_id=role1_.id where roles0_.user_id=?
- Hibernate: select roles0_.user_id as user_id1_2_0_, roles0_.roles_id as roles_id2_2_0_, role1_.id as id1_0_1_, role1_.name as name2_0_1_ from user_roles roles0_ inner join role role1_ on roles0_.roles_id=role1_.id where roles0_.user_id=?
如果看到给定选择查询的 SQL 有多个条目,则很有可能是由于 N+1 问题。
休眠和弹簧数据JPA提供了解决N + 1 ORM问题的机制。
在 SQL 级别,ORM 需要实现的是避免 N+1,即触发一个连接两个表的查询,并在单个查询中获取组合结果。
Hibernate: select user0_.id as id1_1_0_, role2_.id as id1_0_1_, user0_.name as name2_1_0_, role2_.name as name2_0_1_, roles1_.user_id as user_id1_2_0__, roles1_.roles_id as roles_id2_2_0__ from user user0_ left outer join user_roles roles1_ on user0_.id=roles1_.user_id left outer join role role2_ on roles1_.roles_id=role2_.id
select user0_.id, role2_.id, user0_.name, role2_.name, roles1_.user_id, roles1_.roles_id from user user0_ left outer join user_roles roles1_ on user0_.id=roles1_.user_id left outer join role role2_ on roles1_.roles_id=role2_.id
如果我们使用的是弹簧数据JPA,那么我们有两个选项来实现这一点 - 使用实体图或使用带有提取连接的选择查询。
- public interface UserRepository extends CrudRepository
{ -
- List
findAllBy(); -
- @Query("SELECT p FROM User p LEFT JOIN FETCH p.roles")
- List
findWithoutNPlusOne(); -
- @EntityGraph(attributePaths = {"roles"})
- List
findAll(); - }
在数据库级别发出 N+1 个查询 | |
使用左联接获取,我们解决了 N+1 问题 | |
使用属性路径,弹簧数据JPA避免了N + 1问题 |