Come rimuovere l'entità con la relazione ManyToMany in JPA (e le corrispondenti righe della tabella di join)?


91

Diciamo che ho due entità: Gruppo e Utente. Ogni utente può essere membro di molti gruppi e ogni gruppo può avere molti utenti.

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

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

Ora voglio rimuovere un gruppo (diciamo che ha molti membri).

Il problema è che quando chiamo EntityManager.remove () su alcuni gruppi, il provider JPA (nel mio caso Hibernate) non rimuove le righe dalla tabella di join e l'operazione di eliminazione non riesce a causa di vincoli di chiave esterna. La chiamata di remove () sull'utente funziona bene (immagino che abbia qualcosa a che fare con il lato proprietario della relazione).

Quindi come posso rimuovere un gruppo in questo caso?

L'unico modo che ho potuto ottenere è caricare tutti gli utenti nel gruppo, quindi per ogni utente rimuovere il gruppo corrente dai suoi gruppi e aggiornare l'utente. Ma mi sembra ridicolo chiamare update () su ogni utente del gruppo solo per poter eliminare questo gruppo.

Risposte:


86
  • La proprietà della relazione è determinata dalla posizione in cui si inserisce l'attributo "mappedBy" nell'annotazione. L'entità che metti "mappedBy" è quella che NON è il proprietario. Non c'è alcuna possibilità per entrambe le parti di essere proprietari. Se non si dispone di un caso d'uso "elimina utente", è possibile spostare semplicemente la proprietà Groupall'entità, poiché attualmente Userè il proprietario.
  • D'altra parte, non me l'hai chiesto, ma una cosa che vale la pena sapere. I groupse usersnon sono combinati tra loro. Voglio dire, dopo aver eliminato l'istanza di User1 da Group1.users, le raccolte di User1.groups non vengono modificate automaticamente (il che è abbastanza sorprendente per me),
  • Tutto sommato, ti suggerirei di decidere chi è il proprietario. Diciamo che Userè il proprietario. Quindi, quando si elimina un utente, la relazione user-group verrà aggiornata automaticamente. Ma quando elimini un gruppo devi occuparti di eliminare tu stesso la relazione in questo modo:

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

Thnx! Ho avuto lo stesso problema e la tua soluzione lo ha risolto. Ma devo sapere se c'è un altro modo per risolvere questo problema. Produce un codice orribile. Perché non può essere em .remove (entity) e basta?
Royi Freifeld

2
È ottimizzato dietro le quinte? perchè non voglio interrogare l'intero dataset.
Ced

1
Questo è davvero un pessimo approccio, cosa succede se hai diverse migliaia di utenti in quel gruppo?
Sergiy Sokolenko

2
Questo non aggiorna la tabella delle relazioni, nella tabella delle relazioni rimangono ancora le righe su questa relazione. Non ho provato ma questo potrebbe potenzialmente causare problemi.
Saygın Doğu

@SergiySokolenko nessuna query di database viene eseguita in quel ciclo for. Tutti sono raggruppati dopo che la funzione Transazionale è stata lasciata.
Kilves

42

Quanto segue funziona per me. Aggiungi il seguente metodo all'entità che non è proprietaria della relazione (Gruppo)

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

Tieni presente che, affinché funzioni, il gruppo deve disporre di un elenco aggiornato di utenti (che non viene eseguito automaticamente). quindi ogni volta che aggiungi un gruppo all'elenco dei gruppi nell'entità Utente, dovresti anche aggiungere un utente all'elenco degli utenti nell'entità Gruppo.


1
cascade = CascadeType.ALL non deve essere impostato quando si desidera utilizzare questa soluzione. Altrimenti funziona perfettamente!
ltsstar

@damian Ciao, ho usato la tua soluzione, ma ho ricevuto un errore concurrentModficationException penso che come hai sottolineato il motivo potrebbe essere che il gruppo deve avere un elenco aggiornato di utenti. Perché se aggiungo più di un gruppo all'utente ottengo questa eccezione ...
Adnan Abdul Khaliq

