1. Two methods for getting an entity by id
In JPA/Hibernate, an entity by id has two different “entry points”: find() and getReference(). If you need the data right now, find() is usually the right choice. If you do not need fields, just a reference to an entity — for example, to link a new order to a customer by customerId — getReference() often saves you an unnecessary SELECT.
In our Commerce Persistence Lab project, this comes up constantly. An order (PurchaseOrder) stores a reference to a customer (Customer). An order item (OrderItem) stores a reference to a product (Product). Inventory records (InventoryItem) also refer to a product. In all these places, our business operation often already has the related entity's id, but far from always does it need to read all of its fields right away.
Hibernate gives us two different contracts for working with the same entity:
- find() — “honestly read the row from the DB and give me an object with data”;
- getReference() — “give me a managed reference to an entity by id, and if I later need fields, we'll deal with it then”.
One important point right away: this is not about “fast and slow” or “right and wrong”. These are two different contracts. And if you choose them consciously, the SQL log becomes much less mysterious.
With new objects, the picture is already fairly clear: persist() puts them into the context, and the real SQL may come later. But a lot of code is built around existing rows, and there it is important to separate two different requests to the ORM: “give me the data” and “give me the reference”.
2. find(): when you need the data now
The find() method is needed when the data really is needed right now. Essentially, you are telling the ORM: “Bring me the entity from the database and make it managed inside the current persistence context.” This is a good, straightforward contract: either the row exists — we get an object, or it does not — we get null. No surprises “later somewhere else” — at least at the level of the row's existence itself.
Minimal find() example on Product
Imagine we are inside a transaction and want to display the product name, meaning we really need the data:
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
// IMPORTANT: this example only makes sense inside an active transaction / persistence context.
Product product = entityManager.find(Product.class, 1L);
// find() usually does a SELECT right away to populate entity fields.
System.out.println(product.getName()); // for example: "Keyboard"
// SQL (in the log) will look roughly like: select ... from products where id=?
Here it is important to see two things at once. First, the object becomes managed — that is, Hibernate now knows this object and keeps it in the context. Second, to get the fields, Hibernate usually does a SELECT right at the moment of find() — if this object is not already in the current persistence context.
If the row does not exist: find() returns null
This behavior is often underestimated, and that is a shame: for application logic, it is very convenient. You can honestly check whether the row exists.
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
// There is probably no row here, so find() will return null (without a "proxy").
Product product = entityManager.find(Product.class, 999_999L);
System.out.println(product); // null
If you are writing code for beginners — or for yourself a month from now, when you are also a beginner, just tired — null often turns out to be more understandable semantics than “I returned an object, but it may blow up later when accessing fields”.
Yes, null is its own philosophy of pain, but it is honest pain: you see it exactly where the operation is performed.
find() and the entity state
After find(), the object becomes managed. That directly connects today's topic with lecture 1: we do not just “take an object”; we move it into a state where it is under Hibernate's control.
If you want to quickly check that after find() the object really is in the current context, contains() is enough.
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
Product product = entityManager.find(Product.class, 1L);
// contains() checks: is the object managed by the current EntityManager right now.
System.out.println(entityManager.contains(product)); // true
That is enough here: find() does not just read a row, it brings back a managed instance.
3. getReference(): when you need a managed reference, not data
getReference() is needed when you want a managed reference, not the entity data. Formally, it is also a way to “get an entity by id”, but in practice you get a proxy object, that is, a “stand-in” for the entity. Imagine that instead of the person itself, you were given their passport number and a promise: “if needed, we will bring them in”. In Java, this looks like an object of the same type — Product, Customer — but inside it is a little “empty” and may load data later.
The meaning of getReference() is easy to feel through one simple question: “Do I need the entity fields right now, or do I only need the id so I can refer to it?” If you only need the id, getReference() usually lets you avoid an unnecessary SELECT and keeps the SQL trail cleaner.
A proxy in the simplest form: the class looks strange, and that's normal
Let's look at what this object is:
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
// getReference() returns a proxy, not a "filled with data" entity.
Product productRef = entityManager.getReference(Product.class, 1L);
// Usually id is available without touching the database (Hibernate already knows the proxy id).
System.out.println(productRef.getId()); // 1
// The real class will not be Product, but a generated Hibernate proxy class.
System.out.println(productRef.getClass()); // class ...Product$HibernateProxy$...
The output of getClass() often scares people: instead of a “plain” Product, you will see a class whose name contains something like HibernateProxy. This is not an error and not a “broken Hibernate”. This is exactly how Hibernate implements the idea of “the object exists, but the data may be loaded later”.
Important: getReference() returns a managed object — just in proxy form. Such a proxy also lives inside the context, and that is easy to verify with the same contains().
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
Product productRef = entityManager.getReference(Product.class, 1L);
// Even a proxy object is considered managed if it was obtained through EntityManager.
System.out.println(entityManager.contains(productRef)); // true
When SQL happens with getReference()
The key trick of getReference() is that it does not have to do a SELECT at the moment of the call. But as soon as you actually request the data, the proxy may “unwrap” itself and go to the DB.
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
Product productRef = entityManager.getReference(Product.class, 1L);
// As soon as you read a "regular" field, Hibernate may execute a SELECT.
String name = productRef.getName(); // Hibernate may execute a SELECT here
System.out.println(name); // for example: "Keyboard"
It helps to keep a simple model in mind: getReference() gives you an “object-promise”. While you work with it as an identifier, it reads nothing. As soon as you start accessing regular fields, it turns that into a read.
And yes, this is exactly where beginners usually get the feeling of “I just read a field, and suddenly I got SQL”. getReference() exists to defer loading data, not to cancel it.
4. A project scenario: create a PurchaseOrder without loading Customer
Let's make this as practical as possible. Suppose we already have a customerId: it came from a command, DTO, or parameter. We are creating an order and only need to specify whose it is. At that moment, a full SELECT * FROM customers ... is usually not needed. We are not showing the customer's email, not validating the name, and not doing anything like that — we are just setting a foreign key.
From the database point of view, this is simple: write customer_id = ? into the orders table. Hibernate lets you do this just as simply, without turning “reference by id” into “first load the customer, then refer to them”.
Service code example: getReference() for linking
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.commerce.customer.entity.Customer;
import com.example.commerce.orders.entity.PurchaseOrder;
@Service
public class OrderCreateService {
private final EntityManager entityManager;
public OrderCreateService(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public void createOrder(String orderNumber, long customerId) {
// Take a managed reference to the customer without loading its data.
Customer customerRef = entityManager.getReference(Customer.class, customerId);
PurchaseOrder order = new PurchaseOrder();
order.setOrderNumber(orderNumber);
// Set the customer reference — Hibernate uses customerId from the proxy for the FK.
order.setCustomer(customerRef);
// INSERT for the order is possible without SELECTing the customer (if we do not read customer fields).
entityManager.persist(order);
}
}
Here it is important to feel one thing, even if only as a note for now. We linked the order to the customer through a Customer object, but at the same time we did not require the customer's data. Hibernate can build the INSERT for the order so that it uses customerId from the proxy, and does not do an unnecessary SELECT for the customer.
How this shows up in SQL (the idea, not exact strings)
In SQL trace mode, which we enable with the sql-trace profile, you will often see something like this: there will be an INSERT into purchase_orders, where one of the parameters is customer_id, and there will not be a SELECT from customers until you start reading customer fields.
Let's compare this in a small table:
| Approach | What we do in code | What often happens in SQL |
|---|---|---|
| Naive | Customer c = find(...); order.setCustomer(c); persist(order) | SELECT customer + INSERT order |
| Intentional | Customer ref = getReference(...); order.setCustomer(ref); persist(order) | INSERT order (without SELECT customer) |
This is not “magic optimization for optimization's sake”. It is simply sound engineering: if I only need the id and an FK relationship, I do not have to read the whole row from the DB just to set a reference.
5. No row: find() vs getReference()
At this point, students usually have a fair question: “Okay, getReference() does not read the database. So how does it know whether the row exists at all?” The answer is simple and slightly sad: it doesn't. It does not know. It returns a reference “to an entity with this id”, but existence is checked only when that becomes important.
With find(), things are simpler: if the row does not exist, you get null right away. With getReference(), there is no check at call time; it appears either when you try to get data from the proxy or when you try to write a reference that violates constraints, such as a foreign key.
find() and a non-existent id
import jakarta.persistence.EntityManager;
import com.example.commerce.customer.entity.Customer;
// find() immediately returns null if there is no row in the DB.
Customer customer = entityManager.find(Customer.class, 123_456L);
if (customer == null) {
// Here, the "not found" handling happens in a clear place.
System.out.println("Customer not found"); // Customer not found
}
getReference() and a non-existent id: the error appears “later”
If you get a reference and immediately try to read fields, you can run into an EntityNotFoundException:
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import com.example.commerce.catalog.entity.Product;
try {
// You can get a proxy even for a non-existent id: this is not yet a row-existence check.
Product productRef = entityManager.getReference(Product.class, 999_999L);
// The check will happen when trying to load data (usually when reading fields).
System.out.println(productRef.getName()); // may trigger SELECT + exception here
} catch (EntityNotFoundException e) {
// The error may "surface" not on getReference(), but on data access.
System.out.println("Product not found"); // Product not found
}
And if you do not read fields, but simply use the reference in an INSERT — as in the order-and-customer example — then the database itself will check existence through the foreign key. In that case you will see a DB-level error, and it can also arrive at the moment of synchronization with the database, not strictly on the line with persist(). The exact synchronization moment is not important here; it is enough to remember that the problem may show up later than the line with getReference().
6. getReference() is not a check and not a speed booster
The most common trap: start using getReference() as a “quick check for whether a row exists”. That does not work, because getReference() almost always returns not null. It returns a reference object. And you can easily write code that looks logical but is logically wrong.
Incorrect check: “if not null, then it exists”
import jakarta.persistence.EntityManager;
import com.example.commerce.catalog.entity.Product;
// getReference() almost always returns an object, even if the row does not exist in the DB.
Product productRef = entityManager.getReference(Product.class, 999_999L);
// This check is meaningless: the proxy object exists, but the row may not.
if (productRef != null) {
System.out.println("Product exists"); // Product exists (even if it does not in the DB)
}
From Java's point of view, this is correct: the object really is not null. From the point of view of meaning, it is a mistake, because you checked whether “an object exists”, not whether “a row exists in the DB”.
“Accidental reading” turns a reference into SQL (and sometimes into a problem)
The second typical story: you take getReference() “so there won't be a SELECT”, and then in the same method you immediately read fields, for example for logging.
import jakarta.persistence.EntityManager;
import com.example.commerce.customer.entity.Customer;
Customer customerRef = entityManager.getReference(Customer.class, 10L);
// It seems like this is just logging...
// But accessing a regular field may force the proxy to execute SELECT.
System.out.println(customerRef.getEmail()); // but this may become SELECT
Please understand correctly: logging is not evil, and getReference() is not evil. Evil is when you thought SQL would not happen, and then you caused it yourself by reading a field. It is like buying a “don't go to the gym” pass, and then still going to the gym — the pass is not to blame.
And one more important point, which we will leave here as a note in the margin for now: a proxy can load data only when it has access to the persistence context. For today it is enough to remember that a proxy is not omnipotent and, without a context, it cannot fetch the data.
Mini cheat sheet for choosing: find() or getReference()
In real work, choosing a method looks less like “memorize a rule” and more like a small dialogue with yourself: “Do I need information or identity?” Below is a diagram that is handy to keep in mind as a cheat sheet. It does not claim philosophical depth, but it is very practical.
flowchart TD
A["There is an entity id"] --> B{"Need fields right now?"}
B -->|Yes| C["entityManager.find() (get data)"]
B -->|No| D{"Only need to link by id (foreign key)?"}
D -->|Yes| E["entityManager.getReference() (get reference/proxy)"]
D -->|Not sure| F["Default: find() (easier to read code)"]
And one more table for reinforcement:
| Question you ask yourself | Better fit |
|---|---|
| “I need to display/check entity fields” | find() |
| “I just need to set a reference by id (FK), without reading” | getReference() |
| “I need to check whether the row exists” | find() (or a separate exists query, but that is a different conversation) |
| “I am going to read name/email/status right away anyway” | more often find() (simpler and more honest) |
This naturally raises the next engineering question: what happens if you fetch the same entity several times in one persistence context, and why does that not create two competing copies in memory?
7. Typical mistakes when working with find() and getReference()
Before closing the topic, it is worth spelling out a few mistakes that happen constantly — both with beginners and with experienced people who are just tired and have started writing “whatever works”. These mistakes are especially tricky because the code often compiles, runs, and even “sometimes works”, while the problem appears later — in the SQL log or in rare edge cases.
Mistake #1: using getReference() as a check for existence.
This looks logical because the method “gets an entity by id”, but its contract is different. It returns a reference, not confirmation that the row exists in the DB. Therefore checks like if (getReference(...) != null) are almost always wrong: the object will be there, but the row may not be.
Mistake #2: taking getReference() for “optimization” and then immediately reading fields.
If you still need customer.getEmail() or product.getName() two lines later, then getReference() gives you no benefit. It only postpones the SELECT until the field is accessed. For code readability — and for future-you, who will be debugging this later — it is usually more honest to use find() right away.
Mistake #3: not understanding where exactly the SQL will happen.
With find(), SQL often goes out immediately; with getReference(), it happens when data is accessed. If you do not keep the “moment of SQL” in your head, the log starts looking like horror: “I just printed a log, and it went to the database.” Hibernate is not taking revenge — it is fulfilling the proxy contract.
Mistake #4: fetching an entity with find() “just in case” in write operations.
Often, in a service, people create an order and automatically do find(Customer, id) nearby just to “have the object”. But if customer data is not needed, that is an unnecessary SELECT. Later, these “unnecessary” places become dozens, and you suddenly discover that a simple operation creates an order not with 3 queries, but with 25.
Mistake #5: forgetting that find() can return null.
find() honestly reports “not found” through null, but if you do not check that value, you can easily get a NullPointerException where there was actually a normal business situation: “there is no customer with this id”. That is not Hibernate's fault — it is a reason to write application code more carefully.
GO TO FULL VERSION