web-dev-qa-db-fra.com

Comment supprimer une entité avec une relation ManyToMany dans JPA (et les lignes de la table de jointure correspondantes)?

Disons que j'ai deux entités: groupe et utilisateur. Chaque utilisateur peut être membre de nombreux groupes et chaque groupe peut avoir de nombreux utilisateurs.

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

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

Maintenant, je veux supprimer un groupe (disons qu'il a beaucoup de membres).

Le problème est que, lorsque j'appelle EntityManager.remove () sur un groupe, le fournisseur JPA (dans mon cas, Hibernate) ne supprime pas les lignes de la table jointe et que l'opération de suppression échoue en raison de contraintes de clé étrangère. L'appel de remove () sur User fonctionne bien (je suppose que cela a quelque chose à voir avec le côté propriétaire de la relation).

Alors, comment puis-je supprimer un groupe dans ce cas? 

La seule façon dont je pourrais arriver est de charger tous les utilisateurs du groupe, puis, pour chaque utilisateur, supprimer le groupe actuel de ses groupes et mettre à jour l'utilisateur. Mais il me semble ridicule d'appeler update () sur chaque utilisateur du groupe simplement pour pouvoir supprimer ce groupe.

74
rdk
  • La propriété de la relation est déterminée par l'emplacement où vous placez l'attribut "mappéBy" dans l'annotation. L'entité que vous avez mise 'mappedBy' est celle qui n'est PAS le propriétaire. Il n'y a aucune chance que les deux côtés soient propriétaires. Si vous n'avez pas de cas d'utilisation 'supprimer utilisateur', vous pouvez simplement déplacer le propriétaire vers l'entité Group, car actuellement User est le propriétaire.
  • D'autre part, vous ne posez pas de question à ce sujet, mais une chose qui mérite d'être connue. groups et users ne sont pas combinés. Je veux dire, après avoir supprimé l'instance User1 de Group1.users, les collections User1.groups ne sont pas modifiées automatiquement (ce qui est assez surprenant pour moi)
  • Dans l'ensemble, je vous suggérerais de décider qui est le propriétaire. Disons que la User est le propriétaire. Ensuite, lors de la suppression d'un utilisateur, la relation groupe d'utilisateurs sera automatiquement mise à jour. Mais lors de la suppression d'un groupe, vous devez vous assurer de supprimer vous-même la relation comme ceci:

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

Ce qui suit fonctionne pour moi. Ajoutez la méthode suivante à l'entité qui n'est pas le propriétaire de la relation (Groupe)

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

N'oubliez pas que pour que cela fonctionne, le groupe doit disposer d'une liste d'utilisateurs mise à jour (ce qui n'est pas fait automatiquement). Par conséquent, chaque fois que vous ajoutez un groupe à la liste de groupes dans l'entité Utilisateur, vous devez également ajouter un utilisateur à la liste d'utilisateurs de l'entité Groupe.

36
Damian

J'ai trouvé une solution possible, mais ... Je ne sais pas si c'est une bonne 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;
    }

J'ai essayé cela et ça marche. Lorsque vous supprimez le rôle, les relations sont également supprimées (mais pas les entités d'autorisation) et lorsque vous supprimez l'autorisation, les relations avec le rôle sont également supprimées (mais pas l'instance de rôle). Mais nous mappons une relation unidirectionnelle deux fois et les deux entités sont le propriétaire de la relation. Cela pourrait-il causer des problèmes à Hibernate? Quel type de problèmes?

Merci!

Le code ci-dessus provient d'un autre post related.

24
jelies

Comme alternative aux solutions JPA/Hibernate: vous pouvez utiliser une clause CASCADE DELETE dans la définition de la base de données de votre clé foregin sur votre table de jointure, telle que (syntaxe Oracle):

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

Ainsi, le SGBD lui-même supprime automatiquement la ligne qui pointe vers le groupe lorsque vous supprimez le groupe. Et cela fonctionne que la suppression soit effectuée à partir de Hibernate/JPA, JDBC, manuellement dans la base de données ou de toute autre manière.

la fonction de suppression en cascade est prise en charge par tous les principaux SGBD (Oracle, MySQL, SQL Server, PostgreSQL).

7
Pierre Henry

Pour ce que ça vaut, j'utilise EclipseLink 2.3.2.v20111125-r10461 et si j'ai une relation unidirectionnelle @ManyToMany, j'observe le problème que vous décrivez. Toutefois, si je change la relation en une relation bidirectionnelle @ManyToMany, je peux supprimer une entité du côté des non-propriétaires et la table JOIN est mise à jour de manière appropriée. Tout cela sans l'utilisation d'attributs en cascade.

3
NBW

C'est une bonne solution. La meilleure partie est du côté SQL: il est facile d’ajuster tous les niveaux.

J'ai utilisé MySql et MySql Workbench pour mettre en cascade en suppression la clé étrangère requise.

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

Pour mon cas, j'ai supprimé les tables mappées et jointes comme ceci: 

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "user_group", joinColumns = {
        @JoinColumn(name = "user", referencedColumnName = "user_id")
}, inverseJoinColumns = {
        @JoinColumn(name = "group", referencedColumnName = "group_id")
})
private List<User> users;

@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JsonIgnore
private List<Group> groups;
1
szachMati

Cela fonctionne pour moi:

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

    // Other business logic

    groupRepository.delete(group);
}

En outre, marquez la méthode @Transactional (org.springframework.transaction.annotation.Transactional), cela fera tout le processus en une session et vous fera gagner du temps.

0
Mehul Katpara