- JPA vs Hibernate Overview
- Architecture & Core Components
- Entity Lifecycle
- Persistence Context & EntityManager
- Mappings
- JPQL & Criteria API
- Fetching Strategies
- Caching
- Transactions
- Locking
- The N+1 Problem
- Dirty Checking & Flushing
- Hibernate Session Internals
- Spring Data JPA
- Performance Best Practices
- Common Exceptions & Causes
- Key Interview Q&A
┌────────────────────────────────────────────┐
│ Your Application │
├────────────────────────────────────────────┤
│ Spring Data JPA (optional) │
├────────────────────────────────────────────┤
│ JPA (Jakarta Persistence API) │ ← Specification (interfaces only)
│ EntityManager | JPQL | Criteria API │
├────────────────────────────────────────────┤
│ Hibernate ORM │ ← Implementation
│ Session | HQL | SessionFactory │
├────────────────────────────────────────────┤
│ JDBC │
├────────────────────────────────────────────┤
│ Relational Database │
└────────────────────────────────────────────┘
| JPA | Hibernate | |
|---|---|---|
| Type | Specification (JSR 338) | Implementation of JPA |
| Package | jakarta.persistence |
org.hibernate |
| Key Interface | EntityManager |
Session (extends EntityManager) |
| Factory | EntityManagerFactory |
SessionFactory |
| Query Language | JPQL | HQL (superset of JPQL) |
| Extra Features | Standard only | Filters, multi-tenancy, envers, custom types |
Other JPA implementations: EclipseLink (reference implementation), OpenJPA, DataNucleus.
- Heavyweight, thread-safe, one per application
- Reads mapping metadata, builds SQL templates
- Manages connection pools, second-level cache
- Created once at startup — expensive to create
- Lightweight, NOT thread-safe, one per unit of work
- Wraps a JDBC connection
- Manages the first-level cache (persistence context)
- Created and closed per request/transaction
<!-- persistence.xml (JPA standard) -->
<persistence-unit name="myPU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="jakarta.persistence.jdbc.url" value="jdbc:postgresql://localhost/mydb"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.jdbc.batch_size" value="30"/>
</properties>
</persistence-unit>hbm2ddl.auto values:
| Value | Behavior |
|---|---|
none |
Do nothing (production default) |
validate |
Validate schema matches mappings |
update |
Add missing columns/tables (never drops) |
create |
Drop and recreate on startup |
create-drop |
Create on startup, drop on close (testing) |
┌───────────────────────────────────────────────────┐
│ Persistence Context │
│ │
new ──►│ TRANSIENT ──persist()──► MANAGED ──remove()──► REMOVED │
│ ▲ │ │
│ │ │ flush/commit │
│ merge()│ ▼ │
│ │ DATABASE │
│ │ │ │
│ DETACHED ◄──detach()/close()│evict() │
│ │ │
└─────┼─────────────────────────────────────────────┘
│
└──merge()──► MANAGED (re-attached copy)
Transient
- Object created with
new, not associated with anySession - No database row, no identity assigned by JPA
- Not tracked — changes ignored
Managed (Persistent)
- Associated with an open
Session/EntityManager - Has a database identity (primary key)
- Changes are automatically tracked (dirty checking) and flushed to DB
Detached
- Was managed, but
Sessionwas closed ordetach()called - Still has a primary key but no longer tracked
- Re-attach with
merge()(JPA) orupdate()/saveOrUpdate()(Hibernate)
Removed
remove()called on a managed entity- Scheduled for DELETE on next flush
The Persistence Context is a first-level cache and change tracker. It is the set of all currently managed entity instances.
| Method | Description |
|---|---|
persist(entity) |
Transition to Managed; INSERT on flush |
find(Class, id) |
Load by PK; returns null if not found; hits L1 cache first |
getReference(Class, id) |
Returns a proxy; no DB hit until accessed; EntityNotFoundException on access if missing |
merge(entity) |
Merge detached state into a new managed copy; returns the managed copy |
remove(entity) |
Schedule for DELETE |
flush() |
Write pending changes to DB (does NOT commit) |
refresh(entity) |
Reload from DB, discard in-memory changes |
detach(entity) |
Remove from persistence context |
clear() |
Detach all entities (clears L1 cache) |
contains(entity) |
Is entity currently managed? |
Transaction-scoped (default in Java EE / Spring): Context lives for the duration of the transaction. Most common.
Extended (Stateful beans / special Spring config): Context survives across multiple transactions — used in long conversations (stateful session beans).
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "full_name", nullable = false, length = 100)
private String name;
@Enumerated(EnumType.STRING) // store as "ACTIVE", not 0/1
private Status status;
@Temporal(TemporalType.TIMESTAMP) // for java.util.Date
private Date createdAt;
@Transient // not persisted
private String tempField;
@Lob // stored as CLOB/BLOB
private byte[] photo;
@Embedded
private Address address; // maps Address fields inline into this table
}
@Embeddable
public class Address {
private String street;
private String city;
}@GeneratedValue strategies:
| Strategy | Description |
|---|---|
AUTO |
JPA picks (usually SEQUENCE for most DBs) |
IDENTITY |
DB auto-increment column (id SERIAL) |
SEQUENCE |
DB sequence; allows pre-allocation (@SequenceGenerator) |
TABLE |
Portable but slow; uses a dedicated table for IDs |
SEQUENCE is preferred for Hibernate batch inserts — IDENTITY disables batching.
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "passport_id")
private Passport passport;// Parent (Department)
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
// Child (Employee)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;Always put
mappedByon the non-owning side. The owning side (the one with@JoinColumn) controls the FK.
@ManyToMany
@JoinTable(
name = "employee_project",
joinColumns = @JoinColumn(name = "employee_id"),
inverseJoinColumns = @JoinColumn(name = "project_id")
)
private Set<Project> projects = new HashSet<>();| Cascade | Effect |
|---|---|
PERSIST |
Save child when parent is saved |
MERGE |
Merge child when parent is merged |
REMOVE |
Delete child when parent is deleted |
REFRESH |
Refresh child when parent is refreshed |
DETACH |
Detach child when parent is detached |
ALL |
All of the above |
orphanRemoval = true — removes child entity from DB when removed from the parent collection. Only valid on @OneToMany / @OneToOne.
All subclasses in one table. Uses a discriminator column.
- ✅ Best performance (single JOIN)
- ❌ Nullable columns for subclass-specific fields; poor with
NOT NULLconstraints
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
public abstract class Payment { ... }
@Entity
@DiscriminatorValue("CREDIT")
public class CreditCardPayment extends Payment { ... }Each concrete class gets its own table with all fields (including inherited).
- ✅ Clean tables;
NOT NULLon all columns - ❌ Polymorphic queries require
UNION ALLacross all tables
Parent class in one table, subclass-specific fields in separate tables joined by PK FK.
- ✅ Normalized; clean structure
- ❌ Requires JOIN for every query; slowest for reads
Strategy | Tables | Query | Nulls | Normalized
------------------|--------|---------|--------|----------
SINGLE_TABLE | 1 | Fast | Many | No
TABLE_PER_CLASS | N | UNION | None | Partial
JOINED | N+1 | JOIN | None | Yes
Operates on entity objects, not tables. Column names = field names, table names = class names.
// Named parameter
TypedQuery<Employee> q = em.createQuery(
"SELECT e FROM Employee e WHERE e.department.name = :dept AND e.salary > :min",
Employee.class
);
q.setParameter("dept", "Engineering");
q.setParameter("min", 50000);
List<Employee> result = q.getResultList();
// JOIN FETCH (avoids N+1)
em.createQuery("SELECT d FROM Department d JOIN FETCH d.employees", Department.class);
// DTO Projection
em.createQuery("SELECT new com.example.EmpDTO(e.name, e.salary) FROM Employee e", EmpDTO.class);
// Aggregate
em.createQuery("SELECT AVG(e.salary) FROM Employee e", Double.class).getSingleResult();@NamedQuery(name = "Employee.findByDept",
query = "SELECT e FROM Employee e WHERE e.department.name = :dept")Parsed and validated at startup — faster than dynamic queries.
Type-safe, programmatic query building — verbose but refactor-safe.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
Root<Employee> root = cq.from(Employee.class);
cq.select(root)
.where(cb.and(
cb.equal(root.get("status"), Status.ACTIVE),
cb.greaterThan(root.get("salary"), 50000)
))
.orderBy(cb.asc(root.get("name")));
List<Employee> results = em.createQuery(cq).getResultList();Query q = em.createNativeQuery(
"SELECT * FROM employees WHERE department_id = ?", Employee.class);
q.setParameter(1, deptId);| EAGER | LAZY | |
|---|---|---|
| When loaded | Always, with parent | Only when accessed |
| SQL | JOIN or extra SELECT immediately | SELECT on first access |
| Default for | @ManyToOne, @OneToOne |
@OneToMany, @ManyToMany |
| Problem | Loads data you may not need | LazyInitializationException if accessed outside Session |
Best practice: Always use
LAZYfor collections. Override to EAGER only when needed viaJOIN FETCHin JPQL or@EntityGraph.
Occurs when a lazy-loaded association is accessed after the Session is closed.
// WRONG: session closed after service method returns
Employee e = employeeService.findById(1L);
e.getDepartment().getName(); // LazyInitializationException!
// FIX 1: JOIN FETCH in query
"SELECT e FROM Employee e JOIN FETCH e.department WHERE e.id = :id"
// FIX 2: @EntityGraph
@EntityGraph(attributePaths = {"department", "projects"})
Employee findById(Long id);
// FIX 3: @Transactional on calling method (keeps session open)
// FIX 4: Open-Session-In-View (not recommended for production)@EntityGraph(attributePaths = {"department", "skills"})
Optional<Employee> findByIdWithGraph(@Param("id") Long id);Generates a single JOIN query — much better than N+1 selects.
@BatchSize(size = 20) // fetch 20 collections at a time
private List<Order> orders;Instead of N+1, issues SELECT ... WHERE id IN (?, ?, ... 20 ids).
- Always on, scoped to a single
Session/EntityManager - Every entity loaded in a session is stored here by type + PK
- Subsequent
find()calls for the same PK hit cache, not DB - Cleared when session closes or
clear()/evict()called
Employee e1 = em.find(Employee.class, 1L); // SELECT from DB
Employee e2 = em.find(Employee.class, 1L); // from L1 cache — no SQL
assert e1 == e2; // true — same object reference- Optional, shared across all sessions in the same
SessionFactory - Stores entity data by type + PK (not object instances)
- Must be explicitly enabled and configured
- Common providers: EhCache, Caffeine, Infinispan, Redis
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Country { ... }hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactoryCache Concurrency Strategies:
| Strategy | Description | Use Case |
|---|---|---|
READ_ONLY |
No updates; fastest | Reference data (countries, config) |
NONSTRICT_READ_WRITE |
Stale reads possible | Rarely updated data |
READ_WRITE |
Soft locks; no stale reads | Frequently updated |
TRANSACTIONAL |
Full XA transactions | Strict consistency needed |
- Caches query result sets (list of PKs, not full entities)
- Entity data still fetched from L2 cache or DB
- Must be explicitly enabled per query
query.setHint("org.hibernate.cacheable", true);
query.setHint("org.hibernate.cacheRegion", "employee.query");Only useful for queries with same parameters called frequently with infrequently changing data.
// Resource-local (non-JTA)
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
// ... operations
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
em.close();
}@Service
public class EmployeeService {
@Transactional // default: REQUIRED, RUNTIME exception rollback
public void transferDepartment(Long empId, Long deptId) { ... }
@Transactional(readOnly = true) // hint to provider — no dirty checking, no flush
public Employee findById(Long id) { ... }
@Transactional(rollbackFor = Exception.class) // rollback on checked exceptions too
public void riskyOperation() throws Exception { ... }
}| Propagation | Behavior |
|---|---|
REQUIRED (default) |
Join existing TX; create new if none |
REQUIRES_NEW |
Always create new TX; suspend existing |
NESTED |
Nested TX with savepoint inside existing |
SUPPORTS |
Use TX if exists; no TX otherwise |
NOT_SUPPORTED |
Suspend any existing TX; run without |
MANDATORY |
Must have existing TX; throw if none |
NEVER |
Must NOT have TX; throw if one exists |
| Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
READ_UNCOMMITTED |
✅ possible | ✅ possible | ✅ possible |
READ_COMMITTED |
❌ prevented | ✅ possible | ✅ possible |
REPEATABLE_READ |
❌ | ❌ prevented | ✅ possible |
SERIALIZABLE |
❌ | ❌ | ❌ prevented |
Most databases default to READ_COMMITTED. PostgreSQL default is READ_COMMITTED.
Assumes conflicts are rare. Uses a version field to detect concurrent modifications.
@Entity
public class Account {
@Id
private Long id;
@Version
private int version; // or Long, Timestamp
private BigDecimal balance;
}On UPDATE, Hibernate adds: WHERE id = ? AND version = ?
- If
0 rows updated→ another transaction modified it → throwsOptimisticLockException - Version auto-incremented on each update
Best for: Read-heavy workloads; low contention; no long DB locks.
Assumes conflicts are likely. Acquires a DB-level lock.
// Shared lock — others can read, not write
em.find(Employee.class, id, LockModeType.PESSIMISTIC_READ);
// Exclusive lock — others can't read or write
em.find(Employee.class, id, LockModeType.PESSIMISTIC_WRITE);
// In JPQL
em.createQuery("SELECT e FROM Employee e WHERE e.id = :id", Employee.class)
.setParameter("id", id)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.getSingleResult();Best for: Write-heavy workloads; high contention; short operations.
| Mode | Description |
|---|---|
NONE |
No lock |
OPTIMISTIC |
Read with optimistic lock (version check on commit) |
OPTIMISTIC_FORCE_INCREMENT |
Increment version even on read |
PESSIMISTIC_READ |
Shared DB lock |
PESSIMISTIC_WRITE |
Exclusive DB lock |
PESSIMISTIC_FORCE_INCREMENT |
Exclusive lock + increment version |
The most common Hibernate performance issue.
Problem: Loading N parent entities, then issuing 1 additional query per entity to load a collection.
// Loading 100 departments → 1 query for departments + 100 queries for employees = 101 queries!
List<Department> depts = em.createQuery("SELECT d FROM Department d", Department.class).getResultList();
for (Department d : depts) {
System.out.println(d.getEmployees().size()); // triggers SELECT for each department
}Solutions:
// 1. JOIN FETCH — best for small collections
"SELECT DISTINCT d FROM Department d JOIN FETCH d.employees"
// DISTINCT needed to de-duplicate parent rows from JOIN
// 2. @EntityGraph
@EntityGraph(attributePaths = "employees")
List<Department> findAll();
// 3. @BatchSize — good for large collections
@BatchSize(size = 25)
private List<Employee> employees;
// Issues: SELECT * FROM employees WHERE department_id IN (?, ?, ..., 25 ids)
// 4. @Fetch(FetchMode.SUBSELECT) — one extra query for all children
@Fetch(FetchMode.SUBSELECT)
private List<Employee> employees;
// Issues: SELECT * FROM employees WHERE department_id IN (SELECT id FROM departments)Detection: Enable hibernate.show_sql=true and hibernate.format_sql=true. Use Hibernate Statistics or p6spy / datasource-proxy to count queries per request.
Hibernate automatically detects changes to managed entities and generates UPDATE SQL at flush time.
How it works:
- When an entity is loaded, Hibernate stores a snapshot (copy of all field values)
- At flush time, it compares current state with snapshot
- If different (dirty) → generates and executes UPDATE
Employee e = em.find(Employee.class, 1L);
e.setSalary(75000); // no explicit save() needed!
// on flush/commit → UPDATE employees SET salary = 75000 WHERE id = 1| Mode | When SQL is sent to DB |
|---|---|
AUTO (default) |
Before query execution (if needed) + before commit |
COMMIT |
Only before commit |
ALWAYS |
Before every query |
MANUAL |
Only on explicit flush() call |
FlushModeType.COMMIT can improve performance when many reads occur within a transaction — avoids unnecessary flushes before queries.
For read-only operations, disable dirty checking to skip snapshot comparison:
// JPA
em.createQuery("SELECT e FROM Employee e").setHint(
QueryHints.HINT_READONLY, true).getResultList();
// Hibernate Session
session.setDefaultReadOnly(true);
// Spring
@Transactional(readOnly = true) // Hibernate sets session to read-only automaticallyHibernate batches operations (INSERT/UPDATE/DELETE) in an ActionQueue rather than executing immediately. Flushed in order: inserts → updates → deletes. This enables JDBC batching.
hibernate.jdbc.batch_size=30
hibernate.order_inserts=true # group same-type inserts together
hibernate.order_updates=trueSends multiple SQL statements in one network roundtrip. Requires SEQUENCE or TABLE ID generation — IDENTITY forces immediate INSERT to get the generated ID, breaking batching.
A special Hibernate session with no first-level cache and no dirty checking — for bulk operations.
StatelessSession s = sessionFactory.openStatelessSession();
// Manual INSERT/UPDATE/DELETE without entity state tracking
s.insert(employee);getReference() / load() returns a CGLIB/ByteBuddy proxy — a subclass with all fields null. DB hit happens on first field access.
Employee proxy = em.getReference(Employee.class, 1L); // No SQL yet
proxy.getName(); // NOW SQL executesThis is why instanceof checks and getClass() comparisons on Hibernate entities can behave unexpectedly — you may be dealing with a proxy subclass.
Repository (marker)
└── CrudRepository<T, ID> (save, findById, delete, count)
└── PagingAndSortingRepository (findAll with Pageable/Sort)
└── JpaRepository<T, ID> (flush, saveAll, findAll, deleteInBatch)
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByDepartmentName(String name);
List<Employee> findByDepartmentNameAndSalaryGreaterThan(String dept, BigDecimal sal);
Optional<Employee> findFirstBySalaryOrderBySalaryDesc();
boolean existsByEmail(String email);
long countByStatus(Status status);
@Query("SELECT e FROM Employee e WHERE e.department.name = :dept")
List<Employee> findByDept(@Param("dept") String dept);
@Query(value = "SELECT * FROM employees WHERE hire_date < ?1", nativeQuery = true)
List<Employee> findHiredBefore(LocalDate date);
@Modifying
@Query("UPDATE Employee e SET e.salary = e.salary * 1.1 WHERE e.department.id = :deptId")
int giveRaise(@Param("deptId") Long deptId);
}// Interface projection — only selected fields
public interface EmployeeSummary {
String getName();
BigDecimal getSalary();
}
List<EmployeeSummary> findByDepartmentId(Long deptId);
// DTO projection
record EmpDTO(String name, BigDecimal salary) {}
@Query("SELECT new com.example.EmpDTO(e.name, e.salary) FROM Employee e")
List<EmpDTO> findAllProjected();Page<Employee> findByStatus(Status status, Pageable pageable);
// Usage
Page<Employee> page = repo.findByStatus(ACTIVE, PageRequest.of(0, 20, Sort.by("name")));
page.getContent(); // list of entities
page.getTotalElements(); // total count
page.getTotalPages();@Configuration
@EnableJpaAuditing
public class JpaConfig { }
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Employee {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}Fetching:
- Always use
FetchType.LAZYon collections — load eagerly only when needed viaJOIN FETCHor@EntityGraph - Avoid
FetchType.EAGER— it loads data unconditionally, even when not needed - Use DTO projections for read-only data — avoid loading full entity graphs
IDs & Batching:
- Use
SEQUENCEstrategy instead ofIDENTITYto enable JDBC batching - Set
hibernate.jdbc.batch_size(30–50) and enableorder_inserts/updates
Caching:
- Cache reference/lookup data with L2 cache (
READ_ONLY) - Mark read-only transactions with
@Transactional(readOnly = true)
Queries:
- Use
JOIN FETCHor@BatchSizeto solve N+1 - Use JPQL DTO projections for reporting queries — don't load entities just to map to DTOs
- Use
StatelessSessionfor bulk imports/exports - Avoid
SELECT *— use projections to fetch only needed columns
Schema & Mappings:
- Use
@Indexon frequently queried columns - Prefer
SetoverListfor@ManyToMany— Hibernate deletes all + re-inserts for List - Set
orphanRemoval = trueinstead of manually removing children - Always initialize collection fields (
= new ArrayList<>()) to avoid NPE
Diagnostics:
- Enable
hibernate.generate_statistics=trueand log statistics - Use
datasource-proxyorp6spyto count queries per request in tests - Write tests that assert on the number of SQL statements executed
| Exception | Cause | Fix |
|---|---|---|
LazyInitializationException |
Accessing lazy association after session close | JOIN FETCH, @EntityGraph, @Transactional on caller |
OptimisticLockException |
Concurrent modification detected via @Version |
Retry logic; use pessimistic lock if contention high |
PessimisticLockException |
DB lock timeout | Reduce lock scope; increase timeout |
EntityNotFoundException |
getReference() accessed but PK not in DB |
Use find() instead |
NonUniqueResultException |
getSingleResult() returns more than one row |
Check query; use getResultList() |
StaleObjectStateException |
Hibernate version of optimistic lock failure | Retry or inform user |
DetachedObjectException |
update() called on entity already managed in session |
Check session; use merge() |
TransientPropertyValueException |
Cascading not configured; child not persisted before parent | Add cascade = PERSIST or persist child first |
ConstraintViolationException |
DB constraint violated (unique, FK, not-null) | Validate input before save |
MultipleBagFetchException |
Two List collections fetched with JOIN FETCH simultaneously |
Use Set instead of List, or separate queries |
Q: What is the difference between get()/find() and load()/getReference()?
find() hits the database immediately and returns null if not found. getReference() returns a proxy without hitting the DB — the query runs only when you access a field. getReference() throws EntityNotFoundException if the entity doesn't exist (when accessed, not immediately).
Q: What is the difference between save(), persist(), and merge() in Hibernate?
persist() (JPA standard) makes a transient entity managed — must be called within a transaction; returns void. save() (Hibernate) is similar but returns the generated ID and can be called outside a transaction (though not recommended). merge() copies the state of a detached entity into a new managed instance and returns the managed copy — the original argument stays detached.
Q: What is the N+1 problem and how do you solve it?
It happens when loading N parent entities triggers N additional queries to load a related collection (one query per parent). Solutions: JOIN FETCH in JPQL, @EntityGraph, Hibernate @BatchSize, or @Fetch(FetchMode.SUBSELECT).
Q: What is dirty checking? When does it happen?
Hibernate stores a snapshot of every loaded entity's state. At flush time it compares current state with the snapshot and auto-generates UPDATE SQL for any changed fields — no explicit save() needed. It happens before queries (AUTO flush mode) and before commit.
Q: Explain first-level vs second-level cache.
L1 cache is per-session, always on — same find() call within one session returns the same object. L2 cache is per-SessionFactory, shared across sessions, optional, requires explicit configuration and a cache provider (EhCache, etc.). L2 stores entity state by PK; it's invalidated on updates.
Q: What is @Transactional(readOnly = true) good for?
It tells Hibernate to set the session as read-only: dirty checking is skipped (no snapshot comparison), and some databases/JDBC drivers can optimize read-only connections (e.g., route to read replica). It does NOT mean the transaction won't see database updates — it's a performance hint, not a guarantee of isolation.
Q: How does @Version work?
Hibernate adds a WHERE version = ? clause to every UPDATE. If 0 rows are updated (because another transaction already changed and incremented the version), Hibernate throws OptimisticLockException, signaling a concurrent modification conflict.
Q: What is the difference between CascadeType.REMOVE and orphanRemoval?
CascadeType.REMOVE propagates the remove() operation — when you explicitly call em.remove(parent), children are also deleted. orphanRemoval = true goes further — it also deletes a child when it is removed from the parent's collection (e.g., parent.getChildren().remove(child)), even without calling remove().
Q: Why should you use Set instead of List for @ManyToMany?
Hibernate handles @ManyToMany with List poorly — when you add or remove one element, it deletes all rows in the join table and re-inserts them. With Set, only the affected row is deleted/inserted. Also, two List JOIN FETCH collections cause MultipleBagFetchException.
Q: What is the Open Session in View (OSIV) pattern and why is it controversial?
OSIV keeps the Hibernate session open throughout the entire HTTP request (including the view rendering layer), preventing LazyInitializationException. It's controversial because it hides design issues (lazy loading in view layer), can lead to unexpected DB queries in templates, and holds DB connections longer than necessary. Disable with spring.jpa.open-in-view=false in production; solve properly with JOIN FETCH or @EntityGraph.
Q: What is the difference between JPQL and SQL? JPQL operates on the object model — entity class names, field names, and relationships. It's database-independent. SQL operates on tables and columns. Hibernate translates JPQL into the appropriate SQL dialect. JPQL supports polymorphic queries (querying a parent class returns all subclass instances).
Q: How does SEQUENCE ID generation improve batch inserts?
With IDENTITY (AUTO_INCREMENT), the DB generates the ID after the INSERT, so Hibernate must execute the INSERT immediately to get the ID back — breaking batching. With SEQUENCE, Hibernate pre-fetches a range of IDs from the DB sequence (allocationSize), then batches multiple INSERTs together in one JDBC batch call.
Last updated: 2026 | Covers: JPA 3.x / Hibernate 6.x / Spring Data JPA 3.x