Start - Publikationen - Wissen - TOGAF - Impressum -

Das N+1 Problem und die Join Fetch Strategie


Wir betrachten ein einfaches Beispiel

@Entity
public class Organisation {
  @Id
  @GeneratedValue
  private long id;
  //
  private String name;
  //
  @ManyToOne(cascade=CascadeType.ALL)
  private Person person;
  //
  // es folgen Getter und Setter..
}
Die Entität Organisation besitzt also eine @ManyToOne Assoziation zur Entität Person. Im Standardfall wird die Person FetchType.EAGER geladen, das bedeutet, bei der Initialisierung einer Entität Organisation wird ein Select für die assoziierte Person durchgeführt. Man kann dieses Verhalten ändern, und auf FetchType.LAZY umstellen:
  @ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
  private Person person;
Das Select erfolgt nun erst, wenn auf Inhalte der Entität Person zugegruiffen wird. Für die Entität Organisation wird nun eine Query formuliert:
@Entity
@NamedQuery(name = "Organisation.findByName", query = "SELECT o FROM Organisation o WHERE o.name = :name")
public class Organisation {
:
}
//
// eine Organisationensuche:
Query query = em.createNamedQuery("Organisation.findByName");
query.setParameter("name", orgName);
List<Organisation> organisations = query.getResultList();
:
Angenommen diese Query hat als Ergebnis 10000 Entitäten Organisation, dann werden entweder sofort (FetchType.EAGER, Standard für @ManyToOne) oder im weiteren Verlauf der Arbeit mit den gefundenen Entitäten (FetchType.LAZY) 10000 Selects an die Datenbank verschickt und wir haben das N+1 Problem. Sollte FetchType.LAZY das N+1 Problem an dieser Stelle nachhaltig beheben, dann ist das sehr schön, allerdings ist dann fraglich, was das Feld Person in Organisation zu suchen hat.

Als eine Lösung für das N+1 Problem kommt die Join Fetch Strategie zur Anwendung. Dabei wird in einer Query ein JOIN formuliert und mit dem Schlüsselwort FETCH angewiesen, alle so geladenen Entitäten in einem Rutsch zu laden und zu initialisieren:

@Entity
@NamedQuery(name = "Organisation.findByName.joinFetch", 
  query = "SELECT o FROM Organisation o LEFT JOIN FETCH o.person WHERE o.name = :name")
public class Organisation{
:
}
Die Query resultiert in einem Select mit einem LEFT OUTER JOIN bei dem alle assoziierten Entitäten in einem Rutsch initialisiert werden. Join Fetch bedingen damit auch gleichzeitig Eager Fetch!

Bidirektionale @OneToMany Relationen


Der Entität Person fügen wir nun noch die andere Navigationsrichtung hinzu:

@Entity
public class Person {
  //
  @Id
  @GeneratedValue
  private long id;
  //
  private String name;
  //
  @OneToMany(mappedBy="person", cascade=CascadeType.ALL)
  private Collection<Organisation> organisations = new HashSet<Organisation>();
  //
  // Getter und Setter etc...
}
Bei FetchType.EAGER werden dabei alle Entitäten Organisation zusammen mit dem Select für die Entität Person geladen. Technisch geschieht auch das über ein (LEFT OUTER) JOIN. Mit FetchType.LAZY (der Standard für @OneToMany) wird ein solcher Join verhindert und die Entitäten Organisation werden geladen, sobald zum ersten Mal auf die Collection zugegriffen wird. Berücksichtigt werden muss dabei:
  • Enthält die Collection sehr viele Einträge, dann werden diese sofort (FetchType.EAGER) oder etwas später (FetchType.LAZY) gleichzeitig in den Hauptspeicher geladen. JPA sieht nicht vor, solche Collectionen sukzessive zu laden! Einige JPA Implementierungen bieten hier proprietäre Unterstützung, aber eigentlich läuft alles auf eine einfache Regel hinaus: Collections mit sehr vielen Einträgen dürfen nicht Bestandteil des OR Mappings sein!
  • Gibt es mehrere FetchType.EAGER-Collectionen, dann werden diese als kartesisches Produkt geladen. Das korrespondierende ResultSet enthält extrem viele redundante Informationen. Das Problem löst man, indem pro Entität maximal eine Collection mit FetchType.EAGER geladen wird.
Sinngemäß gilt das auch für @ManyToMany-Assoziationen und alle in der JPA erlaubten collection-wertigen Interfaces: java.util.Collection, java.util.Set, java.util.List und java.util.Map.

Bei @OneToMany-Assoziationen kommt genau wie bei @ManyToOne-Assoziationen das N+1 Problem ins Spiel, sobald Queries formuliert werden die N Treffer liefern:

@Entity
@NamedQuery(name = "Person.findByName", 
  query = "SELECT p FROM Person p WHERE p.name = :name")
public class Person {
:
}
Angenommen diese Query liefert 1000 Treffer. Entweder sofort bei der Initialisierung der Person (FetchType.EAGER) oder sobald auf die Collection zugegriffen wird (FetchType.LAZY) folgen 1000 weitere Selects, diesmal zum Laden der Collections. Auch hier bietet JPA Abhilfe mit der Join Fetch Strategie:
@Entity
@NamedQuery(name = "Person.findByName.fetchOrgs", 
  query = "SELECT DISTINCT p FROM Person p LEFT JOIN FETCH p.organisations WHERE p.name = :name")
public class Person {
:
}
Für die Arbeit mit der Join Fetch Strategie gilt hier
  • Join Fetch kann das N+1 Problem mildern. Die Angabe zum FetchType ist hier egal, ein Join Fetch ist immer ein Eager Fetch.
  • Das Schlüsselwort DISTINCT sorgt dafür, dass nur verschiedene Identitäten im Resultat berücksichtigt werden.
  • Generell sollten große Collections nicht Bestandteil eines OR-Mappings sein, die damit verbundenen konzeptionellen Probleme sind nicht lösbar.
  • Kartesische Produkte beim Join Fetch und damit eine Menge redundanter Informationen kommen ins Spiel, wenn mehrere Collections mit FetchType.EAGER gekennzeichnet sind. Deshalb sollte immer maximal eine Collection pro Entität FetchType.EAGER sein.
copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved