Jak usunąć jednostkę z relacją ManyToMany w JPA (i odpowiednich wierszach tabeli łączenia)?

91

Powiedzmy, że mam dwie jednostki: grupę i użytkownika. Każdy użytkownik może być członkiem wielu grup, a każda grupa może mieć wielu użytkowników.

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

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

Teraz chcę usunąć grupę (powiedzmy, że ma wielu członków).

Problem polega na tym, że kiedy wywołuję EntityManager.remove () na jakiejś grupie, dostawca JPA (w moim przypadku Hibernate) nie usuwa wierszy z tabeli łączenia, a operacja usuwania nie udaje się z powodu ograniczeń klucza obcego. Wywołanie funkcji remove () na User działa dobrze (przypuszczam, że ma to coś wspólnego z posiadaniem strony relacji).

Jak więc mogę usunąć grupę w tym przypadku?

Jedynym sposobem, jaki mogłem wymyślić, jest załadowanie wszystkich użytkowników w grupie, a następnie dla każdego użytkownika usunięcie bieżącej grupy ze swoich grup i zaktualizowanie użytkownika. Ale wydaje mi się absurdalne wywoływanie funkcji update () na każdym użytkowniku z grupy tylko po to, aby móc usunąć tę grupę.

rdk
źródło

Odpowiedzi:

86
  • Własność relacji jest określona przez miejsce umieszczenia atrybutu „mappedBy” w adnotacji. Podmiot, który umieściłeś jako „mappedBy”, to ten, który NIE jest właścicielem. Nie ma szans, aby obie strony były właścicielami. Jeśli nie masz przypadku użycia „usuń użytkownika”, możesz po prostu przenieść własność na Grouppodmiot, ponieważ obecnie Userjest on właścicielem.
  • Z drugiej strony nie pytałeś o to, ale jedna rzecz, którą warto wiedzieć. Elementy groupsi usersnie są ze sobą łączone. Chodzi mi o to, że po usunięciu instancji User1 z Group1.users, kolekcje User1.groups nie zmieniają się automatycznie (co jest dla mnie dość zaskakujące),
  • Podsumowując, proponuję zdecydować, kto jest właścicielem. Powiedzmy, że Userjest właścicielem. Następnie podczas usuwania użytkownika relacyjna grupa użytkowników zostanie zaktualizowana automatycznie. Ale kiedy usuwasz grupę, musisz sam zadbać o usunięcie relacji w następujący sposób:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Grzegorz Oledzki
źródło
Dzięki! Miałem ten sam problem i twoje rozwiązanie go rozwiązało. Ale muszę wiedzieć, czy istnieje inny sposób rozwiązania tego problemu. Tworzy okropny kod. Dlaczego nie może to być em.remove (podmiot) i to wszystko?
Royi Freifeld
2
Czy jest to zoptymalizowane za kulisami? bo nie chcę sprawdzać całego zbioru danych.
środa
1
To naprawdę złe podejście, a co jeśli masz kilka tysięcy użytkowników w tej grupie?
Sergiy Sokolenko
2
Nie aktualizuje to tabeli relacji, w tabeli relacji nadal pozostają wiersze dotyczące tej relacji. Nie testowałem, ale to może potencjalnie powodować problemy.
Saygın Doğu,
@SergiySokolenko żadne zapytania do bazy danych nie są wykonywane w pętli for. Wszystkie z nich są grupowane po opuszczeniu funkcji transakcyjnej.
Kilves,
42

Poniższe działa dla mnie. Dodaj następującą metodę do jednostki, która nie jest właścicielem relacji (grupa)

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

Należy pamiętać, że aby to zadziałało, grupa musi mieć zaktualizowaną listę użytkowników (co nie jest wykonywane automatycznie). więc za każdym razem, gdy dodajesz grupę do listy grup w encji Użytkownik, należy również dodać użytkownika do listy użytkowników w encji Grupa.

