Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/e5a94cf43bcc86fc1727fca9c43efc9e to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/e5a94cf43bcc86fc1727fca9c43efc9e to your computer and use it in GitHub Desktop.
JPA & Hibernate — Interview Prep Guide

JPA & Hibernate — Interview Prep Guide

Table of Contents

  1. JPA vs Hibernate Overview
  2. Architecture & Core Components
  3. Entity Lifecycle
  4. Persistence Context & EntityManager
  5. Mappings
  6. JPQL & Criteria API
  7. Fetching Strategies
  8. Caching
  9. Transactions
  10. Locking
  11. The N+1 Problem
  12. Dirty Checking & Flushing
  13. Hibernate Session Internals
  14. Spring Data JPA
  15. Performance Best Practices
  16. Common Exceptions & Causes
  17. Key Interview Q&A

1. JPA vs Hibernate Overview

┌────────────────────────────────────────────┐
│               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.


2. Architecture & Core Components

SessionFactory / EntityManagerFactory

  • 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

Session / EntityManager

  • 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

Configuration & Bootstrap

<!-- 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)

3. Entity Lifecycle

         ┌───────────────────────────────────────────────────┐
         │               Persistence Context                  │
         │                                                    │
  new ──►│  TRANSIENT ──persist()──► MANAGED ──remove()──► REMOVED │
         │                 ▲            │                    │
         │                 │            │ flush/commit       │
         │           merge()│            ▼                    │
         │                 │         DATABASE                 │
         │                 │            │                    │
         │  DETACHED ◄──detach()/close()│evict()             │
         │     │                                             │
         └─────┼─────────────────────────────────────────────┘
               │
               └──merge()──► MANAGED (re-attached copy)

States

Transient

  • Object created with new, not associated with any Session
  • 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 Session was closed or detach() called
  • Still has a primary key but no longer tracked
  • Re-attach with merge() (JPA) or update() / saveOrUpdate() (Hibernate)

Removed

  • remove() called on a managed entity
  • Scheduled for DELETE on next flush

4. Persistence Context & EntityManager

The Persistence Context is a first-level cache and change tracker. It is the set of all currently managed entity instances.

EntityManager Key Methods

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?

Persistence Context Scope

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).


5. Mappings

5.1 Basic & Embedded

@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.


5.2 Relationships

@OneToOne

@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "passport_id")
private Passport passport;

@OneToMany / @ManyToOne

// 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 mappedBy on the non-owning side. The owning side (the one with @JoinColumn) controls the FK.

@ManyToMany

@ManyToMany
@JoinTable(
    name = "employee_project",
    joinColumns = @JoinColumn(name = "employee_id"),
    inverseJoinColumns = @JoinColumn(name = "project_id")
)
private Set<Project> projects = new HashSet<>();

Cascade Types

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.


5.3 Inheritance Strategies

Single Table (SINGLE_TABLE) — Default

All subclasses in one table. Uses a discriminator column.

  • ✅ Best performance (single JOIN)
  • ❌ Nullable columns for subclass-specific fields; poor with NOT NULL constraints
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
public abstract class Payment { ... }

@Entity
@DiscriminatorValue("CREDIT")
public class CreditCardPayment extends Payment { ... }

Table Per Class (TABLE_PER_CLASS)

Each concrete class gets its own table with all fields (including inherited).

  • ✅ Clean tables; NOT NULL on all columns
  • ❌ Polymorphic queries require UNION ALL across all tables

Joined (JOINED)

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

6. JPQL & Criteria API

JPQL (JPA Query Language)

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();

Named Queries

@NamedQuery(name = "Employee.findByDept",
            query = "SELECT e FROM Employee e WHERE e.department.name = :dept")

Parsed and validated at startup — faster than dynamic queries.

Criteria API

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();

Native Queries

Query q = em.createNativeQuery(
    "SELECT * FROM employees WHERE department_id = ?", Employee.class);
q.setParameter(1, deptId);

7. Fetching Strategies

FetchType.EAGER vs FetchType.LAZY

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 LAZY for collections. Override to EAGER only when needed via JOIN FETCH in JPQL or @EntityGraph.

