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

Spring JPA多对多关联问题:无法从非拥有端实体获取数据

多对多关联双向获取问题排查与解决

问题现象

使用Spring Boot 3.0.6、JPA和Kotlin开发,User与Role实体建立多对多关联后:

  • 从User实例可以正常获取关联的Role集合
  • 从Role实例无法获取关联的User集合
  • 测试中,先保存角色再关联用户时,User关联断言通过但Role关联断言失败;调换保存顺序则结果相反

实体代码

@Entity
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    val email: String,
    var name: String,
    val contactNumber: String,

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "user_role",
        joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
        inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")]
    )
    @JsonBackReference
    var roles: MutableSet<Role> = mutableSetOf(),
)

@Entity
data class Role(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    var name: String,
    var description: String,

    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "roles")
    @JsonBackReference
    var users: MutableSet<User> = mutableSetOf(),
)

仓库代码

@Repository
interface UserRepository : JpaRepository<User, Long>{
    fun findFirstByEmail(email: String) : User?
}

@Repository
interface RoleRepository : JpaRepository<Role, Long>{
    fun findFirstByName(name: String) : Role?
}

测试代码

@DataJpaTest
class UserEntityTests(@Autowired val userRepository: UserRepository,
                      @Autowired val roleRepository: RoleRepository) {

 @Test
    fun `test many-to-many relationship with roles`() {

        val role1 = Role(
            name = "Admin",
            description = "Admin Role"
        )
        val role2 = Role(
            name = "User",
            description = "User Role"
        )
        roleRepository.saveAll(listOf(role1, role2))

        var user = User(
            email = "jerry@example.com",
            name = "Jerry Garcia",
            contactNumber = "4444444444",
        )
        user.roles = mutableSetOf(role1, role2)
        userRepository.save(user)

        val savedUser = userRepository.findById(user.id).get()
        assertEquals(2, savedUser.roles.size)

        val savedRole1 = roleRepository.findById(role1.id).get()
        val savedRole2 = roleRepository.findById(role2.id).get()
        assertEquals(1, savedRole1.users.size)
        assertEquals(1, savedRole2.users.size)
    }
}

问题原因与解决方法

核心原因

JPA多对多关联中,只有标注@JoinTable的一方(User)是主控方,负责维护关联表的增删操作。被动方(Role,通过mappedBy关联)的内存集合不会被JPA自动同步,仅靠主控方更新关联表后,被动方的内存实例不会自动感知到关联变化。

解决方案

1. 手动维护双向关联的一致性

在实体中添加辅助方法,确保设置关联时同时更新双方的集合,避免仅更新主控方导致被动方内存数据不一致。

修改User实体,添加addRole方法:

@Entity
data class User(
    // ... 原有字段
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(...)
    @JsonBackReference
    var roles: MutableSet<Role> = mutableSetOf(),
) {
    // 添加双向关联维护方法
    fun addRole(role: Role) {
        this.roles.add(role)
        role.users.add(this)
    }
}

2. 修改测试代码,使用辅助方法建立关联

替换直接赋值集合的方式,用addRole逐个添加角色,确保双向关联都被更新:

// 替换原有的user.roles = mutableSetOf(role1, role2)
user.addRole(role1)
user.addRole(role2)
userRepository.save(user)

3. 可选:强制刷新上下文(适用于未维护双向关联的场景)

如果暂时无法修改实体方法,可在保存用户后刷新JPA上下文,再重新查询角色:

userRepository.save(user)
userRepository.flush() // 刷新上下文,确保关联表已更新

// 重新查询角色,此时会从数据库加载最新关联数据
val savedRole1 = roleRepository.findById(role1.id).get()
val savedRole2 = roleRepository.findById(role2.id).get()

修改后的测试结果

调整后,无论保存顺序如何,两个方向的关联断言都能通过,因为双向关联的内存数据和数据库状态保持一致。


内容的提问来源于stack exchange,提问作者Gayan Kavirathne

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 10:42:47