Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)

This commit is contained in:
2026-09-20 06:06:42 +00:00
committed by Claude
commit 8568c0ce6c
330 changed files with 23668 additions and 0 deletions
@@ -0,0 +1,188 @@
package com.ankurm.hibernatedemo.association;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Backs ankurm.com post 4872, docs/12-association-mappings.md chapters "Cascade and orphanRemoval"
* and "The owning side".
*/
@SpringBootTest
class CascadeOrphanTest {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
@Autowired
private EntityManagerFactory emf;
@BeforeEach
void cleanTables() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.createQuery("DELETE FROM CascadeBook").executeUpdate();
em.createQuery("DELETE FROM CascadeAuthor").executeUpdate();
em.createQuery("DELETE FROM NoOrphanBook").executeUpdate();
em.createQuery("DELETE FROM NoOrphanAuthor").executeUpdate();
em.getTransaction().commit();
em.close();
}
@Test
void cascadeAllPlusOrphanRemoval_reassigningTheCollectionThrowsInsteadOfSilentlyDeleting() {
// CORRECTION vs the common blog claim ("silently deletes what you did not expect"):
// assigning a brand new List instance to a CascadeType.ALL + orphanRemoval=true
// collection does NOT silently delete anything -- Hibernate detects the managed
// collection was dereferenced and throws at flush/commit time instead.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
CascadeAuthor author = new CascadeAuthor("Prolific Author");
em.persist(author);
em.persist(new CascadeBook("Book A", author));
em.persist(new CascadeBook("Book B", author));
em.getTransaction().commit();
Long id = author.getId();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
CascadeAuthor managed = em2.find(CascadeAuthor.class, id);
CascadeBook keep = managed.getBooks().get(0);
managed.replaceBooksWithNewList(new ArrayList<>(List.of(keep))); // assigns a NEW ArrayList
jakarta.persistence.RollbackException ex = org.junit.jupiter.api.Assertions.assertThrows(
jakarta.persistence.RollbackException.class, () -> em2.getTransaction().commit());
Throwable root = ex.getCause();
DEMO.info("cascade=ALL + orphanRemoval=true, reassigning the collection reference -- wrapper: {}", ex.getClass().getName());
DEMO.info("cascade=ALL + orphanRemoval=true, reassigning the collection reference -- root cause: {}: {}",
root.getClass().getName(), root.getMessage());
em2.close();
assertThat(root.getClass().getName()).isEqualTo("org.hibernate.HibernateException");
assertThat(root.getMessage()).contains("no longer referenced by the owning entity instance");
}
@Test
void cascadeAllPlusOrphanRemoval_mutatingTheExistingCollectionDoesDeleteUnexpectedly() {
// The scenario that DOES silently delete: mutating the SAME managed collection instance
// (clear() then re-add the ones to keep) rather than reassigning a new List. This is
// the realistic version of the "developer did not expect this" cascade trap.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
CascadeAuthor author = new CascadeAuthor("Prolific Author 2");
em.persist(author);
em.persist(new CascadeBook("Book A", author));
em.persist(new CascadeBook("Book B", author));
em.persist(new CascadeBook("Book C", author));
em.getTransaction().commit();
Long id = author.getId();
em.close();
EntityManager check1 = emf.createEntityManager();
long before = ((Number) check1.createQuery("SELECT COUNT(b) FROM CascadeBook b WHERE b.author.id = :id")
.setParameter("id", id).getSingleResult()).longValue();
check1.close();
assertThat(before).isEqualTo(3);
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
CascadeAuthor managed = em2.find(CascadeAuthor.class, id);
CascadeBook keep = managed.getBooks().stream().filter(b -> b.getTitle().equals("Book A")).findFirst().orElseThrow();
// Mutate the EXISTING Hibernate-managed collection in place -- this is the pattern that
// actually reaches production: "keep only Book A" implemented via removeIf.
managed.getBooks().removeIf(b -> !b.getTitle().equals("Book A"));
em2.getTransaction().commit();
em2.close();
EntityManager check2 = emf.createEntityManager();
long after = ((Number) check2.createQuery("SELECT COUNT(b) FROM CascadeBook b WHERE b.author.id = :id")
.setParameter("id", id).getSingleResult()).longValue();
DEMO.info("cascade=ALL + orphanRemoval=true, in-place removeIf(): books before={}, books after={}", before, after);
check2.close();
assertThat(after)
.as("orphanRemoval=true DELETEd Book B and Book C as soon as they left the managed collection")
.isEqualTo(1);
}
@Test
void noOrphanRemoval_removingFromCollectionAndFlushingDoesNothingToTheRow() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
NoOrphanAuthor author = new NoOrphanAuthor("Careful Author");
em.persist(author);
NoOrphanBook book1 = new NoOrphanBook("Keep Me", author);
NoOrphanBook book2 = new NoOrphanBook("Remove Me From Collection", author);
em.persist(book1);
em.persist(book2);
em.getTransaction().commit();
Long authorId = author.getId();
Long book2Id = book2.getId();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
NoOrphanAuthor managed = em2.find(NoOrphanAuthor.class, authorId);
managed.getBooks().removeIf(b -> b.getId().equals(book2Id));
em2.getTransaction().commit(); // no orphanRemoval, no cascade REMOVE -- nothing should happen to book2's row
em2.close();
EntityManager check = emf.createEntityManager();
NoOrphanBook stillThere = check.find(NoOrphanBook.class, book2Id);
DEMO.info("orphanRemoval=false: after removing book2 from author.books and flushing, book2 row still exists = {}, author_id still = {}",
stillThere != null, stillThere == null ? null : stillThere.getAuthor().getId());
check.close();
assertThat(stillThere)
.as("without orphanRemoval or cascade REMOVE, removing a child from the collection does not delete or unlink its row")
.isNotNull();
assertThat(stillThere.getAuthor().getId())
.as("its FK is untouched -- still points at the same author")
.isEqualTo(authorId);
}
@Test
void owningSide_mutatingOnlyTheInverseCollectionNeverWritesTheForeignKey() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
NoOrphanAuthor author1 = new NoOrphanAuthor("Author One");
NoOrphanAuthor author2 = new NoOrphanAuthor("Author Two");
em.persist(author1);
em.persist(author2);
NoOrphanBook orphanBook = new NoOrphanBook("Unassigned Book", null);
em.persist(orphanBook);
em.getTransaction().commit();
Long bookId = orphanBook.getId();
Long author2Id = author2.getId();
em.close();
// Mutate ONLY the inverse (mappedBy) side: add the book to author2's collection, but
// never call book.setAuthor(author2) -- the collection is not the owning side.
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
NoOrphanAuthor managedAuthor2 = em2.find(NoOrphanAuthor.class, author2Id);
NoOrphanBook managedBook = em2.find(NoOrphanBook.class, bookId);
managedAuthor2.getBooks().add(managedBook); // inverse side only
em2.getTransaction().commit();
em2.close();
EntityManager check = emf.createEntityManager();
NoOrphanBook reloaded = check.find(NoOrphanBook.class, bookId);
DEMO.info("owning side test: mutated only author2.getBooks().add(book) (inverse side), book.author after flush = {}",
reloaded.getAuthor() == null ? "null (FK not written)" : reloaded.getAuthor().getId());
check.close();
assertThat(reloaded.getAuthor())
.as("mutating only the inverse (mappedBy) collection never persists the association -- the owning side (Book.author) decides the FK")
.isNull();
}
}