LazyInitializationException

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

@EntityGraph(attributePaths = {"department", "skills"})
Optional<Employee> findByIdWithGraph(@Param("id") Long id);

Generates a single JOIN query — much better than N+1 selects.

Batch Fetching (Hibernate)

@BatchSize(size = 20)   // fetch 20 collections at a time
private List<Order> orders;

Instead of N+1, issues SELECT ... WHERE id IN (?, ?, ... 20 ids).


8. Caching

8.1 First-Level Cache

  • 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

8.2 Second-Level Cache

  • 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.EhCacheRegionFactory

Cache 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

8.3 Query Cache

  • 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.


9. Transactions

JPA Transaction Management

// Resource-local (non-JTA)
EntityTransaction tx = em.getTransaction();
try {
    tx.begin();
    // ... operations
    tx.commit();
} catch (Exception e) {
    tx.rollback();
    throw e;
} finally {
    em.close();
}

Spring @Transactional

@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 Behaviors

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

Isolation Levels

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.


10. Locking

10.1 Optimistic Locking

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 → throws OptimisticLockException
  • Version auto-incremented on each update

Best for: Read-heavy workloads; low contention; no long DB locks.

10.2 Pessimistic Locking

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.

LockModeType Summary

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

11. The N+1 Problem

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.


12. Dirty Checking & Flushing

Dirty Checking

Hibernate automatically detects changes to managed entities and generates UPDATE SQL at flush time.

How it works:

  1. When an entity is loaded, Hibernate stores a snapshot (copy of all field values)
  2. At flush time, it compares current state with snapshot
  3. 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

Flush Modes

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.

Disabling Dirty Checking

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 automatically

13. Hibernate Session Internals

ActionQueue

Hibernate batches operations (INSERT/UPDATE/DELETE) in an ActionQueue rather than executing immediately. Flushed in order: inserts → updates → deletes. This enables JDBC batching.

JDBC Batching

hibernate.jdbc.batch_size=30
hibernate.order_inserts=true    # group same-type inserts together
hibernate.order_updates=true

Sends multiple SQL statements in one network roundtrip. Requires SEQUENCE or TABLE ID generationIDENTITY forces immediate INSERT to get the generated ID, breaking batching.

StatelessSession

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);

Proxies & Lazy Loading

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 executes

This is why instanceof checks and getClass() comparisons on Hibernate entities can behave unexpectedly — you may be dealing with a proxy subclass.


14. Spring Data JPA

Repository Hierarchy

Repository (marker)
  └── CrudRepository<T, ID>        (save, findById, delete, count)
        └── PagingAndSortingRepository (findAll with Pageable/Sort)
              └── JpaRepository<T, ID>  (flush, saveAll, findAll, deleteInBatch)

Derived Query Methods

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);
}

Projections

// 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();

Pagination

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();

Auditing

@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;
}

15. Performance Best Practices

Fetching:

  • Always use FetchType.LAZY on collections — load eagerly only when needed via JOIN FETCH or @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 SEQUENCE strategy instead of IDENTITY to enable JDBC batching
  • Set hibernate.jdbc.batch_size (30–50) and enable order_inserts/updates

Caching:

  • Cache reference/lookup data with L2 cache (READ_ONLY)
  • Mark read-only transactions with @Transactional(readOnly = true)

Queries:

  • Use JOIN FETCH or @BatchSize to solve N+1
  • Use JPQL DTO projections for reporting queries — don't load entities just to map to DTOs
  • Use StatelessSession for bulk imports/exports
  • Avoid SELECT * — use projections to fetch only needed columns

Schema & Mappings:

  • Use @Index on frequently queried columns
  • Prefer Set over List for @ManyToMany — Hibernate deletes all + re-inserts for List
  • Set orphanRemoval = true instead of manually removing children
  • Always initialize collection fields (= new ArrayList<>()) to avoid NPE

Diagnostics:

  • Enable hibernate.generate_statistics=true and log statistics
  • Use datasource-proxy or p6spy to count queries per request in tests
  • Write tests that assert on the number of SQL statements executed

16. Common Exceptions & Causes

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

17. Key Interview Q&A

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment