getReference vs find: JPA EntityManager Methods Compared

EntityManager find() loads an entity with a SELECT or returns null, while getReference() returns a proxy without SQL. See when each runs SQL, when EntityNotFoundException and LazyInitializationException are thrown, and how to set a foreign key without loading the entity.

Jakarta_EE

The EntityManager methods find() and getReference() both look up an entity by its primary key, but find() loads the row right away, whereas getReference() returns a placeholder object without running any SQL. The method find() either returns the real entity or null when the row does not exist. The method getReference() returns a proxy, a generated subclass of the entity that knows only the id and loads the rest on first use.

We use find() when we read or change the entity’s data, and getReference() when we need an entity only to set a foreign key, for example to assign a support ticket to an agent whose id we already know.

The following example maps a ticket that refers to its agent by foreign key, and calls both methods with the SQL of each line as a comment.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "assignee_id")
private Agent assignee;
Agent agent = em.find(Agent.class, 1L);   // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
String name = agent.getName();            // "Lokesh", no SQL
Agent none = em.find(Agent.class, 99L);   // none = null
Agent ref = em.getReference(Agent.class, 1L);                   // no SQL, ref is a proxy
Long id = ref.getId();                                          // 1, no SQL
String refName = ref.getName();                                 // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?

ticket.setAssignee(em.getReference(Agent.class, 1L));
em.persist(ticket);                                             // insert into Ticket (assignee_id,subject,id) values (?,?,default)

String missing = em.getReference(Agent.class, 99L).getName();   // EntityNotFoundException

Notice that the ticket insert with getReference() needs no SELECT for the agent, and that a missing id shows up either as null from find() or as an EntityNotFoundException from the proxy.

Next, we see how each method loads the entity and walk through the support desk example, including the errors a proxy can throw. The FAQs cover Spring Data JPA’s getReferenceById() and the replacements for Session.load() and Session.get().

1. How find() and getReference() Load an Entity

Both methods first look in the persistence context, the set of entities the current EntityManager already holds. When the entity is not there, the two methods behave differently.

  • The method find() runs a SELECT at the call and either returns a fully loaded Agent or null.
  • The method getReference() runs no SQL. It returns an Agent$HibernateProxy that holds only the id, and runs the SELECT when we first call a getter other than getId().

A timeline of the SQL that each call sends makes the difference visible.

Timeline comparing em.find and em.getReference for agent 1: find runs a select at the call and returns a loaded Agent; getReference runs no SQL and returns Agent$HibernateProxy with only the id; getId runs no SQL in both cases; getName runs the select only for the proxy; assigning the agent to a new ticket costs a select plus an insert with find and only the insert with getReference
find() pays for the SELECT up front. getReference() delays it, and skips it completely when we only need the id.
find()getReference()
SQL at the callOne SELECT (none if already loaded)None
Returned objectThe entity itselfA proxy subclass of the entity
Row does not existReturns nullReturns a proxy; EntityNotFoundException on first access
Used after the EntityManager closesWorks, the data is loadedLazyInitializationException, unless loaded before
Best forReading or changing the entitySetting a foreign key, deleting by id

2. find() and getReference() Example

The following example is a small support desk built with Hibernate 7.4.11, Java 25 and an in-memory H2 database. The comments show the SQL that Hibernate prints for each call.

2.1. Support Desk Model

An agent has an id and a name, and a ticket has a subject and an optional assignee, mapped as a lazy @ManyToOne. The Ticket table stores only the agent’s id in its assignee_id column.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String subject;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "assignee_id")
private Agent assignee;

The data is one agent, “Lokesh” with id 1, and one ticket, “Printer is offline”, with no assignee yet.

2.2. Loading an Entity With find()

The method find() sends the SELECT at the call and returns the entity class itself. A second find() for the same id in the same EntityManager returns the same object from the persistence context, so it runs no SQL.

Agent agent = em.find(Agent.class, 1L);   // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
Class<?> type = agent.getClass();         // class Agent
String name = agent.getName();            // "Lokesh"
Agent again = em.find(Agent.class, 1L);   // no SQL, again == agent

When the row does not exist, find() returns null and throws nothing. It still runs the SELECT to find that out.

Agent agent = em.find(Agent.class, 99L);   // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
                                           // agent = null

2.3. Getting a Proxy With getReference()

The method getReference() returns an instance of a class that Hibernate generates at runtime. Because the generated class extends Agent, the variable type and instanceof checks work as usual. The proxy has its id set and loads everything else on demand.

Agent agent = em.getReference(Agent.class, 1L);    // no SQL
String className = agent.getClass().getName();     // com.howtodoinjava.hibernate.reference.Agent$HibernateProxy
boolean isAgent = agent instanceof Agent;          // true
Long id = agent.getId();                           // 1, no SQL
boolean before = Hibernate.isInitialized(agent);   // false

String name = agent.getName();                     // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
                                                   // "Lokesh"
boolean after = Hibernate.isInitialized(agent);    // true

The proxy decides what to do on each getter call. It answers getId() itself, whereas any other getter loads the row once, and from then on the proxy forwards every call to the loaded Agent.

