← All cheat sheets ·

JPA & Hibernate Pitfalls

Entity states, fetching, N+1 fixes, flushing, optimistic locking, cascades, equals/hashCode, batching and LazyInitializationException.

Entity states

State In persistence context? Has DB row? How to get there
Transient (new) no no new Entity()
Managed (persistent) yes yes/pending persist, find, query results, merge return value
Detached no yes after close(), clear(), detach(), tx end (non-extended context)
Removed yes (scheduled delete) until flush remove(managed)
  • persist makes the passed instance managed; merge copies state onto a managed instance and returns that copy (the argument stays detached).
  • Managed entities are dirty-checked at flush; no save() call needed for updates inside a transaction.
  • Spring Data save(): persist if isNew() (null id / null @Version), otherwise merge (extra SELECT). With assigned ids, implement Persistable to avoid the SELECT.
  • getReference() / getReferenceById() returns a lazy proxy without hitting the DB.

Fetch types

Association Default Note
@ManyToOne, @OneToOne EAGER set fetch = LAZY explicitly
@OneToMany, @ManyToMany LAZY
@Basic EAGER LAZY needs bytecode enhancement
  • EAGER is not a join guarantee: find() uses a join, JPQL/Criteria queries issue extra selects (N+1).
  • Lazy @OneToOne on the non-owning side (mappedBy) cannot be proxied without enhancement; it loads eagerly.
  • Prefer fetch = LAZY everywhere and fetch per use case (join fetch, entity graph, projection).
  • @ManyToMany: use Set, not List (a List bag deletes and reinserts all rows on change).

N+1 problem and fixes

Symptom: 1 query for N parents + N queries for children (visible with logging.level.org.hibernate.SQL=debug).

Fix Example Caveat
JOIN FETCH select o from Order o join fetch o.items duplicates parents in Hibernate 5; Hibernate 6 dedupes automatically; paging with collection fetch is done in memory (HHH000104 warning)
@EntityGraph @EntityGraph(attributePaths = "items") on repository method same paging caveat
@BatchSize(size = 50) / hibernate.default_batch_fetch_size loads children with IN (?,?,...) N+1 becomes N/50+1; safe with pagination
@Fetch(FetchMode.SUBSELECT) one extra query using the original query as subselect whole result set
DTO projection select new com.x.OrderDto(o.id, o.total) from Order o or interface/record projection read-only, no entity management
Two queries page IDs first, then join fetch ... where id in :ids the standard way to page with collection fetch
  • Fetching two List collections with join fetch throws MultipleBagFetchException; use Set, or fetch one per query.
  • LEFT JOIN FETCH when the association may be empty; plain JOIN FETCH is an inner join.
  • Cartesian product: join fetching two collections multiplies rows.

Flush modes and timing

Mode Behaviour
AUTO (default) flush before commit and before a JPQL/HQL query touching affected tables (native queries: Hibernate flushes when it can determine spaces; JPA mode flushes always)
COMMIT flush only at commit; queries may miss pending changes
MANUAL only on explicit flush(); Spring sets it for @Transactional(readOnly = true) with Hibernate
ALWAYS flush before every query
  • flush() sends SQL but does not commit; constraint violations surface at flush time, not at persist.
  • IDENTITY ids force an immediate INSERT on persist and disable JDBC insert batching; use SEQUENCE (with allocationSize matching the DB increment, default 50) for batching.
  • entityManager.clear() after bulk flush in long loops to bound memory.
  • Bulk JPQL update/delete bypasses the persistence context: managed entities go stale; use @Modifying(clearAutomatically = true, flushAutomatically = true).

Optimistic and pessimistic locking

@Entity class Account {
    @Id Long id;
    @Version long version;      // Long/Integer/Short/Instant; never set it manually
}
  • Update emits UPDATE ... WHERE id=? AND version=?; 0 rows -> OptimisticLockException (Spring: ObjectOptimisticLockingFailureException). Retry in a new transaction.
  • Modifications via bulk queries do not bump @Version unless update versioned.
  • Lock modes: OPTIMISTIC (check version at commit), OPTIMISTIC_FORCE_INCREMENT, PESSIMISTIC_READ (FOR SHARE), PESSIMISTIC_WRITE (FOR UPDATE).
  • Spring Data: @Lock(LockModeType.PESSIMISTIC_WRITE); skip locked rows with hint jakarta.persistence.lock.timeout = -2 (SKIP LOCKED).

