2020-04-24

Hibernate - FetchType (Lazy and Eager)

FetchType (Lazy and Eager) – Hibernate
Package of FetchType

javax.persistence.FetchType
Fetch type is applied in the following format

@OneToMany(fetch=FetchType.LAZY)
Other JPA annotations which can be provided the ‘fetch’ attribute

@Basic, @ElementCollection, @ManyToMany, @OneToMany, @ManyToOne, @OneToOne


Hibernate have two orthogonal notions here: when is the association fetched and how is it fetched.

There are many words on different document to describe ORM fetch like fetch strategy, fetch type, fetch mode, but in essence just the concept of WHEN and HOW.

WHEN to fetch
Lazy – Hibernate won’t load the relationships for that particular object instance unless explicitly “asked for” via getter. JPA compatible
Eager –  Eager will by default load ALL of the relationships related to a particular object loaded by Hibernate.  JPA compatible. Default behavior.
Extra Lazy  –  Try not to fetch the whole collection into memory unless absolutely needed. Not JPA compatible
When means at what time will hibernate assemble a SQL clause to access the database.

How to fetch
If a entity have associations, how the associations of the entity get loaded. In our Client-To-PurchaseOrder(One-To-Many) demo, when a client entity is loaded, how to get his purchase orders.

Join  – Use SQL outer join to get associations. Be careful about the return list, it may not like what you expect,  see the demo below.   JPA compatible. Default behavior for Eager.
Select  – Use a separate select to get only 1 entity’s association,  will cause the ‘N+1’ problem.   JPA compatible. Default behavior FOR Lazy.
Batch  –  Use a separate select to get a batch size entities’ association. Can be treated as a kind of improvement of ‘Select’ Not JPA compatible
Subselect  –  Use a separate select to get all associations for all entities retrieved in a previous query. Not JPA compatible
JPA compatible means in java code, only classes or annotations from javax.persistence.* are used, no need to import any thing  from  org.hibernate.*.  Hibernate can just be used as persistence provider in JPA configure file persistence.xml, which makes the the application not close coupled with Hibernate.



The Entity definitions
PurchaseOrder.java

package com.shengwang.demo.model;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;


@Entity
@Table(name="purchase_order")
public class PurchaseOrder {
  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  @Column(name="order_id")
  private int orderId;
 
  @Column(name="order_desc")
  private String orderDesc;
 
  @ManyToOne(cascade=CascadeType.PERSIST)
  @JoinColumn(name="client_id")
  private Client client;

  public int getOrderId() {
    return orderId;
  }

  public String getOrderDesc() {
    return orderDesc;
  }


  public Client getClient() {
    return client;
  }

  public void setOrderId(int orderId) {
    this.orderId = orderId;
  }

  public void setOrderDesc(String orderDesc) {
    this.orderDesc = orderDesc;
  }

  public void setClient(Client client) {
    this.client = client;
  }
}
All magic happens in the Entity Client. which has a Set variable orders associated with PurcharseOrder to represent the one-to-many mapping. We will change the annoatation for Set variable orders to get different fetch when-how combinations.

Client.java

package com.shengwang.demo.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="client")
public class Client {
  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  @Column(name="client_id")
  private int clientId;
 
  @Column(name="client_name")
  private String clientName;

  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.LAZY,mappedBy="client")
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 

  public int getClientId() {
    return clientId;
  }
  public String getClientName() {
    return clientName;
  }
 
  public Set<PurchaseOrder> getPurchaseOrders() {
    return orders;
  }
  public void setClientId(int clientId) {
    this.clientId = clientId;
  }
  public void setClientName(String clientName) {
    this.clientName = clientName;
  }
  public void setPurchaseOrders(Set<PurchaseOrder> orders) {
    this.orders = orders;
  }
}


Demo Code Snippet

Following code snippet is used to show how the different fetch combinations effect. We’ll keep running this code on different when-how fetch combinations to demonstrate differents effects

@Override
@Transactional
public void printOrdersForEveryClient() {
  List <Client> clients;
  Criteria criteria;
  criteria = sessionFactory.getCurrentSession().createCriteria(Client.class);
  clients = criteria.list();                                   // checkpoint-1
  System.out.println("clients.size="+clients.size());   
 
  for (Client client: clients) {
    System.out.println("client : "+ client.getClientId()); 
    System.out.println(""+client.getPurchaseOrders().size());  // checkpoint-2
    Set <PurchaseOrder> orders = client.getPurchaseOrders();   // checkpoint-3
    for (PurchaseOrder order : orders) {                     
      System.out.println(order.getOrderId()+","+order.getOrderDesc());
    }
  }
}
The method printOrdersForEveryClient() is supposed to print out purchase orders for all clients one by one. The comments checkpoint1-4 is added as anchors to locate where the database accessing happens.



