Skip to main content

Hibernate 7

JPA Cascade Types in Hibernate 7: A Decision Tree (Because the Wrong One Will Delete Your Data)

A startup ran cascade = CascadeType.ALL on a @ManyToOne User from their Project entity. The reasoning was sensible: they wanted deleting a project to clean everything up automatically. It worked perfectly in testing, where each project belonged to a unique test user. In production, users had multiple projects. When a user deleted their first project, Hibernate cascaded REMOVE to the User entity. The user record was deleted. The user's other projects — with all their data — were gone by FK cascade. The account was gone. Three months of work, in two SQL statements. Support ticket volume spiked. The rollback from backup took four hours. The fix was one annotation change: replace cascade = ALL with cascade = {CascadeType.PERSIST, CascadeType.MERGE}. But understanding why that specific pair — and not ALL, not REMOVE, not nothing — requires a proper decision tree.

Lazy Loading in Hibernate 7: Three Fetch Strategies, Benchmarked, with the One Most Tutorials Get Wrong

Switching from EAGER to LAZY on a @ManyToOne association made one of our REST endpoints 4x slower. That's the opposite of what every Hibernate tutorial promises, and understanding why it happened is more useful than any fetch-type cheatsheet. The endpoint served an order detail page. The Order had a @ManyToOne Customer, which was previously EAGER. Response time was ~80ms. We switched to LAZY (correctly, as a "best practice") and response time jumped to ~320ms. The reason: the EAGER mapping had been loading Customer in the same query as Order via a JOIN. LAZY replaced that with a second SELECT — and the second SELECT ran after the session had been handed to Jackson for serialisation, which triggered OSIV to keep the connection open, added latency from the extra round-trip, and eventually started failing under load when connection pool slots ran out waiting for the serialisation thread to finish. LAZY was still the right answer. The fix was a proper fetch strategy. But "switch everything to LAZY" without understanding the four fetch options and when each one earns its keep is how you trade one performance problem for a different one.

Hibernate 7 In-Memory Databases: H2, HSQLDB and Derby for Testing

A working H2 setup for Hibernate 7 unit tests, then a real side-by-side comparison against HSQLDB and Apache Derby: the Derby dialect class that no longer exists in hibernate-core, the H2 MODE= myth settled with identical DDL, a reserved-word column that fails only on H2, and a second-level-cache trap that silently changes what your test suite is actually testing.

Mastering Hibernate 7 @Immutable Entities: Performance, Safety, and Best Practices

@Immutable is a pure marker annotation that skips Hibernate's dirty-checking snapshot -- mutate a field and flush, and nothing happens: no UPDATE, no exception, just silent data loss of the in-memory change. This rewrite runs every claim against Hibernate 7.4.5 -- entity mutation, bulk HQL (rejected at translation time by default, a correction neither prior version made), collection mutation, @Immutable+@Version, and a real flush-cost measurement -- and compares the annotation head-to-head with Session.setReadOnly().

Hibernate 7 Natural IDs: What L1 and L2 Caching Actually Give You, Measured

Verified against Hibernate 7.4.5: a repeat bySimpleNaturalId lookup in the same session costs zero extra queries with no cache at all, but the bigger correction is that @NaturalIdCache populates its L2 region at insert time, not on first lookup -- so the textbook miss-then-hit story only applies to rows Hibernate didn't insert itself.

Mastering Hibernate 7 JPA Persistence Annotations: The Traps That Pass Code Review

Not another annotation catalogue: this rewrite reproduces the four JPA annotation traps that pass code review and fail in production in Hibernate 7.4.5 -- silent enum ordinal corruption, a JSON column that needs a dependency the starter doesn't provide, an access-strategy mismatch, and the equals/hashCode HashSet bug -- plus what Jakarta Persistence 3.2 actually added.

Hibernate Annotations vs. XML Mappings: Making the Right Choice in Hibernate 7

Hibernate 7.4.5 actually ships three XML mapping dialects, not two, and hbm.xml still boots unmodified with just a warning. This rewrite verifies which format wins on conflict, why xml-mapping-metadata-complete is an all-or-nothing kill switch, and what Hibernate's own mapping.xml dialect can do that JPA's orm.xml cannot.

find() vs getReference() in Hibernate 7: A Decision Matrix (and Why get()/load() Belong in the Same Conversation)

Read this code and predict whether it sends a SELECT to the database: @Transactional public void assignCategory(Long productId, Long categoryId) { Category category = em.getReference(Category.class, categoryId); Product product = em.find(Product.class, productId); product.setCategory(category); } If you said "two SELECTs" — one for each line — you would be half wrong. find() on Product does hit the database. getReference() on Category does not, unless categoryId is already in the first-level cache. The UPDATE to write category_id to the product row happens at flush; the category column only needs the ID, which the proxy already holds. That single avoided SELECT matters in bulk operations. It is also one of the most consistently misunderstood distinctions in Hibernate. This post is a decision guide: six scenarios, each with the right call and the reasoning.