Definition
- EntityManager -- the primary JPA interface for all persistence operations, including lifecycle management, entity lookup, and query execution.
- find(Class, id) -- loads an entity by primary key from the first-level cache or database, returning null if not found.
- getReference(Class, id) -- returns a lazy proxy without a database hit, throwing
EntityNotFoundExceptionwhen a field is accessed if the row does not exist. - persist(entity) -- makes a transient entity managed and schedules an INSERT.
- merge(entity) -- copies detached state into a managed instance and returns it.
- remove(entity) -- schedules a managed entity for DELETE; throws on detached entities.
- createQuery(jpql, Class) -- builds a typed JPQL query with named or positional parameter binding.
The EntityManager is the bank clerk at National Bank of India who sits between you and the vault. You can ask the clerk to file a new account form (persist), copy edits from a photocopy onto the official record (merge), shred a closed account file (remove), fetch a specific file by account number (find), or get a lazy reference slip that says "this file exists but I will not fetch it until you actually read it" (getReference). You can also hand the clerk a search request written in the bank's internal language like "find all accounts with balance above one lakh" (JPQL via createQuery).
Understanding the Concepts
What is EntityManager?
EntityManager is the primary JPA interface for interacting with the persistence context. All persistence operations go through it: creating entities, loading them, updating them, deleting them, and querying them. In Spring Boot, you inject it with @PersistenceContext.
- EntityManager manages the first-level cache (persistence context).
- All entities loaded or persisted in a transaction share the same persistence context.
- Changes are flushed to the database at transaction commit or when explicitly flushed.
package com.bank.service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
@PersistenceContext
private EntityManager em;
// Injected by Spring, scoped to the current transaction
}
How does find() work?
find() loads an entity by its primary key. It first checks the persistence context (first-level cache). If the entity is already loaded in the current transaction, it returns the cached instance without hitting the database. If not cached, it issues a SELECT and returns the entity, or null if the row does not exist.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountLookupService {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public Account findAccount(String accountId) {
Account account = em.find(Account.class, "SAV2001");
// Returns Rajesh's account or null
// Second call in same transaction: NO database hit
Account same = em.find(Account.class, "SAV2001");
// same == account (same object from first-level cache)
return account;
}
}
SQL generated (first call only):
SELECT * FROM accounts WHERE account_id = 'SAV2001'
Second find() in same transaction: zero SQL (cached).
How does getReference() work?
getReference() returns a lazy proxy without hitting the database. The proxy is a placeholder that looks like the entity but has not loaded any data. When you access a field on the proxy, Hibernate loads the actual data. If the row does not exist, accessing a field throws EntityNotFoundException.
- Use getReference() when you only need to set a foreign key without loading the full parent entity.
- Faster than find() when you do not need the entity data immediately.
- Throws EntityNotFoundException lazily, not at call time.
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Transaction;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TransactionService {
@PersistenceContext
private EntityManager em;
@Transactional
public void createTransaction(String accountId, BigDecimal amount) {
// getReference: no SELECT, just a proxy with the ID
Account accountRef = em.getReference(Account.class, accountId);
Transaction txn = new Transaction();
txn.setAccount(accountRef); // FK is set without loading Account
txn.setAmount(amount);
em.persist(txn);
// Only INSERT for transaction. No SELECT for account.
}
}
find() vs getReference():
find(Account.class, "SAV2001")
-> SELECT * FROM accounts WHERE account_id = 'SAV2001'
-> Returns full entity or null
getReference(Account.class, "SAV2001")
-> No SQL at call time
-> Returns a proxy
-> SQL fires only when you access a non-ID field
-> Throws EntityNotFoundException if row does not exist
How do persist(), merge(), and remove() work?
These three methods manage the entity lifecycle. persist() creates, merge() reattaches, and remove() deletes. remove() requires a managed entity, so you typically find it first.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class AccountLifecycleService {
@PersistenceContext
private EntityManager em;
@Transactional
public void createAccount() {
Account account = new Account();
account.setAccountId("SAV2001");
account.setHolderName("Rajesh Kumar");
account.setBalance(new BigDecimal("50000.00"));
em.persist(account); // INSERT scheduled
}
@Transactional
public void reattachAccount(Account detached) {
Account managed = em.merge(detached); // SELECT + copy state
managed.setBalance(new BigDecimal("75000.00")); // UPDATE tracked
}
@Transactional
public void closeAccount(String accountId) {
Account account = em.find(Account.class, accountId);
if (account != null) {
em.remove(account); // DELETE scheduled
}
// Cannot remove a detached entity directly:
// em.remove(detachedAccount) -> throws IllegalArgumentException
}
}
Lifecycle SQL:
persist(): INSERT INTO accounts (...) VALUES (...)
merge(): SELECT ... (load managed) + UPDATE ... (on flush)
remove(): DELETE FROM accounts WHERE account_id = 'SAV2001'
How does JPQL querying work with createQuery()?
createQuery() builds typed JPQL queries. JPQL is object-oriented SQL that works against entity names and fields rather than table and column names. Always use named parameter binding with setParameter() to prevent JPQL injection and enable statement caching.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class AccountQueryService {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public List<Account> findHighValueAccounts(BigDecimal minBalance) {
TypedQuery<Account> query = em.createQuery(
"SELECT a FROM Account a WHERE a.balance > :min ORDER BY a.balance DESC",
Account.class
);
query.setParameter("min", minBalance);
return query.getResultList();
// Returns empty list (never null) if nothing matches
}
@Transactional(readOnly = true)
public Account findByPan(String panNumber) {
TypedQuery<Account> query = em.createQuery(
"SELECT a FROM Account a WHERE a.panNumber = :pan",
Account.class
);
query.setParameter("pan", panNumber);
return query.getSingleResult();
// Throws NoResultException if zero rows
// Throws NonUniqueResultException if more than one row
}
}
Generated SQL:
SELECT a.account_id, a.holder_name, a.balance, a.pan_number
FROM accounts a
WHERE a.balance > ?
ORDER BY a.balance DESC
Parameter binding: ? = 100000.00 (bound safely, not concatenated)
What is the difference between getSingleResult() and getResultList()?
getResultList() returns a List that is empty when nothing matches (never null). getSingleResult() returns exactly one result and throws exceptions for zero or multiple results.
| Method | Zero results | One result | Multiple results |
|---|---|---|---|
| getResultList() | Empty list [] | List with 1 element | List with N elements |
| getSingleResult() | NoResultException | Returns the entity | NonUniqueResultException |
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;
import jakarta.persistence.NonUniqueResultException;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class SafeQueryService {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public Optional<Account> findByPanSafely(String pan) {
try {
Account account = em.createQuery(
"SELECT a FROM Account a WHERE a.panNumber = :pan",
Account.class
).setParameter("pan", pan)
.getSingleResult();
return Optional.of(account);
} catch (NoResultException e) {
return Optional.empty();
} catch (NonUniqueResultException e) {
// Duplicate PAN is a data-integrity bug!
throw new IllegalStateException(
"Duplicate PAN found: " + pan, e);
}
}
}
What are common mistakes with EntityManager?
The most common mistakes involve JPQL injection, removing detached entities, and misunderstanding flush behavior.
// WRONG: JPQL injection via string concatenation
String name = "Rajesh'; DROP TABLE accounts; --";
em.createQuery(
"SELECT a FROM Account a WHERE a.holderName = '" + name + "'",
Account.class
).getResultList();
// Potential JPQL injection!
// CORRECT: parameter binding
em.createQuery(
"SELECT a FROM Account a WHERE a.holderName = :name",
Account.class
).setParameter("name", name)
.getResultList();
// Safe: parameter is bound, not concatenated
// WRONG: removing a detached entity
@Transactional
public void deleteAccount(Account detached) {
em.remove(detached);
// Throws: IllegalArgumentException
// "Removing a detached instance"
}
// CORRECT: find-then-remove
@Transactional
public void deleteAccount(String accountId) {
Account managed = em.find(Account.class, accountId);
if (managed != null) {
em.remove(managed);
}
}
What about flush mode and query consistency?
By default, JPA uses FlushModeType.AUTO, which flushes pending changes before executing a query so your JPQL sees your own uncommitted writes. This keeps reads consistent within a transaction.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class FlushModeDemo {
@PersistenceContext
private EntityManager em;
@Transactional
public void demonstrateAutoFlush() {
Account account = new Account();
account.setAccountId("SAV3001");
account.setBalance(new BigDecimal("200000.00"));
em.persist(account); // INSERT scheduled but not yet flushed
List<Account> highValue = em.createQuery(
"SELECT a FROM Account a WHERE a.balance > :min",
Account.class
).setParameter("min", new BigDecimal("100000.00"))
.getResultList();
// AUTO flush: INSERT executes BEFORE the SELECT
// So SAV3001 appears in the results!
}
}
FlushModeType.AUTO behavior:
1. persist(account) -> INSERT scheduled (in memory)
2. createQuery(...).getResultList() -> auto-flush triggered
3. INSERT INTO accounts ... (flushed to DB)
4. SELECT FROM accounts WHERE balance > 100000
5. SAV3001 appears in results because flush happened first
What about pagination with EntityManager queries?
EntityManager queries support pagination through setFirstResult() and setMaxResults(), which translate to SQL LIMIT and OFFSET. This is the raw JPA mechanism that Spring Data's Pageable wraps.
package com.bank.service;
import com.bank.model.Transaction;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class TransactionPaginationService {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public List<Transaction> getPage(String accountId, int page, int size) {
return em.createQuery(
"SELECT t FROM Transaction t "
+ "WHERE t.accountId = :acct "
+ "ORDER BY t.createdAt DESC",
Transaction.class
).setParameter("acct", accountId)
.setFirstResult(page * size) // offset (0-based)
.setMaxResults(size) // page size
.getResultList();
}
}
SQL generated (page=2, size=20):
SELECT * FROM transactions
WHERE account_id = 'SAV2001'
ORDER BY created_at DESC
LIMIT 20 OFFSET 40
Visual Overview
EntityManager operations:
┌────────────────────────────────────────────────────────┐
│ EntityManager │
│ │
│ Lifecycle: Lookup: Query: │
│ ├── persist() ├── find() ├── createQuery()│
│ ├── merge() └── getReference()├── setParameter()│
│ ├── remove() ├── getResultList()│
│ ├── refresh() └── getSingleResult()│
│ └── detach() │
└────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Persistence │ │ First-Level │ │ JPQL -> SQL │
│ Context │ │ Cache │ │ Translation │
│ (managed │ │ (find checks │ │ + Parameter │
│ entities) │ │ here first) │ │ Binding │
└──────────────┘ └──────────────┘ └──────────────────┘
│ │ │
└─────────┬──────────┘──────────────────┘
▼
┌──────────────┐
│ Database │
│ (SQL issued │
│ on flush) │
└──────────────┘
Key Takeaway
EntityManager is the heart of JPA and the single interface through which all persistence happens. For lifecycle operations, persist inserts new entities, merge reattaches detached ones, and remove deletes managed entities. For lookups by primary key, find returns the entity or null and checks the first-level cache before hitting the database, while getReference returns a lazy proxy useful for setting foreign keys without loading the full parent.
For queries, createQuery builds typed JPQL statements that work against entity names and fields rather than tables and columns. Named parameter binding via setParameter is mandatory for preventing JPQL injection and enabling prepared statement caching. getResultList returns an empty list when nothing matches, while getSingleResult throws NoResultException for zero rows and NonUniqueResultException for more than one, making it appropriate for unique lookups like PAN numbers where a duplicate is a data-integrity bug.
In our National Bank of India system, support agents fetch Rajesh's account by primary key with find, the risk team lists high-value accounts with a bound JPQL query, and compliance fetches exactly one account by its unique PAN using getSingleResult. The auto-flush behavior ensures that within a transaction, JPQL queries see the uncommitted changes made earlier in the same transaction.
Remember one sentence: EntityManager is the gateway to the persistence context, where find and getReference handle lookups, persist/merge/remove manage lifecycle, and createQuery with parameter binding handles safe JPQL queries.
---