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.