Cascade and orphanRemoval

Option Effect
CascadeType.PERSIST / MERGE / REMOVE / REFRESH / DETACH / ALL propagate that operation to the associated entities
orphanRemoval = true delete child when removed from the parent's collection or when the reference is nulled
REMOVE on @ManyToOne almost always a bug (deletes shared parents)
  • Cascade only from parent to child in a real composition (Order -> OrderLine); not across independent aggregates.
  • Keep both sides in sync with helper methods; the owning side (the one without mappedBy) writes the FK.
public void addItem(Item i) { items.add(i); i.setOrder(this); }
public void removeItem(Item i) { items.remove(i); i.setOrder(null); }
  • Unidirectional @OneToMany with @JoinColumn is acceptable; without it a join table is created.
  • orphanRemoval and replacing the collection instance (setItems(new ArrayList<>())) throws "A collection with cascade=all-delete-orphan was no longer referenced"; mutate in place.
  • CascadeType.REMOVE / orphanRemoval issue one DELETE per child; use DB ON DELETE CASCADE or bulk delete for large sets.

equals / hashCode

  • Do not use Lombok @Data / @EqualsAndHashCode over all fields on entities (lazy loading, cycles in toString, mutable hash).
  • Safe options: business/natural key, or id-based with constant hash.
@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Order other)) return false;     // instanceof, not getClass(): proxies
    return id != null && id.equals(other.getId());
}
@Override public int hashCode() { return getClass().hashCode(); }   // stable across persist
  • Proxies are subclasses; getClass() comparison fails on them; access fields via getters inside equals.
  • Hash that changes after persist (id assigned) breaks HashSet membership; hence the constant hash or an assigned UUID.

Batching and performance settings

spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.default_batch_fetch_size=50
spring.jpa.properties.hibernate.generate_statistics=true
spring.jpa.open-in-view=false
  • Batching needs SEQUENCE (pooled optimizer) ids, not IDENTITY.
  • PostgreSQL: add reWriteBatchedInserts=true to the JDBC URL; MySQL: rewriteBatchedStatements=true.
  • saveAll of N entities still loads each via merge if ids are assigned; use persist semantics.
  • Read-only queries: @Transactional(readOnly = true) or @QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true")); DTO projections skip dirty-checking.
  • Second-level cache is opt-in (@Cacheable + region provider); query cache rarely worth it.
  • hibernate.ddl-auto=update is not for production; use Flyway/Liquibase and validate.

LazyInitializationException

Cause: lazy association or proxy accessed after the session closed ("could not initialize proxy - no Session").

Fix Notes
Fetch what you need inside the transaction (join fetch, @EntityGraph) preferred
Map to DTO inside the service method preferred
Hibernate.initialize(e.getItems()) explicit, still N+1 prone
spring.jpa.open-in-view=true (Boot default) keeps the session open through view rendering; holds a DB connection for the whole request; disable in production
hibernate.enable_lazy_load_no_trans=true anti-pattern (extra connection per access)
  • Serializing an entity with Jackson triggers lazy loads (or the exception); return DTOs.
  • size() on a lazy collection initializes it; @LazyCollection(EXTRA) is deprecated.
  • Accessing only the id of a lazy @ManyToOne proxy does not load it with property access (field access: use getReference).

Hibernate 6 notes

  • Package jakarta.persistence; HQL is stricter (parsed by an ANTLR 4 grammar); @Type/@TypeDef replaced by @JdbcTypeCode, @JavaType.
  • GenerationType.AUTO now maps to SEQUENCE (named <Entity>_SEQ, allocation size 50) instead of the legacy hilo/table.
  • @GeneratedValue(strategy = UUID) supported natively.
  • Duplicate rows from join fetch are removed automatically; distinct is no longer needed for that purpose.
  • @Embeddable records are supported as of Hibernate 6.2.
  • Pagination with setMaxResults on collection fetch is still in memory.