damian
źródło
1
cascade = CascadeType.ALL nie należy ustawiać, jeśli chcesz użyć tego rozwiązania. W przeciwnym razie działa idealnie!
ltsstar
@damian Cześć, korzystałem z twojego rozwiązania, ale wystąpił błąd concurrentModficationException. Myślę, że jak wskazałeś, powodem może być fakt, że grupa musi mieć zaktualizowaną listę użytkowników. Ponieważ jeśli dodam więcej niż jedną grupę do użytkownika, otrzymam ten wyjątek ...
Adnan Abdul Khaliq
@Damian Pamiętaj, że aby to zadziałało, Grupa musi mieć zaktualizowaną listę Użytkowników (co nie jest robione automatycznie). więc za każdym razem, gdy dodajesz grupę do listy grup w encji Użytkownik, należy również dodać użytkownika do listy użytkowników w encji Grupa. Czy mógłbyś to rozwinąć, podaj mi przykład, dziękuję
Adnan Abdul Khaliq
27

Znalazłem możliwe rozwiązanie, ale ... nie wiem, czy to dobre rozwiązanie.

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

Próbowałem tego i działa. Po usunięciu roli usuwane są również relacje (ale nie jednostki uprawnień), a po usunięciu uprawnień relacje z rolą również są usuwane (ale nie instancja roli). Ale odwzorowujemy relację jednokierunkową dwa razy i oba podmioty są właścicielem relacji. Czy może to spowodować problemy z hibernacją? Jaki rodzaj problemów?

Dzięki!

Powyższy kod pochodzi z innego powiązanego postu .

galaretki
źródło
problemy z odświeżaniem kolekcji?
galaretki
12
To nie jest poprawne. Dwukierunkowe ManyToMany powinno mieć jedną i tylko jedną stronę właściciela relacji. To rozwiązanie sprawia, że ​​obie strony stają się właścicielami i ostatecznie doprowadzi do zduplikowanych rekordów. Aby uniknąć powielania rekordów, zamiast list należy używać zestawów. Ale użycie Set jest tylko obejściem robienia czegoś, co nie jest zalecane.
L. Holanda
4
więc jaka jest najlepsza praktyka dla tak powszechnego scenariusza?
SalutonMondo
8

Jako alternatywę dla rozwiązań JPA / Hibernate: możesz użyć klauzuli CASCADE DELETE w definicji bazy danych klucza obcego w tabeli łączenia, na przykład (składnia Oracle):

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

W ten sposób sam DBMS automatycznie usuwa wiersz wskazujący na grupę, kiedy usuwasz grupę. I działa niezależnie od tego, czy usuwanie jest dokonywane z Hibernate / JPA, JDBC, ręcznie w DB, czy w inny sposób.

funkcja usuwania kaskadowego jest obsługiwana przez wszystkie główne DBMS (Oracle, MySQL, SQL Server, PostgreSQL).

Pierre Henry
źródło
3

Co jest warte, używam EclipseLink 2.3.2.v20111125-r10461 i jeśli mam relację jednokierunkową @ManyToMany, obserwuję opisywany przez ciebie problem. Jeśli jednak zmienię go na relację dwukierunkową @ManyToMany, jestem w stanie usunąć jednostkę ze strony niebędącej właścicielem, a tabela JOIN zostanie odpowiednio zaktualizowana. To wszystko bez użycia atrybutów kaskadowych.

NBW
źródło
2

To działa dla mnie:

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

    // Other business logic

    groupRepository.delete(group);
}

Zaznacz również metodę @Transactional (org.springframework.transaction.annotation.Transactional), to wykona cały proces w jednej sesji, oszczędza trochę czasu.

Mehul Katpara
źródło
1

To jest dobre rozwiązanie. Najlepsze jest po stronie SQL - dostrojenie do dowolnego poziomu jest łatwe.

Użyłem MySql i MySql Workbench do kaskadowego usuwania wymaganego klucza obcego.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
user1776955
źródło
Witamy w SO. Poświęć chwilę i przyjrzyj się temu, aby poprawić formatowanie i
korektę
1

Oto, co ostatecznie zrobiłem. Mam nadzieję, że ktoś uzna to za przydatne.

@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
źródło
0

W moim przypadku usunąłem mappedBy i dołączyłem do tabel w ten sposób:

@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
źródło
3
Nie używaj cascadeType.all w wielu do wielu. Po usunięciu powoduje, że rekord A usuwa wszystkie skojarzone rekordy B. A rekordy B usuwają wszystkie powiązane rekordy A. I tak dalej. Nie, nie.
Josiah,
0

Działa to dla mnie w podobnym problemie, w którym nie udało mi się usunąć użytkownika z powodu odniesienia. Dziękuję Ci

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Ja ja
źródło