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:
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