Skip to content
On this page

多对多关系

我们在之前分析过,用户和角色之间,角色和权限之间都是多对多的关系,现在来分析下级联查询,比如在查询用户列表的时候,想把用户具有的角色也一起拿到

N + 1 问题

在测试类新增测试方法:

@Test
void testRepo() {
	List<User> userList = userRepo.findAll();
	for (User user : userList) {
		System.out.println(user.getNickname() + "具有角色:");
		Set<Role> roleList = user.getRoles();
		for (Role role : roleList) {
			System.out.println(role.getName() + " ");
		}
		System.out.println("----------------");
	}
}

运行后,会报错

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role

这个的意思是说,懒加载没有查询 user 对应的 roles,我们在查询的时候只查询了 user 表,没有查询 user 表关联的 role 信息,每个 user 里面还有 roles,这部分值没查,所以报了错。我们也可以查看下调试信息,Hibernate 确实只查询了一次,只查询了 user 表,现在修改下 User.java 的代码:

@ManyToMany(fetch = FetchType.EAGER,cascade = {CascadeType.MERGE})
@JoinTable
private Set<Role> roles;

这次测试通过了,而且 Hibernate 在查完 user 之后,分别查询了两次每个 user 对应的 role:

select roles0_.users_id as users_id1_4_0_, roles0_.roles_id as roles_id2_4_0_, role1_.id as id1_1_1_, role1_.created_time as created_2_1_1_, role1_.description as descript3_1_1_, role1_.name as name4_1_1_, role1_.updated_time as updated_5_1_1_ from user_roles roles0_ inner join role role1_ on roles0_.roles_id=role1_.id where roles0_.users_id=?

这个就是 N+1 问题,我们如果按照上面修改代码那样的话,每一次对 user 列表进行查询都会进行 N+1 次查询(1 次查询 user,N 次查询每个 user 对应的 roles)

在定义 User 的时候,我们有两种查询方式:

FetchType.LAZY 表示只查询本表(默认) FetchType.EAGER 表示立即查询本表以及所关联的数据

这时候有个疑问,有些时候我想只查询本表(只查询一次效率高),有些时候想查询本表以及所关联的数据(查的数据多,不需要自己再去查了)

解决办法

这时候可以使用图查询来实现,先将添加的fetch = FetchType.EAGER去除,在 User.java 添加图说明:

@Entity
@NamedEntityGraph(name = "user-with-roles",
    attributeNodes = {@NamedAttributeNode("roles")})
public class User {

name 相当于这个图的 key,NamedAttributeNode 表明查询的时候一起查询的属性

在 UserRepo 添加:

  @EntityGraph(value = "user-with-roles")
  @Query(value = "SELECT user FROM User user")
  List<User> findAllUsersWithRoles();

测试语句修改:

@Test
void testRepo() {
	List<User> userList = userRepo.findAllUsersWithRoles();
	for (User user : userList) {
		System.out.println(user.getNickname() + "具有角色:");
		Set<Role> roleList = user.getRoles();
		for (Role role : roleList) {
			System.out.println(role.getName() + " ");
		}
		System.out.println("----------------");
	}
}

这时候发现测试确实通过了,打印出来了想要的结构,这时候观察调试信息,Hibernate 只进行了一次查询

Hibernate: select user0_.id as id1_3_0_, role2_.id as id1_1_1_, user0_.created_time as created_2_3_0_, user0_.description as descript3_3_0_, user0_.nickname as nickname4_3_0_, user0_.password as password5_3_0_, user0_.updated_time as updated_6_3_0_, user0_.username as username7_3_0_, role2_.created_time as created_2_1_1_, role2_.description as descript3_1_1_, role2_.name as name4_1_1_, role2_.updated_time as updated_5_1_1_, roles1_.users_id as users_id1_4_0__, roles1_.roles_id as roles_id2_4_0__ from user user0_ left outer join user_roles roles1_ on user0_.id=roles1_.users_id left outer join role role2_ on roles1_.roles_id=role2_.id

所以在只需要查询 user 自己的时候,直接使用默认的 lazy 查询就行,需要查询关联信息的时候,使用自定义的图查询就行,效率提高了~

也有一种简便方法,不需要使用 NamedEntityGraph,直接在 JpaRepository 这里使用 EntityGraph 搭配 attributePaths:

@EntityGraph(attributePaths = {"roles"})
@Query(value = "SELECT user FROM User user")
List<User> findAllUsersWithRoles();

数据初始化

考虑到项目启动的时候,没有任何数据,所以我在 UserService 添加了两个方法 initAllUsers 和 clearAllUsers,用来初始化和清除数据

专门添加了个启动类:src/main/java/com/example/rbac/StartInit.java 项目在启动监听之前,会先运行 PostConstruct 注释的方法,这时候我们就可以初始化数据了

@PostConstruct
public void init() {
	System.out.println("执行StartInit时间:" + LocalDateTime.now());

	// 初始化用户
	userService.initAllUsers();
}

然后我继续添加了定时任务 src/main/java/com/example/rbac/task/StaticScheduleTask.java,每天凌晨去执行一次,清除数据:

// 每天执行,0时0分0秒执行
@Scheduled(cron = "0 0 0 * * *")
private void configureTask() {
	System.out.println("执行静态定时任务时间:" + LocalDateTime.now());

	// 初始化用户
	userService.clearAllUsers();

	userService.initAllUsers();
}

这样项目启动的时候就有了数据,每天也会自动的清除数据

接入前端

我搞了个 vue3 + vite + pinia 的前端项目进行管理和演示,具体可以看看源码仓库

登录成功后,可以进行权限和用户的管理,可以在本地进行调试