Wie entferne ich eine Entität mit ManyToMany-Beziehung in JPA (und entsprechenden Join-Tabellenzeilen)?

87

Angenommen, ich habe zwei Entitäten: Gruppe und Benutzer. Jeder Benutzer kann Mitglied vieler Gruppen sein und jede Gruppe kann viele Benutzer haben.

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

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

Jetzt möchte ich eine Gruppe entfernen (sagen wir, sie hat viele Mitglieder).

Das Problem ist, dass der JPA-Anbieter (in meinem Fall Hibernate) beim Aufrufen von EntityManager.remove () in einer Gruppe keine Zeilen aus der Join-Tabelle entfernt und der Löschvorgang aufgrund von Fremdschlüsseleinschränkungen fehlschlägt. Das Aufrufen von remove () für den Benutzer funktioniert einwandfrei (ich denke, dies hat etwas mit dem Besitz der Seite der Beziehung zu tun).

Wie kann ich in diesem Fall eine Gruppe entfernen?

Die einzige Möglichkeit, die ich finden könnte, besteht darin, alle Benutzer in der Gruppe zu laden und dann für jeden Benutzer die aktuelle Gruppe aus seinen Gruppen zu entfernen und den Benutzer zu aktualisieren. Es erscheint mir jedoch lächerlich, bei jedem Benutzer aus der Gruppe update () aufzurufen, um diese Gruppe löschen zu können.

rdk
quelle

Antworten:

84
  • Der Besitz der Beziehung wird dadurch bestimmt, wo Sie das Attribut 'mappedBy' in die Anmerkung einfügen. Die Entität, die Sie 'mappedBy' setzen, ist diejenige, die NICHT der Eigentümer ist. Es gibt keine Chance für beide Seiten, Eigentümer zu sein. Wenn Sie keinen Anwendungsfall "Benutzer löschen" haben, können Sie den Eigentümer einfach auf die GroupEntität verschieben, da derzeit Userder Eigentümer der Eigentümer ist.
  • Auf der anderen Seite haben Sie nicht danach gefragt, aber eine Sache, die Sie wissen sollten. Die groupsund userssind nicht miteinander kombiniert. Ich meine, nach dem Löschen der User1-Instanz aus Group1.users werden die User1.groups-Sammlungen nicht automatisch geändert (was für mich ziemlich überraschend ist).
  • Alles in allem würde ich vorschlagen, dass Sie entscheiden, wer der Eigentümer ist. Nehmen wir an, das Userist der Besitzer. Beim Löschen eines Benutzers wird dann die Beziehung Benutzergruppe automatisch aktualisiert. Aber wenn Sie eine Gruppe löschen, müssen Sie dafür sorgen, dass die Beziehung selbst wie folgt gelöscht wird:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Grzegorz Oledzki
quelle
Danke! Ich hatte das gleiche Problem und Ihre Lösung hat es gelöst. Aber ich muss wissen, ob es einen anderen Weg gibt, dieses Problem zu lösen. Es erzeugt einen schrecklichen Code. Warum kann es nicht em.remove (Entität) sein und das war's?
Royi Freifeld
2
Ist das hinter den Kulissen optimiert? Weil ich nicht den gesamten Datensatz abfragen möchte.
Ced
1
Dies ist ein wirklich schlechter Ansatz. Was ist, wenn Sie mehrere tausend Benutzer in dieser Gruppe haben?
Sergiy Sokolenko
1
Dadurch wird die Beziehungstabelle nicht aktualisiert. In der Beziehungstabelle bleiben die Zeilen zu dieser Beziehung weiterhin erhalten. Ich habe nicht getestet, aber das könnte möglicherweise Probleme verursachen.
Saygın Doğu
@SergiySokolenko In dieser for-Schleife werden keine Datenbankabfragen ausgeführt. Alle werden gestapelt, nachdem die Transaktionsfunktion verlassen wurde.
Kilves
42

Folgendes funktioniert für mich. Fügen Sie der Entität, die nicht Eigentümer der Beziehung ist, die folgende Methode hinzu (Gruppe)

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

Beachten Sie, dass die Gruppe über eine aktualisierte Benutzerliste verfügen muss, damit dies funktioniert (was nicht automatisch erfolgt). Jedes Mal, wenn Sie der Gruppenliste in der Benutzerentität eine Gruppe hinzufügen, sollten Sie der Benutzerliste in der Gruppenentität einen Benutzer hinzufügen.

