Hibernate Criteria gibt Kinder mit FetchType.EAGER mehrmals zurück

115

Ich habe eine OrderKlasse mit einer Liste von OrderTransactionsund habe sie mit einer Eins-zu-Viele-Zuordnung im Ruhezustand wie folgt zugeordnet:

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

Diese Orderhaben auch ein Feld orderStatus, das zum Filtern mit den folgenden Kriterien verwendet wird:

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

Dies funktioniert und das Ergebnis ist wie erwartet.

Nun , hier ist meine Frage : Warum, wenn ich die Art holen gesetzt explizit auf EAGER, tut die Orders mehrfach in der Ergebnisliste erscheinen?

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

Wie müsste ich meinen Kriteriencode ändern, um mit der neuen Einstellung das gleiche Ergebnis zu erzielen?

raoulsson
quelle
1
Haben Sie versucht, show_sql zu aktivieren, um zu sehen, was darunter vor sich geht?
Mirko N.
Bitte fügen Sie auch den Code der OrderTransaction- und Order-Klassen hinzu. \
Eran Medan

Antworten:

115

Dies ist tatsächlich das erwartete Verhalten, wenn ich Ihre Konfiguration richtig verstanden habe.

Sie erhalten dieselbe OrderInstanz in jedem der Ergebnisse, aber da Sie jetzt einen Join mit dem durchführen OrderTransaction, muss dieselbe Anzahl von Ergebnissen zurückgegeben werden, die ein regulärer SQL-Join zurückgibt

Eigentlich sollte es also mehrmals auftauchen. Dies wird vom Autor (Gavin King) selbst hier sehr gut erklärt: Es erklärt sowohl warum als auch wie man immer noch eindeutige Ergebnisse erzielt


Auch in den FAQ zum Ruhezustand erwähnt :

Der Ruhezustand gibt keine eindeutigen Ergebnisse für eine Abfrage mit aktiviertem Outer Join-Abruf für eine Sammlung zurück (selbst wenn ich das eindeutige Schlüsselwort verwende). Zunächst müssen Sie SQL verstehen und wissen, wie OUTER JOINs in SQL funktionieren. Wenn Sie äußere Verknüpfungen in SQL nicht vollständig verstehen und verstehen, lesen Sie dieses FAQ-Element nicht weiter, sondern konsultieren Sie ein SQL-Handbuch oder ein Lernprogramm. Andernfalls verstehen Sie die folgende Erklärung nicht und beschweren sich im Hibernate-Forum über dieses Verhalten.

Typische Beispiele, die möglicherweise doppelte Referenzen desselben Order-Objekts zurückgeben:

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();

Alle diese Beispiele erzeugen dieselbe SQL-Anweisung:

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

Möchten Sie wissen, warum die Duplikate vorhanden sind? Schauen Sie sich die SQL-Ergebnismenge an. Hibernate versteckt diese Duplikate nicht auf der linken Seite des äußeren verbundenen Ergebnisses, sondern gibt alle Duplikate der Treibertabelle zurück. Wenn Sie 5 Bestellungen in der Datenbank haben und jede Bestellung 3 Werbebuchungen enthält, besteht die Ergebnismenge aus 15 Zeilen. Die Java-Ergebnisliste dieser Abfragen enthält 15 Elemente vom Typ Order. Von Hibernate werden nur Instanzen mit 5 Ordnungen erstellt, Duplikate der SQL-Ergebnismenge bleiben jedoch als doppelte Verweise auf diese 5 Instanzen erhalten. Wenn Sie diesen letzten Satz nicht verstehen, müssen Sie sich über Java und den Unterschied zwischen einer Instanz auf dem Java-Heap und einem Verweis auf eine solche Instanz informieren.

(Warum eine linke äußere Verknüpfung? Wenn Sie eine zusätzliche Bestellung ohne Werbebuchungen haben würden, würde die Ergebnismenge 16 Zeilen umfassen, wobei NULL die rechte Seite ausfüllt, wobei die Positionsdaten für eine andere Bestellung gelten. Sie möchten Bestellungen, auch wenn Sie haben keine Werbebuchungen, oder? Wenn nicht, verwenden Sie einen inneren Join-Abruf in Ihrem HQL.

Der Ruhezustand filtert diese doppelten Referenzen standardmäßig nicht heraus. Einige Leute (nicht Sie) wollen das tatsächlich. Wie können Sie sie herausfiltern?

So was:

Collection result = new LinkedHashSet( session.create*(...).list() );
Eran Medan
quelle
121
Selbst wenn Sie die folgende Erklärung verstehen, können Sie sich im Hibernate-Forum über dieses Verhalten beschweren, da es dummes Verhalten umdreht!
Tom Anderson
17
Ganz richtig, Tom, ich habe Gavin Kings arrogante Haltung vergessen. Er sagt auch, dass der Ruhezustand diese doppelten Verweise nicht standardmäßig herausfiltert. Einige Leute (nicht Sie) wollen das wirklich. Ich würde mich dafür interessieren, wenn die Leute das wirklich tun.
Paul Taylor
16
@ TomAnderson ja genau. warum sollte jemand diese Duplikate brauchen? Ich frage aus purer Neugier, da ich keine Ahnung habe ... Sie können selbst Duplikate erstellen, so viele wie Sie möchten .. ;-)
Parobay
13
Seufzer. Dies ist tatsächlich ein Fehler im Ruhezustand, IMHO. Ich möchte meine Abfragen optimieren, also gehe ich in meiner Zuordnungsdatei von "Auswählen" zu "Verbinden". Plötzlich bricht mein Code überall. Dann laufe ich herum und repariere alle meine DAOs, indem ich Ergebnistransformatoren und so weiter anhänge. Benutzererfahrung == sehr negativ. Ich verstehe, dass manche Leute es aus bizarren Gründen absolut lieben, Duplikate zu haben, aber warum kann ich nicht sagen "Holen Sie sich diese Objekte SCHNELLER, aber nerven Sie mich nicht mit Duplikaten", indem Sie fetch = "justworkplease" angeben?
Roman Zenka
@Eran: Ich stehe vor einem ähnlichen Problem. Ich erhalte keine doppelten übergeordneten Objekte, aber in jedem übergeordneten Objekt werden untergeordnete Objekte so oft wiederholt, wie die Antwort die Anzahl der übergeordneten Objekte enthält. Irgendeine Idee warum dieses Problem?
Mantri
93