Behaviors of When+How fetch combinations
Let’s try different When-How fetch combinations on the one-to-many(client-to-purchase order) mapping,  see what really happens. All the following SQL clauses are NOT the original Hibernate generated ones, but simplified to make them more understandable.

Eager + Join (Default Behavior)

Changing the definition of Client entity, set when to eager by “fetch=FetchType.EAGER“, set how to join by@Fetch(FetchMode.JOIN). Since join is the default behavior for eager, annotation  @Fetch(FetchMode.JOIN)can be omitted.

@Entity
@Table(name="client")
public class Client {
  // omit other fields
 
  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.EAGER,mappedBy="client",cascade=CascadeType.PERSIST)
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 
  // omit getters & setters
}
The database accessing happens only in 1 place:
select a.*,  b.* from client a left outer join purchase_order b on a.client_id=b.client_id  at checkpoint-1

Suppose there are 5 rows in client table and every client have 10 purchase orders in purchase_order table. The result list size at checkpoint-1 is not 5, but 5×10 = 50.



Lazy + Select (default behavior)
The default when is lazy, and the default how for lazy is select.

The database accessing happens in  2 places:
select *  from client   at checkpoint-1
select * from purchase_order  where client_id=?     at checkpoint-2

Since the checkpoint-2 is in the for loop, every loop will issue a sql query to database, perfectly demonstrate the so-called ‘N+1’ problems.
You may also notice that although lazy fetch is used, the database accessing immediately happens when querying the clients.  That’s because the lazy fetch only works for query entity by primary key.



Eager+ Select
Changing the definition of Client entity, set when to eager by “fetch=FetchType.EAGER“, set how to join by@Fetch(FetchMode.SELECT)

@Entity
@Table(name="client")
public class Client {
  // omit other fields
 
  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.EAGER,mappedBy="client",cascade=CascadeType.PERSIST)
  @Fetch(FetchMode.SELECT)
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 
  // omit getters & setters
}
The database accessing happens only in 1 place, but N+1 SQL queries:

select * from client                                                                             at checkpoint-1
select * from purchase_order where client_id=?                at checkpoint-1
select * from purchase_order where client_id=?                at checkpoint-1
select * from purchase_order where client_id=?                at checkpoint-1
select * from purchase_order where client_id=?                at checkpoint-1
select * from purchase_order where client_id=?                at checkpoint-1

The query in java also creates N+1 SQL clauses, but all at same time. One to get all clients and 5 to get orders for each client.  (There are 5 clients in the client table)



Lazy + Batch
Changing the definition of Client entity, set when to lazy by “fetch=FetchType.LAZY“, set how to batch by@Fetch(FetchMode.SELECT) and @BatchSize(size=3) . Since select is the default behavior for lazy, annotation  @Fetch(FetchMode.SELECT) can be omitted.

@Entity
@Table(name="client")
public class Client {
  // omit other fields
 
  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.LAZY,mappedBy="client",cascade=CascadeType.PERSIST)
  @BatchSize(size=3)
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 
  // omit getters & setters
}
The database accessing happens in 2 places:
select * from client                                                                             at checkpoint-1
select * from purchase_order where client_id in (?, ?, ?)             at checkpoint-2             

Unlike the select, which accesses database N+1 times,  batch fetch accesses database (N/batchSize +1 ) times. In our demo, there are 5 clients in the table,  batch size is 3, total database accessing counts = celling(5/3)+1=3.



Eager + Batch
Changing the definition of Client entity, set when to eager by “fetch=FetchType.EAGER“, set how to batch by@Fetch(FetchMode.SELECT) and @BatchSize(size=3)

@Entity
@Table(name="client")
public class Client {
  // omit other fields
 
  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.LAZY,mappedBy="client",cascade=CascadeType.PERSIST)
  @Fetch(FetchMode.SELECT)
  @BatchSize(size=3)
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 
  // omit getters & setters
}
The database accessing happens in 1 place:
select * from client                                                                             at checkpoint-1
select * from purchase_order where client_id in (?, ?, ?)             at checkpoint-1             
Compare to Lazy+Batch, Eager+Batch  create same amount SQL and exactly same SQL queries. but will no wait for the first time accessing contents of client entity, but immediately load all clients’ associations when query client entity. Henceforth batch fetch accesses database (N/batchSize +1 ) times. In our demo, there are 5 clients in the table,  batch size is 3, total database accessing counts = celling(5/3)+1=3.



Lazy + Subselect
Changing the definition of Client entity, set when to lazy by “fetch=FetchType.LAZY“, set how to subselect by@Fetch(FetchMode.SUBSELECT)

