The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › JPA
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
@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();
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
Struck-through text was removed from the first version of the code; highlighted text was added.