Zusätzlich zu dem, was Eran erwähnt, besteht eine andere Möglichkeit, das gewünschte Verhalten zu erzielen, darin, den Ergebnis-Transformator einzustellen:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
quelle
8
Dies funktioniert in den meisten Fällen ... außer wenn Sie versuchen, Kriterien zum Abrufen von 2 Sammlungen / Assoziationen zu verwenden.
JamesD
42

Versuchen

@Fetch (FetchMode.SELECT) 

beispielsweise

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

}}

Mathi
quelle
11
FetchMode.SELECT erhöht die Anzahl der von Hibernate ausgelösten SQL-Abfragen, stellt jedoch nur eine Instanz pro Stammentitätsdatensatz sicher. Im Ruhezustand wird in diesem Fall für jeden untergeordneten Datensatz eine Auswahl ausgelöst. Sie sollten dies also in Bezug auf die Leistungsaspekte berücksichtigen.
Bipul
1
@BipulKumar ja, aber dies ist die Option, wenn wir Lazy Fetch nicht verwenden können, da wir eine Sitzung für Lazy Fetch für den Zugriff auf Unterobjekte verwalten müssen.
Mathi
18

Verwenden Sie nicht List und ArrayList, sondern Set und HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
quelle
2
Ist dies eine zufällige Erwähnung einer Best Practice für den Ruhezustand oder relevant für die Frage zum Abrufen mehrerer Kinder aus dem OP?
Jacob Zwiers
Verstanden. Sekundär zur Frage des OP. Allerdings sollte der dzone-Artikel wahrscheinlich mit einem Körnchen Salz aufgenommen werden ... basierend auf dem eigenen Eingeständnis des Autors in Kommentaren.
Jacob Zwiers
2
Dies ist eine sehr gute Antwort IMO. Wenn Sie keine Duplikate möchten, ist es sehr wahrscheinlich, dass Sie lieber ein Set als eine Liste verwenden. Die Verwendung eines Sets (und natürlich die Implementierung korrekter Gleichheits- / Hascode-Methoden) hat das Problem für mich gelöst. Achten Sie bei der Implementierung von Hashcode / Equals, wie im Redhat-Dokument angegeben, darauf, das ID-Feld nicht zu verwenden.
Mat
1
Danke für deine IMO. Darüber hinaus sollten Sie keine Probleme beim Erstellen der Methoden equals () und hashCode () haben. Lassen Sie sie von Ihrer IDE oder Lombok für Sie generieren.
Αλέκος
3

Mit Java 8 und Streams füge ich meiner Dienstprogrammmethode diese Rückgabeanweisung hinzu:

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

Streams entfernen Duplikate sehr schnell. Ich verwende Anmerkungen in meiner Entitätsklasse wie folgt:

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

Ich denke, es ist besser in meiner App, eine Sitzung in einer Methode zu verwenden, bei der ich Daten aus der Datenbank benötige. Abschlusssitzung, wenn ich fertig bin. Natürlich habe ich meine Entity-Klasse so eingestellt, dass sie den Leasy-Fetch-Typ verwendet. Ich gehe zum Refactor.

easyScript
quelle
3

Ich habe das gleiche Problem beim Abrufen von 2 zugeordneten Sammlungen: Benutzer hat 2 Rollen (Set) und 2 Mahlzeiten (Liste) und Mahlzeiten werden dupliziert.

@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 hilft nicht (DATA-JPA-Abfrage):

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

Endlich habe ich 2 Lösungen gefunden:

  1. Ändern Sie die Liste in LinkedHashSet
  2. Verwenden Sie EntityGraph nur mit dem Feld "Mahlzeit" und geben Sie LOAD ein, wodurch die deklarierten Rollen geladen werden (EAGER und BatchSize = 200, um ein N + 1-Problem zu vermeiden):

Endgültige Lösung:

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

Anstatt Hacks wie:

  • Set anstatt List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

die Ihre SQL-Abfrage nicht ändern, können wir verwenden (unter Angabe von JPA-Spezifikationen)

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

Dadurch wird die resultierende SQL-Abfrage geändert, sodass ein DISTINCTdarin enthalten ist.

Kaustubh
quelle
0

Es klingt nicht nach einem großartigen Verhalten, einen äußeren Join anzuwenden und doppelte Ergebnisse zu erzielen. Die einzige verbleibende Lösung besteht darin, unser Ergebnis mithilfe von Streams zu filtern. Dank Java8 gibt es eine einfachere Möglichkeit zum Filtern.

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