@Entity
@Table(name="client")
public class Client {
  // omit other fields
 
  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.LAZY,mappedBy="client",cascade=CascadeType.PERSIST)
  @Fetch(FetchMode.SUBSELECT)
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 
  // omit getters & setters
}


The database accessing happens in 2 places:
select * from client                                                                             at checkpoint-1
select a.* from purchase_order a where a.client_id in (select b.client_id from client b)        at checkpoint-2

Unlike the select(N+1) or batch (N/batchsize+1), subselect only access database twice. First SQL query is fired to get all the clients. The first time in the loop cause the second SQL to get fired and rest of the loop won’t access database anymore.



Eager+ Subselect
Changing the definition of Client entity, set when to eager by “fetch=FetchType.EAGER“, set how tosubselect by @Fetch(FetchMode.SUBSELECT)

@Entity
@Table(name="client")
public class Client {
  // omit other fields
 
  //---------------------------------
  // This is where the magic happens
  //---------------------------------
  @OneToMany(fetch=FetchType.EAGER,mappedBy="client",cascade=CascadeType.PERSIST)
  @Fetch(FetchMode.SUBSELECT)
  private Set<PurchaseOrder> orders = new HashSet<PurchaseOrder>();
 
  // omit getters & setters
}
The database accessing happens in 1 place:
select * from client                                                                             at checkpoint-1
select a.* from purchase_order a where a.client_id in (select b.client_id from client b)        at checkpoint-1

Compare to Lazy+Subselect, Eager+Subselect  create same amount SQL and exactly same SQL queries. but will no wait for the first time accessing contents of client entity, but immediately load all clients’ associations when query client entity. Henceforth unlike the select(N+1) or batch (N/batchsize+1), subselect only access database twice. First SQL query is fired to get all the clients. The first time in the loop cause the second SQL to get fired and rest of the loop won’t access database anymore.



Default Fetch Strategy for One-To-One, One-To-Many and Many-To-Many
There is nothing about changing global fetching strategy in JPA specification. JPA offers for 1-1/N-1 associations EAGER fetching, whereas for 1-N/M-N it is LAZY. All JPA implementations should follow the specification to be compliant. I think, it is better that the application developer can’t change this default beheavior globally, since these are the best practices in almost most cases unless you’ve just one type of association between all entities, like 1-1. think about, that you could set it to “EAGER” in an application, which contains a really rich entity model with complex relations and millions of data in the database. Overriding the fetching strategy per association manually allows the developer to take the responsibility about what will next happen. it is not error prone, instead, a powerful feature.

A Very Useful Link to Understand Default Fetch Values
http://stackoverflow.com/questions/10127831/jpa-default-fetch-type


Lazy Options and Fetching Modes in Hibernate and JPA – IMPORTANT
JPA comes with the fetch option to define lazy loading and fetching modes, however Hibernate has a much more option set in this area. To fine tune the lazy loading and fetching strategies, some additional annotations have been introduced:

@LazyToOne: defines the lazyness option on @ManyToOne and @OneToOne associations. LazyToOneOption can be PROXY (i.e. use a proxy based lazy loading), NO_PROXY (use a bytecode enhancement based lazy loading – note that build time bytecode processing is necessary) and FALSE (association not lazy)

@LazyCollection: defines the lazyness option on @ManyToMany and @OneToMany associations. LazyCollectionOption can be TRUE (the collection is lazy and will be loaded when its state is accessed),EXTRA (the collection is lazy and all operations will try to avoid the collection loading, this is especially useful for huge collections when loading all the elements is not necessary) and FALSE (association not lazy)

@Fetch: defines the fetching strategy used to load the association. FetchMode can be SELECT (a select is triggered when the association needs to be loaded), SUBSELECT (only available for collections, use a subselect strategy – please refers to the Hibernate Reference Documentation for more information) or JOIN(use a SQL JOIN to load the association while loading the owner entity). JOIN overrides any lazy attribute (an association loaded through a JOIN strategy cannot be lazy).

Table – Lazy and fetch options equivalent

Annotations
(javax.persistence) Lazy
(org.hibernate.annotations) Fetch
(org.hibernate.annotations)
@[One|Many]ToOne](fetch=FetchType.LAZY) @LazyToOne(LazyToOneOption.PROXY) @Fetch(SELECT)
@[One|Many]ToOne](fetch=FetchType.EAGER) @LazyToOne(LazyToOneOption.FALSE) @Fetch(JOIN)
@ManyTo[One|Many](fetch=FetchType.LAZY) @LazyCollection(LazyCollectionOption.TRUE) @Fetch(SELECT)
@ManyTo[One|Many](fetch=FetchType.EAGER) @LazyCollection(LazyCollectionOption.FALSE) @Fetch(JOIN)

No comments:

Post a Comment