Hibernate and JPA interview questions: ORM mappings, caching, lazy loading, inheritance and transactions.
30 questions
JPA (Java Persistence API) is a specification - interfaces and annotations like @Entity, @Table and EntityManager. Hibernate is an implementation of that spec (the most popular JPA provider), plus additional native features such as HQL extensions and enhanced caching.
You should code against JPA interfaces so you can swap providers; Hibernate just happens to be the default in Spring Boot.
new, not associated with a session, no database row.Both load an entity by id. get() fetches immediately and returns null if the row does not exist. load() returns a lazy proxy without hitting the database until a property is accessed, and throws ObjectNotFoundException if the row is missing.
Prefer get() when you need the data now or are not sure the row exists; load() helps avoid unnecessary queries for references.
Lazy loading defers loading a relationship until it is actually accessed by fetching a proxy. It avoids loading child collections you never use, but causes trouble outside an open session (LazyInitializationException).
The N+1 problem: querying N parents triggers N extra queries for their children. Fixes: join fetch in JPQL, EntityGraph/annotations, @BatchSize, or session management such as Spring Open Session in View.
Choose based on query patterns: single table is usually the simplest default.
First-level cache is the Session/PersistenceContext: within one session, the same entity id is returned without re-querying. It is always on and cannot be disabled.
Second-level cache is shared across sessions. It is optional and disabled by default; use it for read-mostly entities. Hibernate ships providers such as EHCache. Queries are cached separately via the query cache. Overusing the second-level cache adds invalidation complexity - cache sparingly.
JPA is the Java specification (Jakarta Persistence) defining the API and mapping annotations. Hibernate is one implementation (the reference implementation) of that specification. You write against JPA interfaces (@Entity, EntityManager, @PersistenceContext) and can swap the provider; Hibernate adds extra features like second-level cache and native criteria.
An entity is a plain Java class mapped to a database table. Rules: it must be annotated @Entity, have a no-arg constructor, a primary key (@Id), and its persistent fields map to columns. Entities should not be final, and field access must be either all fields or all getters (consistency).
Transient - new object, not associated with a session, no DB row. Persistent - associated with a session and tracked; changes are flushed to the DB. Detached - was persistent but the session closed; changes are not tracked. Removed - a delete is pending. The session/session context drives the transitions between these states.
get() loads an entity immediately and returns null if the row does not exist. load() is lazy: it returns a proxy without hitting the DB, and throws ObjectNotFoundException if the entity does not exist when the proxy is accessed. Use load() when you only need the object as a reference for associations.
@GeneratedValue(strategy = ...): IDENTITY lets the DB auto-increment; SEQUENCE uses a DB sequence (Hibernate default, efficient batching); TABLE emulates a sequence with a table; AUTO lets the provider choose. You may also assign keys manually, or use composite keys with @EmbeddedId/@IdClass.
@ManyToOne is the many side (many rows reference one parent); @OneToMany is the inverse. The owning side of a bidirectional association is the one holding the foreign key - usually the @ManyToOne side. Updates are written from the owning side; the inverse uses mappedBy and is read-only for relationship management.
Lazy loading defers loading of collections/associations until they are accessed, improving performance. The exception is thrown when a lazy property is accessed outside the session that loaded the entity - for example a collection read in a view after the transaction closed. Fixes: initialize within the transaction (fetch join), use Hibernate.initialize(), configure open-session-in-view carefully, or switch to DTOs.
You issue one query for a list of parents, then, while iterating, Hibernate fires one extra query per child access - N+1 queries total. Solutions: join fetch or @EntityGraph to load associations in the main query, second-level cache, or batch fetching (@BatchSize).
EntityManager is the JPA interface for creating, reading, updating and deleting entities. The persistence context is a first-level set of managed entity instances that the EntityManager tracks; it acts as the first-level cache and delay-writes changes until flush/commit.
First-level cache is the persistence context: per-session (or per-transaction), always on, prevents duplicate loads of the same id within a session. Second-level cache is a session-global (application-level) cache shared across sessions, configured per entity with @Cacheable and a provider such as EHCache; it caches entities and collections across sessions.
The query cache stores the result set identifiers of executed queries (their entity ids and timestamps) so a repeated query can be answered without re-running the SQL. It only helps if the underlying entities/collections are in the second-level cache, and it must be explicitly enabled. Invalidated when related entities change.
Associations may be optional (nullable FK) or required (non-null FK), set with the nullable attribute of @JoinColumn. Element collections (@ElementCollection) store collections of basic or embeddable types (a List of Strings), which cannot be lazily fetched without careful config and are stored in a separate table. For entities prefer a real @OneToMany association instead.
Use @Query with JPQL or native SQL on repository methods:
@Query("select u from User u where u.email = ?1")
Optional<User> findByEmailCustom(String email);
@Query(value = "select * from users where status='A'", nativeQuery = true)
List<User> findActive();JPQL is portable; native queries are DB-specific but can use the full power of the dialect.
JPQL operates on entities and their fields, not tables: select u from User u where u.name = :name. It is database-agnostic. Native SQL is written in the actual database dialect against tables/columns; it enables DB-specific features but removes portability.
When an entity is persistent, Hibernate snapshots its state at load time. At flush, it compares the current state with the snapshot and issues UPDATE statements only for changed entities - no explicit save is needed after modifying a managed object. Merge() can be used to re-attach detached modifications.
flush() synchronizes the persistence context with the database - pending INSERT/UPDATE/DELETE statements are executed but the transaction is not finished. commit() ends the transaction, flushes first, then makes the changes durable. flush can be triggered automatically (AUTO mode) before queries that may be affected.
FlushMode controls when the session flushes changes: AUTO (default - flush before queries that could be affected by pending changes), COMMIT (flush only at commit - risks stale query results), MANUAL/NEVER (flush only when you call flush() explicitly - best performance, but you must manage timing).
Set hibernate.jdbc.batch_size (e.g. 20-50) so multiple INSERT/UPDATE statements are grouped into batches sent in one round trip. With IDENTITY keys, batching is limited because the id must be known; use SEQUENCE keys and order inserts for best results.
hibernate.jdbc.batch_size=50
hibernate.order_inserts=true
hibernate.order_updates=trueOptimistic locking uses a version column (@Version): each UPDATE checks the version, and a concurrent write bumps it - the loser gets an OptimisticLockException. No locks during read; best for read-heavy apps. Pessimistic locking acquires DB locks (SELECT ... FOR UPDATE via LockMode) up front, guaranteeing no conflict but reducing concurrency.
An entity becomes detached when its session closes (or after clear()). Attempting to reference or persist its lazy associations outside the session can throw LazyInitializationException, and re-saving it may create duplicates. Re-attach with merge() or update(), or fetch everything needed before the session ends.
cascade propagates operations (PERSIST, REMOVE, MERGE, ALL) from parent to children - e.g. saving a parent also saves its children. orphanRemoval = true deletes an entity that has been removed from the collection, i.e. it is orphaned from its parent. They are independent: orphanRemoval only applies to relationships.
You map entity fields into plain DTO objects either manually, with MapStruct, or via JPQL constructor expressions. Benefits: you fetch only the columns you need (fewer round trips, less memory), decouple the API from the entity model, avoid lazy-loading and Jackson serialization issues (cycles, LazyInitializationException) and hide internal fields.