webentwicklung-frage-antwort-db.com.de

Was ist die Lösung für das N + 1-Problem in JPA und Hibernate?

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?

22
Vipul Agarwal

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

26
karim mohsen

Das Problem

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).

Die Reparatur

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. 

13
Vlad Mihalcea

Die native Lösung für 1 + N in Hibernate heißt:

20.1.5. Batch-Abruf verwenden

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:

  • erfordert nicht ein explizites Abrufen in unseren Abfragen
  • wird auf eine beliebige Anzahl von Referenzen angewendet, die nach dem Laden der Stammentität (faul) berührt werden (während explizites Abrufen nur die in Abfrage genannten Namen bewirkt)
  • löst Problem 1 + N mit Collections _ ​​(da nur eine Collection mit der Root-Abfrage abgerufen werden konnte) ohne weitere Verarbeitung erforderlich. DISTINCT-Stammwerte (prüfen: Kriterien.DISTINCT_ROOT_ENTITY vs Projections.distinct )
12
Radim Köhler

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
0
philippn