Damian
quelle
1
cascade = CascadeType.ALL sollte nicht festgelegt werden, wenn Sie diese Lösung verwenden möchten. Ansonsten funktioniert es perfekt!
ltsstar
@damian Hallo, ich habe Ihre Lösung verwendet, aber ich habe einen Fehler erhalten concurrentModficationException Ich denke, als Sie darauf hingewiesen haben, könnte der Grund dafür sein, dass die Gruppe eine aktualisierte Liste der Benutzer haben muss. Denn wenn ich dem Benutzer mehr als eine Gruppe hinzufüge, erhalte ich diese Ausnahme ...
Adnan Abdul Khaliq
@Damian Beachten Sie, dass die Gruppe über eine aktualisierte Benutzerliste verfügen muss, damit dies funktioniert (was nicht automatisch erfolgt). Jedes Mal, wenn Sie der Gruppenliste in der Benutzerentität eine Gruppe hinzufügen, sollten Sie der Benutzerliste in der Gruppenentität einen Benutzer hinzufügen. Könnten Sie bitte näher darauf eingehen, geben Sie mir ein Beispiel, danke
Adnan Abdul Khaliq
27

Ich habe eine mögliche Lösung gefunden, aber ... ich weiß nicht, ob es eine gute Lösung ist.

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

Ich habe es versucht und es funktioniert. Wenn Sie die Rolle löschen, werden auch die Beziehungen gelöscht (jedoch nicht die Berechtigungsentitäten), und wenn Sie die Berechtigung löschen, werden auch die Beziehungen zur Rolle gelöscht (jedoch nicht die Rolleninstanz). Wir ordnen jedoch zweimal eine unidirektionale Beziehung zu, und beide Entitäten sind Eigentümer der Beziehung. Könnte dies zu Problemen im Ruhezustand führen? Welche Art von Problemen?

Vielen Dank!

Der obige Code stammt aus einem anderen Beitrag .

Gelees
quelle
Probleme beim Auffrischen der Sammlung?
Gelees
12
Das ist nicht richtig. Bidirektional ManyToMany sollte nur eine Eigentümerseite der Beziehung haben. Diese Lösung macht beide Seiten zu Eigentümern und führt schließlich zu doppelten Datensätzen. Um doppelte Datensätze zu vermeiden, müssen anstelle von Listen Sets verwendet werden. Die Verwendung von Set ist jedoch nur eine Problemumgehung, um etwas zu tun, das nicht empfohlen wird.
L. Holanda
3
Was ist dann die beste Vorgehensweise für solch ein gemeinsames Szenario?
SalutonMondo
7

Als Alternative zu JPA / Hibernate-Lösungen: Sie können eine CASCADE DELETE-Klausel in der Datenbankdefinition Ihres Foregin-Schlüssels in Ihrer Join-Tabelle verwenden, z. B. (Oracle-Syntax):

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

Auf diese Weise löscht das DBMS selbst automatisch die Zeile, die auf die Gruppe verweist, wenn Sie die Gruppe löschen. Und es funktioniert, ob das Löschen aus dem Ruhezustand / JPA, JDBC, manuell in der Datenbank oder auf andere Weise erfolgt.

Die Kaskadenlöschfunktion wird von allen wichtigen DBMS (Oracle, MySQL, SQL Server, PostgreSQL) unterstützt.

Pierre Henry
quelle
3

Für was es wert ist, verwende ich EclipseLink 2.3.2.v20111125-r10461 und wenn ich eine unidirektionale @ ManyToMany-Beziehung habe, beobachte ich das von Ihnen beschriebene Problem. Wenn ich es jedoch in eine bidirektionale @ ManyToMany-Beziehung ändere, kann ich eine Entität von der nicht besitzenden Seite löschen und die JOIN-Tabelle wird entsprechend aktualisiert. Dies ist alles ohne die Verwendung von Kaskadenattributen.

NBW
quelle
1

Dies ist eine gute Lösung. Das Beste ist auf der SQL-Seite - die Feinabstimmung auf jede Ebene ist einfach.

Ich habe MySql und MySql Workbench verwendet, um beim Löschen für den erforderlichen Fremdschlüssel eine Kaskade zu erstellen.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
user1776955
quelle
Willkommen bei SO. Bitte nehmen Sie sich einen Moment Zeit und schauen Sie sich dies an, um Ihre Formatierung und das Korrekturlesen zu verbessern: stackoverflow.com/help/how-to-ask
petezurich
0

Das funktioniert bei mir:

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

    // Other business logic

    groupRepository.delete(group);
}

Markieren Sie auch die Methode @Transactional (org.springframework.transaction.annotation.Transactional). Dies führt den gesamten Prozess in einer Sitzung aus und spart Zeit.

Mehul Katpara
quelle
0

Für meinen Fall habe ich das mappedBy gelöscht und Tabellen wie folgt verbunden:

@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
quelle
2
Verwenden Sie cascadeType.all nicht in vielen zu vielen. Beim Löschen löscht der A-Datensatz alle zugehörigen B-Datensätze. Und die B-Datensätze löschen alle zugehörigen A-Datensätze. Und so weiter. Großes Nein-Nein.
Josiah
0

Dies funktioniert bei einem ähnlichen Problem, bei dem ich den Benutzer aufgrund der Referenz nicht löschen konnte. Danke dir

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