Hibernate Criteria restituisce i figli più volte con FetchType.EAGER


115

Ho una Orderclasse che ha un elenco di OrderTransactionse l'ho mappata con una mappatura Hibernate uno-a-molti in questo modo:

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Questi Orderhanno anche un campo orderStatus, che viene utilizzato per filtrare con i seguenti criteri:

public List<Order> getOrderForProduct(OrderFilter orderFilter) {
    Criteria criteria = getHibernateSession()
            .createCriteria(Order.class)
            .add(Restrictions.in("orderStatus", orderFilter.getStatusesToShow()));
    return criteria.list();
}

Funziona e il risultato è come previsto.

Ora ecco la mia domanda : perché, quando imposto il tipo di recupero esplicitamente su EAGER, le Orders vengono visualizzate più volte nell'elenco risultante?

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Come dovrei cambiare il mio codice Criteri per ottenere lo stesso risultato con la nuova impostazione?


1
Hai provato ad abilitare show_sql per vedere cosa sta succedendo sotto?
Mirko N.

Aggiungi anche il codice delle classi OrderTransaction e Order. \
Eran Medan

Risposte:


115

Questo è in realtà il comportamento previsto se ho capito correttamente la tua configurazione.

Ottieni la stessa Orderistanza in uno qualsiasi dei risultati, ma poiché ora stai facendo un join con OrderTransaction, deve restituire la stessa quantità di risultati che restituirà un normale sql join

Quindi in realtà dovrebbe apparire più volte. questo è spiegato molto bene dallo stesso autore (Gavin King) qui : spiega sia perché, sia come ottenere risultati distinti


Menzionato anche nelle FAQ di Hibernate :

Hibernate non restituisce risultati distinti per una query con il recupero del join esterno abilitato per una raccolta (anche se utilizzo la parola chiave distinta)? Innanzitutto, è necessario comprendere SQL e come funzionano gli OUTER JOIN in SQL. Se non comprendi e comprendi appieno gli outer join in SQL, non continuare a leggere questo articolo delle domande frequenti ma consulta un manuale o un'esercitazione SQL. Altrimenti non capirai la seguente spiegazione e ti lamenterai di questo comportamento sul forum di Hibernate.

Esempi tipici che potrebbero restituire riferimenti duplicati dello stesso oggetto Order:

List result = session.createCriteria(Order.class)
                    .setFetchMode("lineItems", FetchMode.JOIN)
                    .list();

<class name="Order">
    ...
    <set name="lineItems" fetch="join">

List result = session.createCriteria(Order.class)
                       .list();
List result = session.createQuery("select o from Order o left join fetch o.lineItems").list();

Tutti questi esempi producono la stessa istruzione SQL:

SELECT o.*, l.* from ORDER o LEFT OUTER JOIN LINE_ITEMS l ON o.ID = l.ORDER_ID

Vuoi sapere perché ci sono i duplicati? Guarda il set di risultati SQL, Hibernate non nasconde questi duplicati sul lato sinistro del risultato del join esterno ma restituisce tutti i duplicati della tabella guida. Se hai 5 ordini nel database e ogni ordine ha 3 voci, il gruppo di risultati sarà di 15 righe. L'elenco dei risultati Java di queste query avrà 15 elementi, tutti di tipo Order. Solo 5 istanze di Order verranno create da Hibernate, ma i duplicati del set di risultati SQL vengono conservati come riferimenti duplicati a queste 5 istanze. Se non capisci quest'ultima frase, devi leggere su Java e la differenza tra un'istanza sull'heap Java e un riferimento a tale istanza.

(Perché un join esterno sinistro? Se avessi un ordine aggiuntivo senza elementi pubblicitari, il set di risultati sarebbe 16 righe con NULL che riempie il lato destro, dove i dati dell'elemento pubblicitario sono per l'altro ordine. Vuoi ordini anche se non hanno elementi pubblicitari, giusto? In caso contrario, utilizza un inner join fetch nel tuo HQL).

Hibernate non filtra questi riferimenti duplicati per impostazione predefinita. Alcune persone (non tu) lo vogliono davvero. Come puoi filtrarli?

Come questo:

Collection result = new LinkedHashSet( session.create*(...).list() );

121
Anche se comprendi la seguente spiegazione, potresti lamentarti di questo comportamento sul forum Hibernate, perché sta capovolgendo un comportamento stupido!
Tom Anderson

17
Giustissimo Tom, mi ero dimenticato dell'atteggiamento arrogante di Gavin Kings. Dice anche 'Hibernate non filtra questi riferimenti duplicati per impostazione predefinita. Alcune persone (non tu) in realtà vogliono questo "Id essere interessato quando le persone effettivamente lo fanno.
Paul Taylor

16
@ TomAnderson sì esattamente. perché qualcuno dovrebbe aver bisogno di quei duplicati? Te lo chiedo per pura curiosità, visto che non ne ho idea ... Puoi creare tu stesso duplicati, quanti ne desideri .. ;-)
Parobay

