You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 22:13:14