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

91

Disons que j'ai deux entités: Groupe et Utilisateur. Chaque utilisateur peut être membre de plusieurs 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 de nombreux 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 de jointure et 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?

Le seul moyen que je pourrais trouver est de charger tous les utilisateurs du groupe, puis pour chaque utilisateur, supprimez le groupe actuel de ses groupes et mettez à jour l'utilisateur. Mais il me semble ridicule d'appeler update () sur chaque utilisateur du groupe juste pour pouvoir supprimer ce groupe.

rdk
la source

Réponses:

86
  • La propriété de la relation est déterminée par l'endroit où vous placez l'attribut «mappedBy» dans l'annotation. L'entité que vous mettez «mappedBy» est celle qui n'est PAS le propriétaire. Il n'y a aucune chance pour les deux parties d'être propriétaires. Si vous n'avez pas de cas d'utilisation «supprimer un utilisateur», vous pouvez simplement déplacer la propriété vers l' Groupentité, car il en Userest actuellement le propriétaire.
  • D'un autre côté, vous n'avez pas posé de questions à ce sujet, mais une chose vaut la peine de savoir. Les groupset usersne sont pas combinés entre eux. Je veux dire, après la suppression de 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ère de décider qui est le propriétaire. Disons que Userc'est le propriétaire. Ensuite, lors de la suppression d'un utilisateur, la relation utilisateur-groupe sera mise à jour automatiquement. Mais lors de la suppression d'un groupe, vous devez vous occuper de supprimer la relation vous-même comme ceci:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Grzegorz Oledzki
la source
Thnx! J'ai eu le même problème et votre solution l'a résolu. Mais je dois savoir s'il existe une autre façon de résoudre ce problème. Cela produit un code horrible. Pourquoi ne peut-il pas être em.remove (entité) et c'est tout?
Royi Freifeld
2
Est-ce optimisé dans les coulisses? cuz je ne veux pas interroger l'ensemble de données.
Ced
1
C'est vraiment une mauvaise approche, que faire si vous avez plusieurs milliers d'utilisateurs dans ce groupe?
Sergiy Sokolenko
2
Cela ne met pas à jour la table de relations, dans la table de relations, les lignes concernant cette relation restent toujours. Je n'ai pas testé mais cela pourrait potentiellement causer des problèmes.
Saygın Doğu
@SergiySokolenko aucune requête de base de données n'est exécutée dans cette boucle for. Tous sont regroupés une fois la fonction transactionnelle terminée.
Kilves
42

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

Gardez à l'esprit que pour que cela fonctionne, le groupe doit disposer d'une liste à jour des utilisateurs (ce qui n'est pas fait automatiquement). Ainsi, 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 dans l'entité Groupe.

Damien
la source
1
cascade = CascadeType.ALL ne doit pas être défini lorsque vous souhaitez utiliser cette solution. Sinon, cela fonctionne parfaitement!
ltsstar
@damian Salut, j'ai utilisé votre solution, mais j'ai une erreur concurrentModficationException, je pense que, comme vous l'avez souligné, la raison pourrait être que le groupe doit avoir une liste d'utilisateurs à jour. Parce que si j'ajoute plus d'un groupe à l'utilisateur, j'obtiens cette exception ...
Adnan Abdul Khaliq
@Damian Gardez à l'esprit que pour que cela fonctionne, le groupe doit disposer d'une liste d'utilisateurs mise à jour (ce qui n'est pas fait automatiquement). Ainsi, chaque fois que vous ajoutez un groupe à la liste des groupes dans l'entité Utilisateur, vous devez également ajouter un utilisateur à la liste des utilisateurs dans l'entité Groupe. Pourriez-vous s'il vous plaît élaborer sur cela, donnez-moi un exemple, merci
Adnan Abdul Khaliq
27

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é ceci et cela fonctionne. 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 cartographions une relation unidirectionnelle deux fois et les deux entités sont le propriétaire de la relation. Cela pourrait-il causer des problèmes de mise en veille prolongée? Quel type de problèmes?

Merci!

Le code ci-dessus provient d'un autre article lié.

gelées
la source
des problèmes de rafraîchissement de la collection?
jelies
12
Ce n'est pas correct. Bidirectionnel ManyToMany doit avoir un et un seul côté propriétaire de la relation. Cette solution fait des deux côtés des propriétaires et entraînera éventuellement des enregistrements en double. Pour éviter les enregistrements en double, des ensembles doivent être utilisés à la place des listes. Mais, l'utilisation de Set n'est qu'une solution de contournement pour faire quelque chose qui n'est pas recommandé.
L. Holanda
4
alors quelle est la meilleure pratique pour un scénario aussi commun?
SalutonMondo
8

Comme alternative aux solutions JPA / Hibernate: vous pouvez utiliser une clause CASCADE DELETE dans la définition de 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

De cette façon, le SGBD supprime automatiquement la ligne qui pointe vers le groupe lorsque vous supprimez le groupe. Et cela fonctionne que la suppression soit faite à 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).

Pierre Henry
la source
3

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. Cependant, si je le change en une relation bidirectionnelle @ManyToMany, je suis en mesure de supprimer une entité du côté non propriétaire et la table JOIN est mise à jour de manière appropriée. Tout cela sans l'utilisation d'attributs en cascade.

NBW
la source
2

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

Aussi, marquez la méthode @Transactional (org.springframework.transaction.annotation.Transactional), cela fera tout le processus en une seule session, ce qui vous fera gagner du temps.

Mehul Katpara
la source
1

C'est une bonne solution. La meilleure partie est du côté SQL - il est facile de régler finement à n'importe quel niveau.

J'ai utilisé MySql et MySql Workbench pour Cascade lors de la suppression de la clé étrangère requise.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
user1776955
la source
Bienvenue à SO. Veuillez prendre un moment et examiner ceci pour améliorer votre formatage et votre relecture: stackoverflow.com/help/how-to-ask
petezurich
1

C'est ce que j'ai fini par faire. J'espère que quelqu'un pourra le trouver utile.

@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);
}
Stephen Paul
la source
0

Pour mon cas, j'ai supprimé les tables mappedBy 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;
szachMati
la source
3
N'utilisez pas cascadeType.all dans plusieurs-à-plusieurs. Lors de la suppression, l'enregistrement A supprime tous les enregistrements B associés. Et les enregistrements B suppriment tous les enregistrements A associés. Etc. Gros non-non.
Josiah
0

Cela fonctionne pour moi sur un problème similaire où je n'ai pas réussi à supprimer l'utilisateur en raison de la référence. Merci

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Moi moi
la source