13
Sospiro. Questo è in realtà un difetto di ibernazione, IMHO. Voglio ottimizzare le mie query, quindi vado da "seleziona" a "unisciti" nel mio file di mappatura. All'improvviso il mio codice SI ROMPE dappertutto. Quindi corro e riparo tutti i miei DAO aggiungendo trasformatori di risultati e quant'altro. Esperienza utente == molto negativa. Capisco che alcune persone adorino avere duplicati per motivi bizzarri, ma perché non posso dire "prendi questi oggetti PIÙ VELOCEMENTE ma non disturbarmi con i duplicati" specificando fetch = "justworkplease"?
Roman Zenka

@Eran: sto affrontando un tipo di problema simile. Non ricevo oggetti genitore duplicati, ma ottengo i figli in ogni oggetto genitore ripetuti tante volte quante sono il numero di oggetti genitore nella risposta. Qualche idea sul perché questo problema?
mantri

93

Oltre a quanto menzionato da Eran, un altro modo per ottenere il comportamento desiderato, è impostare il trasformatore del risultato:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

8
Questo funzionerà per la maggior parte dei casi ... tranne quando si tenta di utilizzare Criteri per recuperare 2 raccolte / associazioni.
JamesD

42

provare

@Fetch (FetchMode.SELECT) 

per esempio

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch (FetchMode.SELECT)
public List<OrderTransaction> getOrderTransactions() {
return orderTransactions;

}


11
FetchMode.SELECT aumenta il numero di query SQL attivate da Hibernate ma garantisce solo un'istanza per record di entità root. In questo caso, Hibernate attiverà una selezione per ogni record figlio. Quindi dovresti tenerne conto rispetto alle considerazioni sulle prestazioni.
Bipul

1
@BipulKumar sì, ma questa è l'opzione quando non possiamo usare il recupero lento perché abbiamo bisogno di mantenere una sessione per il recupero lento per accedere agli oggetti secondari.
mathi

18

Non utilizzare List e ArrayList ma Set e HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

2
Si tratta di una menzione accidentale di una migliore pratica di Hibernate o pertinente alla domanda sul recupero multi-bambino dal PO?
Jacob Zwiers


Fatto. Secondario alla domanda dell'OP. Tuttavia, l'articolo di dzone dovrebbe probabilmente essere preso con le pinze ... in base all'ammissione dell'autore nei commenti.
Jacob Zwiers

2
Questa è un'ottima risposta IMO. Se non vuoi duplicati, è altamente probabile che preferiresti usare un Set piuttosto che un List: l'uso di un Set (e l'implementazione di metodi corretti di uguale / hascode ovviamente) ha risolto il problema per me. Attenzione quando si implementa hashcode / uguale, come indicato nel documento redhat, a non utilizzare il campo id.
Sabato

1
Grazie per il tuo IMO. Inoltre, non avere problemi a creare metodi equals () e hashCode (). Lascia che il tuo IDE o Lombok li generi per te.
Αλέκος

3

Utilizzando Java 8 e Streams aggiungo nel mio metodo di utilità questa dichiarazione di ritorno:

return results.stream().distinct().collect(Collectors.toList());

I flussi rimuovono i duplicati molto velocemente. Uso l'annotazione nella mia classe Entity in questo modo:

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "STUDENT_COURSES")
private List<Course> courses;

Penso che sia meglio nella mia app utilizzare la sessione nel metodo in cui ho bisogno di dati dal database. Closse session quando ho finito. Ovviamente ha impostato la mia classe Entity per utilizzare il tipo di recupero lento. Vado al refactoring.


3

Ho lo stesso problema per recuperare 2 raccolte associate: l'utente ha 2 ruoli (Set) e 2 pasti (Elenco) e i pasti sono duplicati.

@Table(name = "users")
public class User extends AbstractNamedEntity {

   @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
   @Column(name = "role")
   @ElementCollection(fetch = FetchType.EAGER)
   @BatchSize(size = 200)
   private Set<Role> roles;

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
   @OrderBy("dateTime DESC")
   protected List<Meal> meals;
   ...
}

DISTINCT non aiuta (query DATA-JPA):

@EntityGraph(attributePaths={"meals", "roles"})
@QueryHints({@QueryHint(name= org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")}) // remove unnecessary distinct from select
@Query("SELECT DISTINCT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

Finalmente ho trovato 2 soluzioni:

  1. Cambia elenco in LinkedHashSet
  2. Usa EntityGraph solo con il campo "meal" e digita LOAD, che carica i ruoli come hanno dichiarato (EAGER e da BatchSize = 200 per evitare problemi N + 1):

Soluzione finale:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

1

Invece di usare hack come:

  • Set invece di List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

che non modificano la tua query sql, possiamo usare (citando le specifiche JPA)

q.select(emp).distinct(true);

che modifica la query sql risultante, avendo così un file DISTINCT.


0

Non sembra un ottimo comportamento applicare un outer join e portare risultati duplicati. L'unica soluzione rimasta è filtrare il nostro risultato utilizzando i flussi. Grazie java8 che ci ha fornito un modo più semplice per filtrare.

return results.stream().distinct().collect(Collectors.toList());
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.