Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › JPA

Lazy fetching

When is an associated object read from the database?

An order (Order) points to the member (Member) who placed it. In code, the member field of Order points to that member. In the database, the order row in the orders table holds only the number of the member (member_id). So when you read one order with em.find, JPA has to decide: should it read the member that the order points to at the same time, or should it read the member at the moment the code actually uses its values? You write this in @ManyToOne(fetch = ...) above the member field. FetchType.EAGER is the way that reads the member together with the order when the order is read (eager fetching), and FetchType.LAZY is the way that puts it off until the member is used (lazy fetching). While it is put off, the member field holds a proxy, an empty stand-in object that knows only the number of member 1. At the moment the code takes a value of the member, such as the name, JPA sends a SELECT statement that reads the member table and fills the proxy. The code on the screen has no SQL at all, yet SQL is sent, and when it is sent depends on the setting. In this lab you change the fetch setting and delete the order.getMember().getName() line, and read the same order each time, and you see which table each line sent a SELECT for.

Checked against real execution · Ran 4 combinations on Hibernate 7.4.5 and H2 2.4.240 and checked the SQL sent on each line against the results, and checked the FetchType rules against the Jakarta Persistence 3.2 API documentation

Concepts in this lab

Lazy fetching and eager fetching
Settings for when to load an associated entity. Eager fetching (EAGER) loads the associated entities together when the entity is loaded. Lazy fetching (LAZY) puts a proxy object in the place of the associated entity and loads it when its values are actually used. Eager fetching always loads data even when it is not needed and produces queries that are hard to predict, so using lazy fetching as the default is recommended.
@ManyToOne
@ManyToOne is an annotation that you put on a field to show that "this field points to one other entity, and many of this entity can point to the same one". A typical case is many orders pointing to one member. The type of the target is found from the type of the field. In tables, the table of the entity that has this field gets a foreign key column that holds the primary key value of the other side, and if you do not set the column name separately, it becomes the name that joins the field name, an underscore, and the name of the other side's primary key column (member_id for a member field of the type Member). If a field of another entity type has no such association mark, Hibernate 7.4.5 cannot find a way to put that field in a column and throws an error while starting.
Entity proxy
A stand-in object that is put in the place of a real entity. Hibernate creates a subclass of the entity class at run time and returns an object of it, and at first it knows only the identifier and the rest of its contents is empty. The moment you first read a value other than the identifier, it queries the DB and reads the real entity; this is called proxy initialization. The result of em.getReference and an association configured for lazy fetching come in as proxies. The class of a proxy is different from the entity class, so use instanceof instead of getClass() when you compare types.
FetchType
FetchType is an enum that says when to read associated data, and it has two values, EAGER and LAZY. You write FetchType.LAZY or FetchType.EAGER in the fetch element of an association annotation such as @ManyToOne. EAGER is a requirement that the data must be read at once, and LAZY is a hint that the data may be read when it is first accessed. Because LAZY is a hint, the implementation is also permitted to read data marked LAZY in advance. If you do not write fetch on @ManyToOne, the default is EAGER.

Try these

Running with the default settings

  1. The database setup: before the code runs, the orders table in the database holds one order row (id 10, member_id 1), and the member table holds one member row (id 1, Alice). member_id is the column that holds the number of the member who placed this order. These rows were put in beforehand, outside the code on the screen. Order and Member are Java classes (entities) that each correspond to a table (one object of the class is one row), and em in the code is the EntityManager that reads entities from the database. Nothing has been read yet.
  2. Order order = em.find(Order.class, 10L): em looks for the order whose id is 10. JPA (Hibernate) builds and sends 1 SELECT statement (SQL that reads rows from the database) that reads the orders table, and builds an Order object from the row it read. This row holds only the member number, member_id = 1. LAZY is written above the member field, so the member table is not read, and the member field gets a proxy that knows only the number of member 1. The name of the proxy is still empty. This SELECT is not in the code we wrote. JPA built and sent it.
  3. order.getMember(): returns as it is the proxy that was in the member field of the Order object. Up to this line, no SQL has been sent for the member table, and the name of the proxy that was returned is still empty.
  4. .getName(): the proxy holds only the number of the member, so it does not know the name. At this moment, JPA (Hibernate) sends a SELECT statement that reads the member table, reads the row of member 1, fills in the proxy, and returns the name Alice. The SELECT was sent on this line, where a value of the member was taken for the first time, and this SELECT is not in the code we wrote either.
  5. The code has finished. SELECT statements sent in total: 2. 1 was sent on the find line that reads the order, and 1 on the getName line that takes the name of the member. The member was read on the getName line, where the name was actually used.

