Ich verstehe, dass das Problem N + 1 darin liegt, dass eine Abfrage ausgeführt wird, um N Datensätze abzurufen, und N Abfragen, um relationale Datensätze abzurufen.
Aber wie kann man es in Hibernate vermeiden?
Nehmen wir an, wir haben einen Klassenhersteller, der mit Contact eine Viel-zu-Eins-Beziehung hat.
Wir lösen dieses Problem, indem wir sicherstellen, dass die ursprüngliche Abfrage alle Daten abruft, die zum Laden der benötigten Objekte in ihrem entsprechend initialisierten Zustand erforderlich sind. Eine Möglichkeit hierzu ist die Verwendung eines HQL-Abrufjobs. Wir verwenden die HQL
"from Manufacturer manufacturer join fetch manufacturer.contact contact"
mit der Abruf-Anweisung. Dies führt zu einem inneren Join:
select MANUFACTURER.id from manufacturer and contact ... from
MANUFACTURER inner join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id
Mit einer Kriterienabfrage können wir dasselbe Ergebnis erhalten
Criteria criteria = session.createCriteria(Manufacturer.class);
criteria.setFetchMode("contact", FetchMode.EAGER);
was erstellt die SQL:
select MANUFACTURER.id from MANUFACTURER left outer join CONTACT on
MANUFACTURER.CONTACT_ID=CONTACT.id where 1=1
in beiden Fällen gibt unsere Abfrage eine Liste von Herstellerobjekten zurück, deren Kontakt initialisiert wurde. Es muss nur eine Abfrage ausgeführt werden, um alle erforderlichen Kontakt- und Herstellerinformationen zurückzugeben
für weitere Informationen hier einen Link zum Problem und der Lösung
Das Problem mit der N + 1-Abfrage tritt auf, wenn Sie vergessen, eine Assoziation abzurufen, und Sie müssen darauf zugreifen.
Nehmen wir zum Beispiel an, wir haben die folgende JPA-Abfrage:
List<PostComment> comments = entityManager.createQuery(
"select pc " +
"from PostComment pc " +
"where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();
Wenn wir nun die PostComment
-Entitäten iterieren und die post
-Verknüpfung durchqueren:
for(PostComment comment : comments) {
LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}
Der Ruhezustand generiert die folgenden SQL-Anweisungen:
SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM post_comment pc
WHERE pc.review = 'Excellent!'
INFO - Loaded 3 comments
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 1
INFO - The post title is 'Post nr. 1'
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 2
INFO - The post title is 'Post nr. 2'
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 3
INFO - The post title is 'Post nr. 3'
So wird das N + 1-Abfrageproblem generiert.
Da die post
-Verknüpfung beim Abrufen der PostComment
-Entitäten nicht initialisiert wird, muss Hibernate die Post
-Entität mit einer sekundären Abfrage abrufen. Für N PostComment
-Entitäten werden N weitere Abfragen ausgeführt (daher das N + 1-Abfrageproblem).
Das erste, was Sie tun müssen, um dieses Problem zu lösen, ist das Hinzufügen von ordnungsgemäßer SQL-Protokollierung und -Überwachung . Ohne Protokollierung werden Sie das Problem der N + 1-Abfrage während der Entwicklung einer bestimmten Funktion nicht bemerken.
Zweitens, um das Problem zu beheben, können Sie einfach JOIN FETCH die Beziehung, die dieses Problem verursacht:
List<PostComment> comments = entityManager.createQuery(
"select pc " +
"from PostComment pc " +
"join fetch pc.post p " +
"where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();
Wenn Sie mehrere untergeordnete Zuordnungen abrufen müssen, ist es besser, eine Auflistung in der ersten Abfrage und die zweite mit einer sekundären SQL-Abfrage abzurufen.
Dieses Problem sollte besser von Integrationstests erfasst werden. Sie können eine automatische JUnit-Bestätigung verwenden, um die erwartete Anzahl der generierten SQL-Anweisungen zu überprüfen . Das db-util-Projekt bietet bereits diese Funktionalität und ist Open Source und die Abhängigkeit von Maven Central.
Die native Lösung für 1 + N in Hibernate heißt:
Bei Verwendung des Batch-Abrufs kann Hibernate mehrere nicht initialisierte Proxys laden, wenn auf einen Proxy zugegriffen wird. Batch-Abruf ist eine Optimierung der Abrufstrategie für Lazy Select. Es gibt zwei Möglichkeiten, den Stapelabruf zu konfigurieren: auf der 1) Klassenebene und auf der 2) Sammelebene ...
Überprüfen Sie diese Fragen und Antworten:
Mit Anmerkungen können wir es so machen:
Eine class
-Ebene:
@Entity
@BatchSize(size=25)
@Table(...
public class MyEntity implements Java.io.Serializable {...
Eine collection
-Ebene:
@OneToMany(fetch = FetchType.LAZY...)
@BatchSize(size=25)
public Set<MyEntity> getMyColl()
Lazy Loading und Batch-Abruf zusammen stellen eine Optimierung dar, die:
Sie können es sogar zum Laufen bringen, ohne die @BatchSize
-Annotation überall hinzufügen zu müssen. Setzen Sie einfach die Eigenschaft hibernate.default_batch_fetch_size
auf den gewünschten Wert, um den Batch-Abruf global zu aktivieren. Weitere Informationen finden Sie in den Hibernate-Dokumenten .
Wenn Sie dabei sind, möchten Sie wahrscheinlich auch BatchFetchStyle ändern, da der Standardwert (LEGACY
) höchstwahrscheinlich nicht Ihren Vorstellungen entspricht. Eine vollständige Konfiguration für das globale Aktivieren des Batch-Abrufs würde also folgendermaßen aussehen:
hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25
Ich bin auch überrascht, dass eine der vorgeschlagenen Lösungen das Join-Fetching beinhaltet. Join-Fetching ist selten wünschenswert, da mit jeder Ergebniszeile mehr Daten übertragen werden, auch wenn die abhängige Entität bereits in den L1- oder L2-Cache geladen wurde. Daher würde ich empfehlen, es durch die Einstellung komplett zu deaktivieren
hibernate.max_fetch_depth=0