Kürzlich bin ich auf ein ärgerliches Problem mit Hibernate gestoßen. Ich habe versucht, ein Abfrageergebnis zu paginieren, das unter der Haube einen SQL-JOIN durchführte. Die Abfrage vor der Paginierung lieferte etwa 100 Ergebnisse. Als ich die Paginierung einschaltete (mit 20 Ergebnissen pro Seite), hatten alle Seiten weniger als 20 Ergebnisse!
Lassen Sie mich dies anhand einiger Beispiele erläutern. Unser heutiges Datenmodell besteht aus Türen und Farben. Eine Tür kann mehrere Farben haben. Der Datensatz, mit dem wir beginnen: Tür A: -id: 1 -Größe: groß -Farben: rot und grün Tür B: -id: 2 -Größe: klein -Farbe: blau und grün Jetzt wählen wir alle roten, grünen oder blauen Türen aus, das sind dann zwei Türen, richtig?
Criteria crit = session.createCriteria(Door.class);
crit.createAlias("Farben", "c");
crit.add(Restrictions.in("c.name", new Object[]{ "red" , "green" , "blue" }));
Wegen des Farben-Alias wird ein INNER JOIN verwendet und das Ergebnis ist möglicherweise nicht das, was Sie erwarten.
Die Größe der Liste beträgt 4, es werden 4 Door-Objekte zurückgegeben! Das ist sehr seltsam. Wenn Sie sich die SQL-Datei ansehen, sehen Sie die folgende Abfrage:
select this_.id as id21, this_.size as size21, colors3_.DOORID as DOOR1, c1.id as COLOR2, c1_.id as id30, c1_.name as name30
from doors this_ inner join DOORSCOLORS colors3 on this.id=colors3.DOORID inner join colors c1 on colors3_.COLORID=c1.id
where c1_.name in (?, ?, ?)
Übersetzt in lesbares SQL:
SELECT * FROM doors d
INNER JOIN doors_colors dc on d.id=dc.door_id
INNER JOIN colors c ON c.id=dc.color_id
WHERE c.name in (?, ?, ?)
Diese Abfrage liefert tatsächlich 4 Ergebnisse, aber wir wählen Door-Objekte aus, keine SQL-Zeilen. Wenn ich mir den Java-Code ansehe, erwarte ich ein Door-Objekt und keine Abfrageergebniszeilen, die in ein Door-Objekt verpackt sind.
Um dieses Problem zu lösen, können Sie dem Crit-Objekt einen ResultTransformer hinzufügen:
crit.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
Jetzt sind nur noch zwei Türen ausgewählt, Problem gelöst. Nun, nicht so schnell, schauen wir mal, was mit vielen Türen passiert. Unsere Datenbank hat jetzt 16 Türen. Wenn wir wieder die roten, grünen und blauen Türen auswählen, werden 11 Ergebnisse angezeigt. Nehmen wir an, wir möchten die Ergebnisse umblättern, weil es einfach zu viele Türen sind, die wir auf einmal verarbeiten können. 5 Türen pro Seite sind genug. Wir können dies erreichen, indem wir die folgenden zwei Zeilen hinzufügen:
crit.setFirstResult(0); crit.setMaxResults(5);
11 Türen gefunden, 5 Ergebnisse pro Seite und Anzeige der Ergebnisse ab dem ersten Ergebnis (das 0-indiziert ist). Wie viele Ergebnisse erwarten Sie auf der ersten Seite? Wahrscheinlich nicht die 3 zurückgegebenen Ergebnisse. Der ResultTransformer ist übrigens immer noch aktiv. Was passiert, ist, dass das Paging angewendet wird, bevor die doppelten Entitäten herausgefiltert werden. Wenn Sie anfangen, in SQL zu denken, kommen Sie vielleicht auf die Idee, eine Subquery zu verwenden. Genau das werden wir in Hibernate tun. Eine Unterabfrage in Pseudocode: select * from doors d where d.id in (select id from doors, colors where colors.name in ('red','green','blue')) Wie Sie sehen, wählt die Unterabfrage nur die Spalte id aus und in meinem vorherigen Beispiel wurde ein komplettes Door-Objekt ausgewählt. Um andere Spalten als die Spalten im Entity-Objekt auszuwählen, können Sie Projections verwenden. Bei mehreren Spalten verwenden Sie das ProjectionList Objekt. Für den Anfang brauchen wir eine id-Projektion, die eine eigene Methode hat:
crit.setProjection(Projections.id());
Jetzt wird eine Liste mit int's ausgewählt. Dieses Ergebnis ist die Eingabe für die nächste Abfrage. Sie können Criteria-Objekte ohne eine Hibernate-Sitzung zur späteren Verwendung erstellen. Diese Objekte werden DetachedCriteria genannt. Wenn wir crit in dc umbenennen und als DetachedCriteria erstellen, erhalten Sie die folgenden Codezeilen:
DetachedCriteria dc = DetachedCriteria.forClass(Door.class);
dc.createAlias("colors" , "c");
dc.add(Restrictions.in( "c.name" , new Object[]{ "red" , "green" , "blue" }));
dc.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
dc.setProjection(Projections.id());
Der nächste Schritt ist die Erstellung einer äußeren Abfrage um die vorherige Abfrage herum.
Criteria outer = session.createCriteria(Door.class);
outer.add(Subqueries.propertyIn("id", dc));
outer.setFirstResult(0);
outer.setMaxResults(5);
Mit Subqueries.propertyIn können Sie die vorherige Abfrage hinzufügen. Wenn Sie dem neuen äußeren Abfrageobjekt Paging und Sortierung hinzufügen, sind alle Ihre Probleme gelöst. Sie können sogar den ResultTransformer loswerden und erhalten das gleiche Ergebnis. Wenn Sie die Leistung optimieren, können Sie testen, ob es einen Unterschied macht, DISTINCT durchzuführen, bevor Sie die Ergebnisse an die äußere Abfrage übergeben. Wie Sie DISTINCT-Probleme lösen können, wird in den Hibernate FAQ ausführlich erklärt: Hibernate liefert keine eindeutigen Ergebnisse für eine Abfrage mit aktiviertem Outer Join Fetching für eine Sammlung (selbst wenn ich das Schlüsselwort distinct verwende)? Die Lektion, die ich mit Hibernate gelernt habe, ist, dass man SQL immer noch kennen muss und den Ergebnissen nicht blind vertrauen darf. Wenn Sie nicht wissen, warum Sie DISTINCT verwenden müssen und wie ein JOIN seine Ergebnisse präsentiert, werden Sie sich sehr bald verirren. Mit dem DetachedCriteria-Objekt können Sie Ihre Abfragen auf eine sehr lesbare Weise aufteilen und bei der Verwendung von Projektionen wählt das SQL unter der Haube nur die benötigten Spalten aus, was die Leistung verbessert.
Quellen
Verfasst von
Jeroen van Wilgenburg
Unsere Ideen
Weitere Blogs
Contact