Result of this run

Code

@Entity
public class Member {
    @Id
    private Long id;
    private String name;

    public String getName() { return name; }
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private Long id;

    // Link: decides when to read the member that this order points to
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    public Member getMember() { return member; }
}

// The database holds order 10 and member 1 (Alice), who placed that order
Order order = em.find(Order.class, 10L);
// Use: actually use a value (the name) of the member
String name = order.getMember().getName();

Assumptions of this model

This was checked against real execution on Hibernate 7.4.5 and H2 2.4.240 (in-memory database). A SessionFactory was built directly without Spring, on JDK 21. All 4 combinations were compiled and run with the two entity classes on the screen (Member and Order) and the lines of code that read, exactly as they are (including the comment lines). What was left out of the screen is only the package and import lines, and what does not appear on the screen is the code that gets and closes the EntityManager. For each combination, the database and the SessionFactory were created anew, the tables were created by Hibernate, and before the run, member (1, Alice) and order (10, member_id 1) were put into the database with JDBC. The SQL of this setup was not counted, and the code that reads was run in an EntityManager different from the one used for the setup, with an empty persistence context. The code that reads has no line that starts a transaction, and it was run with em left open. What was checked against the run is the SQL that was captured with Hibernate's StatementInspector, split by line of code (the line that sent it was found from the call stack, and the original SQL and the lines were kept in the output of the checking run). In this environment, LAZY sent 1 SELECT that reads only the orders table on the find line and 1 SELECT that reads only the member table on the getName line, and EAGER sent 1 SELECT that does a left join of orders and member on the find line and sent none on the getName line. When there was no line that uses the name, LAZY sent no SELECT that reads the member table at all. The facts that the step sentences rely on were also measured in a second run that inserts a measuring line right below each line of code and splits getMember() and getName() into two lines. With LAZY, right after find, the object in the member field is an object of a proxy class that Hibernate made (Member$HibernateProxy), it is not initialized, and it knows the identifier 1. Right after order.getMember() was called, the SQL did not increase and it was still not initialized. Right after getName(), one SELECT was added and it was initialized. With EAGER, right after find, the object in the member field is a real Member object and not a proxy, it is initialized, and the SQL did not increase after getMember() and getName(). Also, when the same code was run without writing fetch in @ManyToOne, the member was read by 1 joined SELECT on the find line, the same as EAGER (this was not put into the settings on the screen, and was used only as the evidence for the glossary). What the documentation defines is in the Jakarta Persistence 3.2 API documentation: FetchType (EAGER is a requirement that data must be eagerly fetched, and LAZY is a hint that data may be fetched when it is first accessed, and the implementation may eagerly fetch data marked LAZY) and the fetch element of ManyToOne (the default is EAGER if it is not written). The Fetching chapter of the Hibernate User Guide could not be opened at the relevant part of the page, so it was not read, and how a proxy is put in and how EAGER is read with a join are not defined by the documentation but are behavior observed on Hibernate 7.4.5. By the specification, LAZY is a hint, and whether to read EAGER with a join or with a separate SELECT is decided by the implementation, so the screen shows only the SQL that was sent in this environment. This lab deals only with the time while em is open. It does not cover the LazyInitializationException that occurs when you try to fill a proxy after em is closed, the N+1 problem in which the number of SELECTs grows with the number of orders when there are many orders, JPQL queries, or the loading of collections (@OneToMany). The details of proxies are covered in the lab "Proxies and lazy fetching", and N+1 in the lab "N+1". The run was not checked against MySQL.

Previous lab: Associations · Next lab: JPQL

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these