基于Spring Data JPA的用户-角色-权限多对多关联查询重复数据问题咨询
我之前也碰到过这个坑,其实这是JPA多对多关联的EAGER加载引发的笛卡尔积问题。你看你的User类里roles用了FetchType.EAGER,同时Role类里的privileges也设置了FetchType.EAGER。当你调用userService.findById(id)查询用户时,JPA会自动执行一条关联了users、users_roles、roles、roles_privileges、privileges的SQL语句,这条查询的结果是典型的笛卡尔积:角色0绑定3个权限,对应3条记录;角色1绑定1个权限,对应1条记录,总共4条结果。
JPA在把这些SQL结果映射成Java对象时,会为每一条SQL记录创建对应的Role实例,所以你就得到了[0,0,0,1]的重复角色列表。
至于为什么authUser.getRoles()结果正常?那是因为Spring Security在加载认证用户时,内部查询逻辑做了去重处理(比如默认用了DISTINCT),或者它加载角色时并没有触发权限的EAGER加载,完美避开了笛卡尔积的坑。
这里有几个可行的方案,你可以根据业务场景选择:
1. 用Set替代Collection存储角色
把roles的类型从Collection<Role>改成Set<Role>,因为Set集合本身会自动去重(前提是你的Role类正确实现了equals()和hashCode()方法,通常基于id字段实现即可)。这样即使查询返回了重复的Role实例,Set也会只保留唯一的一个:
@ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "users_roles", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id") ) private Set<Role> roles;
同时修改getRoles()方法的返回类型:
@JsonIgnore public Set<Role> getRoles() { return this.roles; }
2. 调整FetchMode为SUBSELECT
在User类的roles关联上添加@Fetch(FetchMode.SUBSELECT)注解,让JPA分两步查询,避免笛卡尔积:
- 第一步:查询目标用户
- 第二步:用子查询获取该用户的所有角色,再分别加载每个角色的权限
代码修改如下:
@ManyToMany(fetch = FetchType.EAGER) @Fetch(FetchMode.SUBSELECT) @JoinTable( name = "users_roles", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id") ) private Collection<Role> roles;
3. 在查询时添加DISTINCT
修改userService.findById(id)的实现,用JPQL或者Criteria Query添加DISTINCT关键字,让SQL查询直接返回去重后的结果:
// 示例JPQL查询 @Query("SELECT DISTINCT u FROM User u JOIN FETCH u.roles WHERE u.id = :id") User findById(@Param("id") Long id);
这样JPA在映射结果时就不会创建重复的Role对象了。
4. 将权限的加载改为LAZY(按需加载)
如果业务上不是每次查询用户都需要立即获取角色的权限,可以把Role类里的privileges关联改为FetchType.LAZY:
@ManyToMany(fetch = FetchType.LAZY) @JoinTable( name="roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id") ) private Collection<Privilege> privileges;
这样查询用户时只会加载角色信息,不会加载权限,自然不会产生笛卡尔积导致重复角色。需要权限时再通过延迟加载获取(注意要在事务范围内操作,否则会抛出LazyInitializationException)。
最推荐的方案是结合Set存储角色+正确实现equals/hashCode,或者使用@Fetch(FetchMode.SUBSELECT),这两种方式都能很好地解决重复问题,同时保持原有的EAGER加载策略。如果权限不是每次都需要,改为LAZY加载也是更优的性能选择。
内容的提问来源于stack exchange,提问作者Óscar Raya