@Damian Tieni presente che affinché funzioni, il gruppo deve avere un elenco aggiornato di utenti (che non viene fatto automaticamente). quindi ogni volta che aggiungi un gruppo all'elenco dei gruppi nell'entità Utente, dovresti anche aggiungere un utente all'elenco degli utenti nell'entità Gruppo. Potresti approfondire questo, fammi un esempio, grazie
Adnan Abdul Khaliq

27

Ho trovato una possibile soluzione, ma ... non so se sia una buona soluzione.

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

L'ho provato e funziona. Quando elimini il ruolo, vengono eliminate anche le relazioni (ma non le entità di autorizzazione) e quando elimini l'autorizzazione, vengono eliminate anche le relazioni con il ruolo (ma non l'istanza del ruolo). Ma stiamo mappando una relazione unidirezionale due volte ed entrambe le entità sono le proprietarie della relazione. Questo potrebbe causare alcuni problemi a Hibernate? Che tipo di problemi?

Grazie!

Il codice sopra è da un altro post correlato.


raccolta di problemi di aggiornamento?
gelatine

12
Questo non è corretto. ManyToMany bidirezionale dovrebbe avere un solo lato proprietario della relazione. Questa soluzione sta rendendo entrambe le parti come proprietari e alla fine si tradurrà in record duplicati. Per evitare record duplicati, è necessario utilizzare i set anziché gli elenchi. Ma l'uso di Set è solo una soluzione alternativa per fare qualcosa che non è raccomandato.
L. Holanda

4
allora qual è la migliore pratica per uno scenario così comune?
SalutonMondo

8

In alternativa alle soluzioni JPA / Hibernate: è possibile utilizzare una clausola CASCADE DELETE nella definizione del database della chiave foregin sulla tabella di join, ad esempio (sintassi Oracle):

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

In questo modo il DBMS stesso elimina automaticamente la riga che punta al gruppo quando si elimina il gruppo. E funziona se l'eliminazione viene effettuata da Hibernate / JPA, JDBC, manualmente nel DB o in qualsiasi altro modo.

la funzionalità di eliminazione a cascata è supportata da tutti i principali DBMS (Oracle, MySQL, SQL Server, PostgreSQL).


3

Per quel che vale, sto usando EclipseLink 2.3.2.v20111125-r10461 e se ho una relazione unidirezionale @ManyToMany osservo il problema che descrivi. Tuttavia, se cambio una relazione @ManyToMany bidirezionale, sono in grado di eliminare un'entità dal lato non proprietario e la tabella JOIN viene aggiornata in modo appropriato. Tutto questo senza l'uso di attributi a cascata.


2

Questo funziona per me:

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

    // Other business logic

    groupRepository.delete(group);
}

Inoltre, contrassegna il metodo @Transactional (org.springframework.transaction.annotation.Transactional), questo eseguirà l'intero processo in una sessione, risparmiando tempo.


1

Questa è una buona soluzione. La parte migliore è sul lato SQL: la regolazione fine a qualsiasi livello è facile.

Ho usato MySql e MySql Workbench per Cascade on delete per la chiave esterna richiesta.

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

Benvenuto in SO. Si prega di prendere un attimo e guardare in questo per migliorare la vostra formattazione e correzione di bozze: stackoverflow.com/help/how-to-ask
petezurich

1

Questo è quello che ho finito per fare. Si spera che qualcuno possa trovarlo 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);
}

0

Per il mio caso, ho eliminato mappedBy e unito tabelle in questo modo:

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

3
Non utilizzare cascadeType.all in many-to-many. In caso di eliminazione, il record A elimina tutti i record B associati. E i record B eliminano tutti i record A associati. E così via. Grande no-no.
Josiah

0

Questo funziona per me su un problema simile in cui non sono riuscito a eliminare l'utente a causa del riferimento. Grazie

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.