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

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

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


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

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

问题是,当我在一些组上调用 EntityManager.move ()时,由于外键约束,JPA 提供程序(在我的例子中是 Hibernate) 不从联接表中删除行和 delete 操作失败。在 User 上调用 move ()可以正常工作(我猜这与拥有关系有关)。

那么在这种情况下,我如何删除一个组呢?

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

141167 次浏览
  • The ownership of the relation is determined by where you place the 'mappedBy' attribute to the annotation. The entity you put 'mappedBy' is the one which is NOT the owner. There's no chance for both sides to be owners. If you don't have a 'delete user' use-case you could simply move the ownership to the Group entity, as currently the User is the owner.
  • On the other hand, you haven't been asking about it, but one thing worth to know. The groups and users are not combined with each other. I mean, after deleting User1 instance from Group1.users, the User1.groups collections is not changed automatically (which is quite surprising for me),
  • All in all, I would suggest you decide who is the owner. Let say the User is the owner. Then when deleting a user the relation user-group will be updated automatically. But when deleting a group you have to take care of deleting the relation yourself like this:

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

I found a possible solution, but... I don't know if it's a good solution.

@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;
}

I have tried this and it works. When you delete Role, also the relations are deleted (but not the Permission entities) and when you delete Permission, the relations with Role are deleted too (but not the Role instance). But we are mapping a unidirectional relation two times and both entities are the owner of the relation. Could this cause some problems to Hibernate? Which type of problems?

Thanks!

The code above is from another post related.

For what its worth, I am using EclipseLink 2.3.2.v20111125-r10461 and if I have a @ManyToMany unidirectional relationship I observe the problem that you describe. However, if I change it to be a bi-directional @ManyToMany relationship I am able to delete an entity from the non-owning side and the JOIN table is updated appropriately. This is all without the use of any cascade attributes.

The following works for me. Add the following method to the entity that is not the owner of the relationship (Group)

@PreRemove
private void removeGroupsFromUsers() {
for (User u : users) {
u.getGroups().remove(this);
}
}

Keep in mind that for this to work, the Group must have an updated list of Users (which is not done automatically). so everytime you add a Group to the group list in User entity, you should also add a User to the user list in the Group entity.

As an alternative to JPA/Hibernate solutions : you could use a CASCADE DELETE clause in the database definition of your foregin key on your join table, such as (Oracle syntax) :

CONSTRAINT fk_to_group
FOREIGN KEY (group_id)
REFERENCES group (id)
ON DELETE CASCADE

That way the DBMS itself automatically deletes the row that points to the group when you delete the group. And it works whether the delete is made from Hibernate/JPA, JDBC, manually in the DB or any other way.

the cascade delete feature is supported by all major DBMS (Oracle, MySQL, SQL Server, PostgreSQL).

This is a good solution. The best part is on the SQL side – fine tuning to any level is easy.

I used MySql and MySql Workbench to Cascade on delete for the Required Foreign KEY.

ALTER TABLE schema.joined_table
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;

This works for me:

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


// Other business logic


groupRepository.delete(group);
}

Also, mark the method @Transactional (org.springframework.transaction.annotation.Transactional), this will do whole process in one session, saves some time.

This works for me on a similar issue where I failed to delete the user due to the reference. Thank you

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})

This is what I ended up doing. Hopefully someone might find it useful.

@Transactional
public void deleteGroup(Long groupId) {
Group group = groupRepository.findById(groupId).orElseThrow();
group.getUsers().forEach(u -> u.getGroups().remove(group));
userRepository.saveAll(group.getUsers());
groupRepository.delete(group);
}

If you are using Spring Data Jpa, then simply create a repository interface for the owner class Group.class, then use their deleteById(Long id) method extended from JpaRepository.class. Then, when you delete a Group, the related rows(containing the same group id as you specify) in the join table will also be removed. Be aware of the CascadeType, avoid CascadeType.All, otherwise it will attempt to delete the user from the User table, which would cause the foreign key constraint runtime error again.