Hibernate Internals
1. The Core Hibernate Components
Hibernate becomes much easier to understand once we stop seeing it as a tool that only generates SQL. Internally, Hibernate creates and manages a working area in memory, tracks entity objects inside that area, detects changes, and synchronizes those changes with the database at the correct time.
This chapter covers:
- EntityManagerFactory and EntityManager
- Persistence Context
- @PersistenceContext
- Transactions and @Transactional
- Entity lifecycle states
- Dirty checking
- Flushing and write-behind
- First-level cache
Core Components
Basic Relationship
The factory is expensive to create and is normally shared for the lifetime of the application. An actual EntityManager is short-lived and must not be shared concurrently between threads.
2. JPA, Hibernate, and Spring Boot
JPA: The Specification
Jakarta Persistence, commonly called JPA, defines a standard contract for object-relational mapping in Java. It provides interfaces, annotations, and rules such as:
- EntityManagerFactory
- EntityManager
- @Entity
- @Id
- @GeneratedValue
JPA defines what persistence operations should do. It does not contain the complete ORM engine that performs those operations.
Hibernate: The Provider
Hibernate is a JPA provider. It implements the JPA contract and performs the actual work:
- Mapping entity objects to tables
- Generating SQL
- Interacting with JDBC
- Tracking entity state
- Performing dirty checking
- Managing the persistence context
- Coordinating synchronization with the database
Hibernate also offers its own native API:
In Hibernate, SessionFactory extends EntityManagerFactory, and Session extends EntityManager. This allows Hibernate to support the standard JPA API while also providing Hibernate-specific features.
Spring Boot's Role
In a Spring Boot application, our code normally uses JPA types while Hibernate works underneath as the provider.
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Id;
Programming against JPA keeps most persistence code provider-independent. In principle, Hibernate could be replaced by another JPA provider with fewer changes than if the application directly depended on Hibernate-specific APIs.
Spring Boot configures the persistence infrastructure, JPA provides the standard API, and Hibernate implements the ORM behavior.
3. The Persistence Context
A persistence context is Hibernate's in-memory workspace for managed entities.
Suppose we execute:
Student student = entityManager.find(Student.class, 1L);
If the entity is not already available in the current persistence context, Hibernate may:
- Execute a SELECT
- Create a Student object from the returned row
- Place that object in the persistence context
- Begin managing its state
Conceptually:
The persistence context is responsible for much more than storing objects. It supports:
- Entity identity
- Lifecycle state management
- Automatic change detection
- First-level caching
- Transactional write-behind
- Synchronization with the database
The Real Purpose of EntityManager
EntityManager is not merely a class that runs database queries. It is the main JPA interface through which we interact with a persistence context.
entityManager.persist(student);
entityManager.find(Student.class, 1L);
entityManager.remove(student);
entityManager.flush();
entityManager.detach(student);
entityManager.clear();
entityManager.refresh(student);
Each operation affects an entity, the persistence context, or the synchronization between the persistence context and the database.
4. @PersistenceContext in Spring
An EntityManager can be injected into a Spring repository:
@Repository
public class StudentRepository {
@PersistenceContext
private EntityManager entityManager;
}
The name @PersistenceContext is meaningful. We are asking Spring for an EntityManager connected to the persistence context that is appropriate for the current operation.
Why Spring Does Not Inject One Permanent EntityManager
Spring repositories are singleton beans by default. The same repository object can therefore be used by many request threads.
An actual JPA EntityManager, like a Hibernate Session, is not thread-safe. Sharing one physical instance across concurrent requests would be unsafe.
Spring normally injects a shared proxy instead:
Two concurrent transactions can therefore use the same repository while working with different actual persistence contexts:
The injected proxy is stable, but the actual EntityManager handling a call depends on the current transactional context.
5. Transactions from First Principles
Consider a bank transfer:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
If the first statement succeeds and the second fails, money is deducted from one account but never added to the other.
A transaction treats both statements as one logical operation:
BEGIN
UPDATE account A
UPDATE account B
COMMIT
If any operation fails:
BEGIN
UPDATE account A
UPDATE account B - error
ROLLBACK
The region between beginning and completing the transaction is called the transaction boundary.
Unit of Work
A unit of work is the complete set of persistence actions required to perform one business operation.
For example, changing a student's email may involve:
- Retrieving the student
- Checking business rules
- Modifying the entity
- Detecting the change
- Generating SQL
- Executing SQL
- Committing the transaction
All these steps belong to one logical unit of work.
The transaction defines whether the database changes succeed together. The persistence context tracks the entities participating in that work.
Database, Hibernate, and Spring Transactions
These are different layers around the same transactional work.
Database Transaction
The database understands commands such as:
BEGIN;
UPDATE students SET email = 'new@email.com' WHERE id = 1;
COMMIT;
Hibernate Transaction
Using Hibernate's native API, transaction management can be written manually:
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
try {
session.persist(student);
transaction.commit();
} catch (Exception exception) {
transaction.rollback();
throw exception;
} finally {
session.close();
}
Spring Transaction
Spring removes the repetitive transaction-management code:
@Transactional
public void createStudent(Student student) {
entityManager.persist(student);
}
Conceptually, Spring performs:
If the method fails with an exception that triggers rollback:
By default, Spring rolls back for unchecked exceptions such as RuntimeException and Error. Rollback behavior for checked exceptions can be configured using attributes such as rollbackFor.
6. How @Transactional Works
Spring normally applies @Transactional through an AOP proxy.
@Service
public class StudentService {
@Transactional
public void updateStudent(Long id) {
// business operation
}
}
Conceptual call flow:
Because the proxy controls the transaction boundary, a call made from one method to another method on the same object may bypass the proxy. This is the usual self-invocation limitation of Spring proxy-based AOP.
7. Complete Transaction and Persistence Context Flow
Consider:
@Transactional
public void updateStudent(Long id) {
Student student = entityManager.find(Student.class, id);
student.setName("Rohan");
}
Step 1: The Call Reaches the Service Proxy
Step 2: Spring Starts the Transaction
The transaction interceptor creates or joins a transaction according to the propagation rules.
Step 3: An EntityManager Is Associated with the Transaction
Step 4: Hibernate Executes find()
Hibernate first checks the persistence context for Student#id. If present, it returns the managed instance. If absent, it queries the database, creates the entity, and manages it.
Step 5: The Managed Entity Is Modified
student.setName("Rohan");
No explicit update method is required. Hibernate tracks the managed object.
Step 6: Hibernate Flushes
Before commit, Hibernate normally synchronizes pending persistence-context changes with the database.
UPDATE students SET name = 'Rohan' WHERE id = ?;
Step 7: The Database Transaction Commits
After a successful commit, the changes become durable. When the transaction-scoped persistence context ends, its entities are no longer managed by that context.
8. Why Transactions Usually Belong in the Service Layer
A repository method often represents one database action. A business operation may involve several repository calls.
@Transactional
public void enrollStudent(Long studentId, Long courseId) {
Student student = studentRepository.findById(studentId);
Course course = courseRepository.findById(courseId);
enrollmentRepository.create(student, course);
paymentRepository.recordPayment(student, course);
}
If each repository method had an independent transaction, part of the business operation could commit while a later part failed.
The service layer is normally the better transaction boundary because it represents the complete business use case.
Repository methods perform persistence operations. Service methods define which operations must succeed or fail together.
Repository-level @Transactional can still be useful, but it should not accidentally divide one business operation into several unrelated transaction boundaries.
9. Which Operations Require a Transaction?
For a normal transaction-scoped JPA persistence context:
A basic find() may work without an explicit @Transactional, but modifying data should be performed inside a clearly defined transaction.
10. Entity Lifecycle States
An entity instance can move through four main lifecycle states:
- Transient
- Managed
- Detached
- Removed
The managed state is central to Hibernate because dirty checking, automatic updates, first-level caching, relationship synchronization, and flushing depend on it.
10.1 Transient State
Student student = new Student();
student.setName("Rahul");
student.setEmail("rahul@gmail.com");
This object:
- Exists in Java memory
- Is not associated with a persistence context
- Is not tracked by Hibernate
- Normally does not yet represent a stored database row
@Entity makes instances of the class eligible for persistence. It does not cause every object created using new to be managed automatically.
10.2 Managed State
An entity is managed when it is associated with the current persistence context.
A new entity becomes managed through persist():
entityManager.persist(student);
An existing entity returned by find() is also managed:
Student student = entityManager.find(Student.class, 1L);
Hibernate does not create a second copy when persist() is called. The same Java object becomes managed.
Managed Does Not Mean "Already Written to the Database"
Calling:
entityManager.persist(student);
makes the entity managed, but the INSERT may be delayed until a flush. The exact timing can depend on identifier generation and other Hibernate behavior.
The term managed describes the object's relationship with the persistence context, not whether SQL has already executed.
10.3 Detached State
A detached entity:
- Was previously managed
- Still exists as a Java object
- May still represent an existing row
- Is no longer associated with its original persistence context
Common ways an entity becomes detached:
The Persistence Context Ends
Manual detach
entityManager.detach(student);
This detaches one entity.
clear()
entityManager.clear();
This detaches every managed entity and empties the persistence context.
Changes made after detachment are not automatically detected by the original persistence context.
10.4 Removed State
A managed entity becomes removed when:
entityManager.remove(student);
Hibernate now knows that the corresponding row should be deleted when changes are flushed.
remove() is primarily a lifecycle transition. It does not necessarily mean that the DELETE statement executes at that exact line.
Removed vs. Detached
detach(student) does not delete the student. remove(student) does.
11. Entity Lifecycle Diagram
Important Rule About merge()
merge() does not reattach the same detached Java object.
Student managedStudent = entityManager.merge(detachedStudent);
Hibernate copies the detached object's state into a managed instance and returns that managed instance.
detachedStudent→ remains detachedmanagedStudent→ managed return value
Always use the object returned by merge() when further managed operations are required.
12. Checking Whether an Entity Is Managed
Use contains():
Student student = entityManager.find(Student.class, 1L);
boolean managed = entityManager.contains(student);
Inside the same persistence context, the result is normally:
true
After:
entityManager.detach(student);
the result becomes:
false
13. Managed Entity Identity
Within one persistence context, Hibernate maintains one managed object for a particular entity type and primary key.
Student first = entityManager.find(Student.class, 1L);
Student second = entityManager.find(Student.class, 1L);
Within the same persistence context:
first == second // normally true
Both references point to the same managed Java object.
This is called the identity map principle:
Within one persistence context, one database identity is represented by one managed entity instance.
Without this rule, two objects representing the same row could contain conflicting values inside the same unit of work.
14. Dirty Checking
In Hibernate, dirty does not mean invalid or corrupted. An entity is dirty when its current managed state differs from the state Hibernate previously knew.
Original state: name = "Rahul"
Current state: name = "Rohan"
The Problem Dirty Checking Solves
@Transactional
public void updateStudent(Long id) {
Student student = entityManager.find(Student.class, id);
student.setName("Rohan");
}
There is no call to:
entityManager.update(student);
JPA does not even define an EntityManager.update() method. The object returned by find() is managed, so Hibernate can automatically detect its changed state and generate the required UPDATE.
When Dirty Checking Happens
Calling a setter does not normally execute SQL immediately.
Hibernate can compare the entity's current state with its earlier snapshot during flushing. Depending on configuration and enhancement, Hibernate may also use more optimized change-tracking techniques.
Dirty Checking Applies Only to Managed Entities
Student student = entityManager.find(Student.class, 1L);
entityManager.detach(student);
student.setName("Rohan");
The name change is not automatically written to the database because student is detached.
Setting the Same Value
student.setName("Rahul");
If the original value was already "Rahul", Hibernate normally detects no actual state change and does not need to issue an update for that field change.
15. Flushing
Flush synchronizes the changes represented by the persistence context with the database by executing the required SQL statements.
Flush Is Not Commit
This distinction is essential:
Example:
BEGIN
Modify managed entity
FLUSH → UPDATE executes
Error occurs
ROLLBACK → update is undone
SQL execution does not automatically mean that a transaction has committed.
Automatic Flush
With the common AUTO flush mode, Hibernate normally flushes:
- Before transaction commit
- Before certain queries whose results could be affected by pending changes
Example:
@Transactional
public void updateStudent() {
Student student = entityManager.find(Student.class, 1L);
student.setName("Rohan");
}
Typical flow:
Manual Flush
entityManager.flush();
Manual flushing is useful when SQL must be executed before the end of the method, for example:
- To detect a database constraint violation earlier
- To synchronize pending changes before later database work
- To control memory during batch processing when combined with
clear()
16. Transactional Write-Behind
Hibernate describes the persistence context as a transactional write-behind cache.
In a write-through approach, each Java change would immediately cause SQL:
With write-behind:
Delaying synchronization gives Hibernate an opportunity to:
- Avoid unnecessary work
- Order SQL statements
- Group operations
- Support JDBC batching
- Synchronize at a suitable point in the transaction
Write-behind does not remove the need for transactions. It works inside the transaction so that the database can still commit or roll back the resulting SQL as one unit.
17. flush() and clear() in Batch Processing
Suppose we persist 100,000 entities in one loop:
for (int i = 0; i < 100_000; i++) {
Student student = createStudent(i);
entityManager.persist(student);
}
If every entity remains managed until the end, the persistence context can grow very large. That increases memory usage and the amount of entity-management work.
A common batching pattern is:
for (int i = 0; i < 100_000; i++) {
Student student = createStudent(i);
entityManager.persist(student);
if ((i + 1) % 100 == 0) {
entityManager.flush();
entityManager.clear();
}
}
The two operations serve different purposes:
Conceptually:
This controls persistence-context memory. Actual JDBC batching also depends on Hibernate's batch configuration.
18. First-Level Cache
The persistence context provides Hibernate's first-level cache behavior.
The cache key is conceptually made from:
Entity type + primary key
The first-level cache:
- Is enabled by default
- Cannot be disabled like an optional external cache
- Belongs to one persistence context
- Is normally transaction-scoped in a Spring application
- Also guarantees managed entity identity
First Lookup: Cache Miss
Student first = entityManager.find(Student.class, 1L);
If Student#1 is not in the persistence context:
Second Lookup: Cache Hit
Student second = entityManager.find(Student.class, 1L);
If this happens in the same persistence context:
Therefore:
first == second // normally true
Scope of the First-Level Cache
@Transactional
public void methodA() {
entityManager.find(Student.class, 1L); // SELECT
entityManager.find(Student.class, 1L); // first-level cache hit
}
In a later independent transaction:
@Transactional
public void methodB() {
entityManager.find(Student.class, 1L); // SELECT again
}
This is expected because the second transaction normally uses a different persistence context.
First-level cache is not an application-wide cache. It is local to one persistence context.
Important Query Limitation
Primary-key lookups such as find() can be satisfied directly from the persistence context.
A JPQL or Criteria query may still execute SQL even if matching entities are already managed. Hibernate will preserve identity by returning the existing managed instances where applicable, but the query itself is not automatically skipped in every case.
19. How clear() and detach() Affect the Cache
clear()
Student first = entityManager.find(Student.class, 1L);
entityManager.clear();
Student second = entityManager.find(Student.class, 1L);
clear() removes all managed entities from the persistence context. The second find() may therefore execute another SELECT.
first == second // normally false
first is the old detached object, while second is a new managed object.
detach()
Student student = entityManager.find(Student.class, 1L);
entityManager.detach(student);
Only that entity is detached. A later lookup may need to load and manage the entity again.
20. Refreshing an Entity
The first-level cache can contain values that are older than the current database values if another transaction or external process changes the row.
Suppose the managed entity contains:
name = "Rahul"
Another process changes the database value to:
name = "Aman"
The managed Java object does not update automatically. Its value can be reloaded using:
entityManager.refresh(student);
Conceptually:
refresh() discards unsynchronized in-memory changes for the refreshed state and replaces them with database values, so it should be used deliberately.
21. Quick Reference: Important EntityManager Methods
22. Common Misconceptions
"An @Entity object is automatically managed."
No. new Student() is transient until it becomes associated with a persistence context.
"Calling a setter immediately executes an UPDATE."
No. A setter changes the Java object. Hibernate normally detects the change during flush.
"Managed means the SQL has already executed."
No. Managed describes the entity's relationship with the persistence context.
"flush() and commit() are the same."
No. Flush executes pending SQL inside the current transaction. Commit completes the transaction.
"detach() deletes the database row."
No. It only stops the current persistence context from managing that object.
"merge() makes the same detached object managed."
No. It returns a managed instance containing copied state. The supplied object remains detached.
"The first-level cache is shared across the application."
No. It belongs to a single persistence context.
"A singleton repository shares one physical EntityManager across all requests."
Normally no. Spring injects a proxy that routes calls to the transaction-associated EntityManager.
23. Complete Mental Model
The central idea is:
Hibernate does not simply translate individual method calls into SQL. It manages entity state inside a persistence context and synchronizes that state with the database within a transaction.