Agent$HibernateProxy extends Agent and holds id 1, an unloaded name and a link to its EntityManager; calling getName checks whether the EntityManager is still open; if yes Hibernate runs select from Agent where id equals 1 and either returns Lokesh when the row exists or throws EntityNotFoundException when there is no row; if the EntityManager is closed it throws LazyInitializationException
A proxy has three possible outcomes on its first real use: the data, EntityNotFoundException, or LazyInitializationException.

2.4. Assigning a Ticket Without Loading the Agent

Setting a foreign key is the main reason getReference() exists. To save the ticket, Hibernate needs only the agent’s id for the assignee_id column, so loading the agent’s name and other columns is wasted work.

emf.runInTransaction(em -> {
  Ticket ticket = new Ticket("VPN keeps disconnecting");
  ticket.setAssignee(em.getReference(Agent.class, agentId));
  em.persist(ticket);
});
// insert into Ticket (assignee_id,subject,id) values (?,?,default)

The same code with find() sends an extra SELECT for an agent we never read.

ticket.setAssignee(em.find(Agent.class, agentId));
em.persist(ticket);
// select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
// insert into Ticket (assignee_id,subject,id) values (?,?,default)

Reassigning an existing ticket works the same way. We load the ticket because we change it, and only reference the agent.

Ticket ticket = em.find(Ticket.class, ticketId);   // select t1_0.id,t1_0.assignee_id,t1_0.subject from Ticket t1_0 where t1_0.id=?
ticket.setAssignee(em.getReference(Agent.class, agentId));
// at commit: update Ticket set assignee_id=?,subject=? where id=?

One saved SELECT looks small, but it is saved for every ticket, which adds up in an import or a batch insert of thousands of rows. For example, a nightly job that imports 10,000 tickets from an email inbox and assigns each one to an agent sends 10,000 fewer queries with getReference().

2.5. When getReference() Throws EntityNotFoundException

The method getReference() does not check that the row exists. The EntityNotFoundException comes later, at the first getter call that needs the data, not at the getReference() call.

Agent agent = em.getReference(Agent.class, 99L);   // no SQL, no error
Long id = agent.getId();                           // 99
String name = agent.getName();                     // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?, then EntityNotFoundException
jakarta.persistence.EntityNotFoundException: No row with the given identifier exists for entity [com.howtodoinjava.hibernate.reference.Agent with id '99']

Jakarta Persistence allows a provider to throw the exception already in getReference(), but Hibernate does not. When we never read the proxy and only use it as a foreign key, there is no EntityNotFoundException at all. The database rejects the INSERT instead.

ticket.setAssignee(em.getReference(Agent.class, 99L));
em.persist(ticket);    // insert into Ticket (assignee_id,subject,id) values (?,?,default)
org.hibernate.exception.ConstraintViolationException: could not execute statement [Referential integrity constraint violation: "FKSPV3UFG8AKGDRCOTXC1DRQKUM: PUBLIC.TICKET FOREIGN KEY(ASSIGNEE_ID) REFERENCES PUBLIC.AGENT(ID) (CAST(99 AS BIGINT))"; ...]

Hibernate generates our ids with IDENTITY, so it runs the INSERT inside persist(). With a sequence, the INSERT and the error come at flush, which by default happens at commit. When the id comes from user input, we either check it with find() first or handle the constraint error, because a foreign key on the table is the only check getReference() gets.

Missing row, what we dofind()getReference()
Call the methodSELECT, returns nullNo SQL, returns a proxy
Read getId()NullPointerException on nullReturns 99
Read getName()NullPointerException on nullSELECT, then EntityNotFoundException
Use it as a foreign keyAssigns null (no error)Foreign key violation on INSERT

2.6. LazyInitializationException After the EntityManager Closes

A proxy loads its data through the EntityManager that created it. Once that EntityManager is closed, the proxy has no way to run the SELECT.

