2011-10-26 19 views
18

Ho una domanda riguardante Hibernate 3.6.7 e JPA 2.0.Hibernate inserisce i duplicati in una raccolta @OneToMany

Considerare (alcuni getter e setter sono omessi per brevità) seguenti entità:

@Entity 
public class Parent { 
    @Id 
    @GeneratedValue 
    private int id; 

    @OneToMany(mappedBy="parent") 
    private List<Child> children = new LinkedList<Child>(); 

    @Override 
    public boolean equals(Object obj) { 
     return id == ((Parent)obj).id; 
    } 

    @Override 
    public int hashCode() { 
     return id; 
    } 
} 

@Entity 
public class Child { 
    @Id 
    @GeneratedValue 
    private int id; 

    @ManyToOne 
    private Parent parent; 

    public void setParent(Parent parent) { 
     this.parent = parent; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     return id == ((Child)obj).id; 
    } 

    @Override 
    public int hashCode() { 
     return id; 
    } 
} 

Ora considerare questo pezzo di codice:

// persist parent entity in a transaction 

EntityManager em = emf.createEntityManager(); 
em.getTransaction().begin(); 

Parent parent = new Parent(); 
em.persist(parent); 
int id = parent.getId(); 

em.getTransaction().commit(); 
em.close(); 

// relate and persist child entity in a new transaction 

em = emf.createEntityManager(); 
em.getTransaction().begin(); 

parent = em.find(Parent.class, id); 
// *: parent.getChildren().size(); 
Child child = new Child(); 
child.setParent(parent); 
parent.getChildren().add(child); 
em.persist(child); 

System.out.println(parent.getChildren()); // -> [[email protected], [email protected]] 

em.getTransaction().commit(); 
em.close(); 

è erroneamente viene inserito l'un'entità figlio due volte nella lista di bambini dell'entità genitore.

Quando si esegue uno dei seguenti, il codice funziona bene (non le voci duplicate nella lista):

  • rimuovere l'attributo mappedBy nella controllante
  • eseguire qualche operazione di lettura sulla lista dei figli (per esempio linea di commento contrassegnata da *)

Questo è ovviamente un comportamento molto strano. Inoltre, quando si utilizza EclipseLink come provider di persistenza, il codice funziona esattamente come previsto (nessun duplicato).

È un errore Hibernate o mi manca qualcosa?

Grazie

+0

Potrebbe aggiungere il codice del metodo setParent e degli Eguali/metodi hashCode? –

+0

Ho appena aggiunto i metodi che hai richiesto. Tuttavia, non penso che questo problema sia correlato a equals/hashCode. – user1014562

+1

I tuoi metodi di uguale non rispettano il contratto di Object.equals. Inoltre, l'hashCode cambia quando l'ID viene generato e assegnato all'entità. Non sarei sorpreso se il bug scomparisse dopo aver rimosso hashCode e uguale. BTW. Hibernate consiglia di non utilizzare ID per implementare equals e hashCode. –

risposta

27

Si tratta di un bug in Hibernate. Sorprendentemente, non è ancora stato segnalato, feel free to report it.

Le operazioni su raccolte pigre non inizializzate vengono accodate per eseguirle dopo l'inizializzazione della raccolta e Hibernate non gestisce la situazione quando queste operazioni sono in conflitto con i dati del database. Di solito non è un problema, perché questa coda viene cancellata su flush() e le possibili modifiche in conflitto vengono propagate al database su flush(). Tuttavia, alcune modifiche (come la persistenza di entità con id generati dal generatore di tipo IDENTITY, suppongo, è il tuo caso) vengono propagate al database senza l'intero flush(), e in questi casi i conflitti sono possibili.

Per risolvere il problema si può flush() la sessione dopo persistendo il bambino:

em.persist(child); 
em.flush(); 
+1

Grazie per la risposta e la soluzione! Ho creato un bug report e mi sono riferito alla tua risposta: [HHH-6776] (https://hibernate.onjira.com/browse/HHH-6776) – user1014562

+0

È davvero strano che questo bug non sia ancora risolto? È così povero da dover leggere gli oggetti prima di persistere. – nize

+0

Ho anche duplicato in unoMolti con una lista. Cambiare per impostare aiuta, comunque, perché sono obbligato a usare set quando voglio usare la lista? L'utilizzo della query sql generata di hibernate non restituisce duplicati. Tuttavia, l'ibernazione restituisce duplicati nell'elenco anche se la query non ha restituito duplicati. Impossibile trovare la soluzione senza usare il set. Non ho bisogno di usare flush, a causa del bean senza stato .. – nimo23

1

Utilizzando contesto enterprise Java in wildfly (8.2.0-Finale) (penso che sia Hibernate versione 4.3.7) la soluzione per me è stato, a persistere prima il bambino un l'aggiungerlo alla collezione pigro:

... 
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) 
public void test(){ 
    Child child = new Child(); 
    child.setParent(parent); 
    childFacade.create(child); 

    parent.getChildren().add(cild); 

    parentFacade.edit(parent); 
} 
2

ho risolto questo problema dicendo Hibernate non aggiungere i duplicati nella mia collezione. Nel tuo caso cambia il tipo del tuo campo da List<Child> a Set<Child> e attua equals(Object obj) e hashCode() nella classe Child.

Ovviamente questo non sarà possibile in ogni caso, ma se esiste un modo corretto per identificare che un'istanza Child è univoca, questa soluzione può essere relativamente indolore.

+0

E impostando @OneToMany ((. ..), cascade = CascadeType.MERGE) – Hinotori

2

Mi sono imbattuto in questa domanda quando ho riscontrato problemi con l'aggiunta di elementi a un elenco annotato con @OneToMany, ma quando si tenta di iterare sugli elementi di tale elenco. Gli elementi nell'elenco erano sempre duplicati, a volte molto più di due volte. (Si è verificato anche quando annotato con @ManyToMany). L'uso di un set non era una soluzione qui, poiché questi elenchi dovevano consentire elementi duplicati in essi.

Esempio:

@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) 
@Cascade(CascadeType.ALL) 
@LazyCollection(LazyCollectionOption.FALSE) 
private List entities; 

Come si è scoperto, Hibernate esegue le istruzioni SQL utilizzando left outer join, che può portare a risultati duplicati restituiti dal db. Che cosa ha aiutato è stato semplicemente definendo un ordinamento sul risultato, utilizzando OrderColumn:

@OrderColumn(name = "columnName")
+0

non posso ringraziarti abbastanza, puoi indicarmi una risorsa in cui posso leggere di più su questo? –

0

gestito questa semplicemente chiamando il metodo vuoto(). Per questo scenario,

parent.getChildren().isEmpty()

prima

parent.getChildren().add(child); 
Problemi correlati