Persistence
Database access from Java — JDBC, statements, and parameterized queries.
6 questions
MiddlePerformanceVery commonWhat causes the N+1 select problem in Hibernate, and how do you remove it?
What causes the N+1 select problem in Hibernate, and how do you remove it?
One query loads N parents; touching each parent's lazy association then fires one extra SELECT per parent, so a page of 100 orders issues 101 queries. Remove it by loading the association in the same query — JOIN FETCH in JPQL or an @EntityGraph on the repository method — or by batching (@BatchSize), which collapses the N selects into a few IN (...) queries. EAGER is not a fix: it issues the same N selects earlier.
Common mistakes
- ✗Reaching for
EAGERfetching as the fix, which only moves the same N selects earlier - ✗Blaming a missing index when the query count, not the query cost, is the problem
- ✗Not noticing the problem at all because it only shows up under production-sized N
Follow-up questions
- →Why does
@BatchSizereduce the query count without joining the tables? - →What does
JOIN FETCHdo to pagination withsetMaxResults?
JuniorTheoryCommonWhat is the difference between Statement and PreparedStatement?
What is the difference between Statement and PreparedStatement?
Statement sends a full SQL string each time, built by concatenation — slow to re-plan and open to SQL injection. PreparedStatement sends a parameterized SQL template once; the database compiles it into a reusable plan, and values are bound separately as typed parameters. That binding makes injection impossible (data is never parsed as SQL) and lets the same statement run repeatedly without re-parsing, so PreparedStatement is the default choice.
Common mistakes
- ✗Reversing which one is parameterized and injection-safe
- ✗Thinking manual quote-escaping is an adequate substitute for parameter binding
- ✗Assuming there is no plan-reuse performance benefit to
PreparedStatement
Follow-up questions
- →Why can't a bound parameter be interpreted as part of the SQL syntax?
- →When does reusing one
PreparedStatementacross many executions help most?
JuniorTheoryCommonWhat is JPA, and what does Hibernate add as its implementation?
What is JPA, and what does Hibernate add as its implementation?
JPA is only a specification — the interfaces and annotations of jakarta.persistence (EntityManager, @Entity, JPQL) with no runtime behaviour of its own. Hibernate is a provider that implements it: it maps entities to tables, generates the SQL, keeps a persistence context with dirty checking, and flushes the changes at commit. Code written against JPA stays portable between providers; Hibernate-only extensions do not.
Common mistakes
- ✗Calling JPA a library that executes queries, rather than a specification
- ✗Assuming every Hibernate feature is portable JPA
- ✗Thinking a setter reaches the database immediately, with no flush at commit
Follow-up questions
- →What does the persistence context give you that plain JDBC does not?
- →When does Hibernate actually send the
UPDATEfor a changed entity?
MiddleDebuggingCommonDiagnose a LazyInitializationException thrown when a controller renders an entity
Diagnose a LazyInitializationException thrown when a controller renders an entity
items is a lazy proxy that can only initialize while the persistence context that produced it is open. @Transactional ends when findOrder returns, the session closes, and the serializer touches the proxy outside it — hence no Session. The test passes only because its own @Transactional keeps the session open. Fix it by loading what the caller needs inside the transaction (JOIN FETCH, @EntityGraph) and returning a DTO.
Common mistakes
- ✗Blaming the
LAZYmapping itself instead of the closed persistence context - ✗Turning on
open-in-viewand calling it a fix — it hides the N+1 selects behind the view - ✗Returning the detached entity straight from the controller instead of a DTO
Follow-up questions
- →Why does
Hibernate.initialize(order.getItems())fail outside the transaction too? - →Why is
open-in-viewa poor default in a REST service?
MiddleTheoryCommonHow do @OneToMany, @ManyToOne and @ManyToMany map onto database tables?
How do @OneToMany, @ManyToOne and @ManyToMany map onto database tables?
@ManyToOne is the owning side: it maps to a foreign-key column on the child's own table. @OneToMany is normally its inverse and must say mappedBy — the same foreign key, just read from the parent end; leave mappedBy out and Hibernate silently adds a join table. @ManyToMany always needs a join table of two foreign keys. Only the owning side is written on flush, which is why you must set both ends in Java.
Common mistakes
- ✗Omitting
mappedByon@OneToManyand getting an unwanted join table - ✗Setting only one end of a bidirectional link in Java and expecting the foreign key to be written
- ✗Believing
@ManyToOneneeds a join table of its own
Follow-up questions
- →Why does setting only the inverse side leave the foreign-key column unchanged?
- →When is a join table preferable to a foreign key for a one-to-many link?
SeniorDesignRareYour team must split the customer.full_name column of a Hibernate-mapped entity into first_name and last_name. The service runs on eight instances behind a load balancer and is deployed by a rolling restart that takes about twenty minutes, so old and new instances serve traffic side by side the whole time. The table holds forty million rows, the database is a single primary, Flyway runs the migration during application startup, and Hibernate is configured with ddl-auto: validate — so an instance refuses to boot if the schema does not match its entity mapping. There is no maintenance window: writes never stop. Walk through the sequence of schema changes and code releases you would use, what each step is allowed to assume about the versions still running, how the forty million existing rows get their new columns filled, and how you would roll back if the new release turns out to be broken after the backfill has already started.
Your team must split the customer.full_name column of a Hibernate-mapped entity into first_name and last_name. The service runs on eight instances behind a load balancer and is deployed by a rolling restart that takes about twenty minutes, so old and new instances serve traffic side by side the whole time. The table holds forty million rows, the database is a single primary, Flyway runs the migration during application startup, and Hibernate is configured with ddl-auto: validate — so an instance refuses to boot if the schema does not match its entity mapping. There is no maintenance window: writes never stop. Walk through the sequence of schema changes and code releases you would use, what each step is allowed to assume about the versions still running, how the forty million existing rows get their new columns filled, and how you would roll back if the new release turns out to be broken after the backfill has already started.
Never change schema and code in one step. Ship a migration that only ADDs the nullable first_name/last_name: additive DDL still validates against the old version. Then release code that dual-writes both shapes while reading full_name. Backfill in bounded batches outside the deploy, so no statement holds a long lock. Only then flip reads over, and drop full_name much later. Each step is revertible: the schema always satisfies both versions running.
Common mistakes
- ✗Coupling the DDL and the code change into one release, so the old instances see a schema they cannot validate against
- ✗Backfilling forty million rows with a single
UPDATEinside the startup migration, which locks the table and stalls the deploy - ✗Dropping the old column in the same release that stops reading it, leaving no version-safe rollback
Follow-up questions
- →How would you verify the backfill converged before flipping reads to the new columns?
- →What changes if the same migration must also run against a read replica with lag?