Agent agent = emf.callInTransaction(em -> em.getReference(Agent.class, agentId));
Long id = agent.getId();         // 1, still works
String name = agent.getName();   // LazyInitializationException
org.hibernate.LazyInitializationException: Could not initialize proxy [com.howtodoinjava.hibernate.reference.Agent#1] - no session

The proxy throws the same LazyInitializationException as lazy associations do. We fix it in one of two ways.

  • Use find() when the caller needs the data after the transaction.
  • Load the proxy while the EntityManager is open, with Hibernate.initialize(agent) or any getter call.
Agent agent = emf.callInTransaction(em -> {
  Agent ref = em.getReference(Agent.class, agentId);
  Hibernate.initialize(ref);     // select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
  return ref;
});
String name = agent.getName();   // "Lokesh"

2.7. New find() and getReference() Options in JPA 3.2

Jakarta Persistence 3.2, which Hibernate 7 implements, adds three overloads. With it, the method find() accepts FindOption values such as LockModeType, Timeout, CacheRetrieveMode and CacheStoreMode, in any combination.

Agent agent = em.find(Agent.class, agentId, LockModeType.PESSIMISTIC_WRITE, Timeout.seconds(2));
// select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=? for update wait 2

The method find() also accepts an EntityGraph, a list of associations to load together with the entity. With the graph, the ticket and its assignee come back in one query instead of a ticket with an agent proxy.

EntityGraph<Ticket> graph = em.createEntityGraph(Ticket.class);
graph.addAttributeNode("assignee");
Ticket ticket = em.find(graph, ticketId);
// select t1_0.id,a1_0.id,a1_0.name,t1_0.subject from Ticket t1_0
//   left join Agent a1_0 on a1_0.id=t1_0.assignee_id where t1_0.id=?
boolean loaded = Hibernate.isInitialized(ticket.getAssignee());   // true

The method getReference() gets an overload that takes an entity instead of a class and an id. The new overload helps with a detached entity, which is an entity loaded by an EntityManager that is already closed.

Agent loaded = emf.callInTransaction(em -> em.find(Agent.class, agentId));   // detached after this line

emf.runInTransaction(em -> {
  Ticket ticket = new Ticket("Monitor is flickering");
  ticket.setAssignee(em.getReference(loaded));   // no SQL
  em.persist(ticket);                            // insert into Ticket (assignee_id,subject,id) values (?,?,default)
});

The complete project on GitHub runs every case from section 2 and prints the SQL (mvn -q compile exec:java, mvn test). Its 23 JUnit tests count statements with Hibernate’s Statistics API.

3. getReference() vs find() FAQs

3.1. When Should I Use getReference() Instead of find()?

We use getReference() when the code needs only the id, and find() when it reads the entity’s data.

TaskMethod
Show or return the agent’s datafind()
Check whether an id existsfind() and compare with null
Change the agent’s fieldsfind() (a proxy would load anyway)
Set a foreign key, as in ticket.setAssignee()getReference()
Delete by idgetReference() (see FAQ 3.4)
Pass the entity out of the transactionfind()

3.2. What Is getReferenceById() in Spring Data JPA?

The method getReferenceById() is the Spring Data JPA repository method that calls EntityManager.getReference(), so it has the same proxy behavior. The method findById() calls find() and wraps the result in an Optional.

public interface AgentRepository extends JpaRepository<Agent, Long> { }

Optional<Agent> agent = agentRepository.findById(1L);   // select, Optional.empty() when missing
Agent ref = agentRepository.getReferenceById(1L);       // proxy, no SQL
ticket.setAssignee(ref);

Spring Data JPA 2.7 added getReferenceById(). It replaces getOne() and getById(), which are deprecated and do the same thing under older names.

3.3. Does find() Return a Proxy?

Yes, in one case, namely when the same EntityManager already handed out a proxy for that id. Hibernate keeps one object per id in the persistence context, so the call order decides the class we get back.

Agent found = em.find(Agent.class, 1L);                  // select ...
Agent ref = em.getReference(Agent.class, 1L);            // no SQL, ref == found, class Agent

Agent ref2 = em.getReference(Agent.class, 1L);           // in a new EntityManager: no SQL, a proxy
Agent found2 = em.find(Agent.class, 1L);                 // select ..., found2 == ref2, still the proxy
boolean initialized = Hibernate.isInitialized(found2);   // true

For that reason, we never compare entity classes with getClass(). We use instanceof, or Hibernate.getClass(entity), which returns Agent. Keep in mind that Hibernate.getClass() loads an uninitialized proxy to find its real class.

3.4. Can I Update or Delete an Entity Through getReference()?

Yes. A setter on the proxy loads the row first, because Hibernate needs the old values to detect the change. A delete in Hibernate 7.4 needs no data at all.

em.getReference(Agent.class, 1L).setName("Lokesh Gupta");
// select a1_0.id,a1_0.name from Agent a1_0 where a1_0.id=?
// update Agent set name=? where id=?

em.remove(em.getReference(Agent.class, alexId));
// delete from Agent where id=?

Removing a missing id through a proxy fails at commit with Unexpected row count (expected row count 1 but was 0), because the DELETE matched no row. Hibernate also offers other ways of deleting entities, such as a bulk DELETE query.

3.5. What Replaced Session.load() and Session.get() in Hibernate 7?

The Hibernate Session had its own pair of methods, get() and load(), and Hibernate 7.4 changes both.

  • The method Session.load(Class, id) no longer exists. Use getReference().
  • The method Session.get(Class, id) is deprecated. Use find().

Both replacements exist on EntityManager and on Session, since Session extends EntityManager.

4. Conclusion

The method find() runs a SELECT at once and either returns the entity or null. The method getReference() returns a proxy without SQL and loads it on the first non-id getter. That getter either throws EntityNotFoundException if the row is missing, or LazyInitializationException if the EntityManager is closed. We use find() to read data, and getReference() to set foreign keys or delete by id without loading the entity.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. hi i recently tried Hibernate 4 annotation example, where i m getting communication link failure exception.
    i googled it but i didn’t get proper answer.
    i m using mysql connector 3.0.8. and my database is not in idle mode.
    can u clarify ?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.