如何在JPA中删除具有ManyToMany关系的实体(以及相应的连接表行)?

rdk*_*rdk 81 java orm hibernate jpa

假设我有两个实体:Group和User.每个用户都可以是许多组的成员,每个组可以拥有许多用户.

@Entity
public class User {
    @ManyToMany
    Set<Group> groups;
    //...
}

@Entity
public class Group {
    @ManyToMany(mappedBy="groups")
    Set<User> users;
    //...
}
Run Code Online (Sandbox Code Playgroud)

现在我想删除一个组(假设它有很多成员).

问题是当我在某个Group上调用EntityManager.remove()时,JPA提供程序(在我的情况下是Hibernate)不会从连接表中删除行,并且由于外键约束而导致删除操作失败.在User上调用remove()工作正常(我猜这与拥有关系的一方有关).

那么在这种情况下如何删除组呢?

我能想到的唯一方法是加载组中的所有用户,然后为每个用户从他的组中删除当前组并更新用户.但是,为了能够删除该组,对组中的每个用户调用update()似乎很荒谬.

Grz*_*zki 79

  • 关系的所有权取决于将"mappedBy"属性放置到注释的位置.您放置'mappedBy'的实体是不是所有者的实体.双方都没有机会成为所有者.如果您没有"删除用户"用例,您只需将所有权移至Group实体,因为目前User是所有者.
  • 另一方面,你没有问过这个问题,但有一件事值得一提.这些groupsusers没有相互结合.我的意思是,从Group1.users中删除User1实例后,User1.groups集合不会自动更改(这对我来说非常令人惊讶),
  • 总而言之,我建议你决定谁是主人.让我们说User是主人.然后,当删除用户时,关系用户组将自动更新.但是当删除一个组时,你必须像下面这样自己删除这个关系:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Run Code Online (Sandbox Code Playgroud)

  • 这是在幕后进行优化的吗?因为我不想查询整个数据集. (2认同)
  • 这是非常糟糕的方法,如果该组中有数千个用户怎么办? (2认同)
  • 这不会更新关系表,在关系表中关于此关系的行仍然保留。我没有测试,但这可能会导致问题。 (2认同)

dam*_*ian 40

以下适用于我.将以下方法添加到不是关系所有者的实体(组)

@PreRemove
private void removeGroupsFromUsers() {
    for (User u : users) {
        u.getGroups().remove(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

请记住,要使其工作,该组必须具有更新的用户列表(不会自动完成).因此,每次将组添加到用户实体中的组列表时,还应将用户添加到组实体中的用户列表.

  • 当您想使用此解决方案时,不应设置cascade = CascadeType.ALL。否则它就完美了! (2认同)

小智 26

我找到了一个可能的解决方案,但是......我不知道这是不是一个好的解决方案.

@Entity
public class Role extends Identifiable {

    @ManyToMany(cascade ={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name="Role_Permission",
            joinColumns=@JoinColumn(name="Role_id"),
            inverseJoinColumns=@JoinColumn(name="Permission_id")
        )
    public List<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(List<Permission> permissions) {
        this.permissions = permissions;
    }
}

@Entity
public class Permission extends Identifiable {

    @ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name="Role_Permission",
            joinColumns=@JoinColumn(name="Permission_id"),
            inverseJoinColumns=@JoinColumn(name="Role_id")
        )
    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
Run Code Online (Sandbox Code Playgroud)

我试过这个并且它有效.如果删除角色,也关系被删除(但不是许可的实体),当你删除权限,与角色的关系被删除过(但不是角色实例).但是我们两次映射单向关系,两个实体都是关系的所有者.这会给Hibernate带来一些问题吗?哪种类型的问题?

谢谢!

上面的代码来自另一个相关的帖子.

  • 这是不正确的.双向ManyToMany应该只有一个关系的所有者方.此解决方案使双方都成为所有者,并最终导致重复记录.为避免重复记录,必须使用集合而不是列表.但是,使用Set只是做一些不推荐的事情的解决方法. (12认同)
  • 那么,这种常见的最佳实践是什么? (2认同)

Pie*_*nry 7

作为JPA/Hibernate解决方案的替代方法:您可以在连接表上的foregin键的数据库定义中使用CASCADE DELETE子句,例如(Oracle语法):

CONSTRAINT fk_to_group
     FOREIGN KEY (group_id)
     REFERENCES group (id)
     ON DELETE CASCADE
Run Code Online (Sandbox Code Playgroud)

这样,DBMS本身会在您删除组时自动删除指向该组的行.无论是从Hibernate/JPA,JDBC,在DB中手动还是以任何其他方式进行删除,它都有效.

所有主要的DBMS(Oracle,MySQL,SQL Server,PostgreSQL)都支持级联删除功能.


Meh*_*ara 5

这对我有用:

@Transactional
public void remove(Integer groupId) {
    Group group = groupRepository.findOne(groupId);
    group.getUsers().removeAll(group.getUsers());

    // Other business logic

    groupRepository.delete(group);
}
Run Code Online (Sandbox Code Playgroud)

另外,标记方法@Transactional(org.springframework.transaction.annotation.Transactional),这将在一个会话中完成整个过程,节省一些时间。