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,181 @@
package com.ankurm.hibernatedemo.association;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.util.List;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
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 "MultipleBagFetchException"
* and "The cartesian-product trap".
*/
@SpringBootTest
class BagFetchTest {
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 BagBookL").executeUpdate();
em.createQuery("DELETE FROM BagAwardL").executeUpdate();
em.createQuery("DELETE FROM BagAuthorList").executeUpdate();
em.createQuery("DELETE FROM BagBookS").executeUpdate();
em.createQuery("DELETE FROM BagAwardS").executeUpdate();
em.createQuery("DELETE FROM BagAuthorSet").executeUpdate();
em.getTransaction().commit();
em.close();
}
private Statistics stats() {
return emf.unwrap(SessionFactory.class).getStatistics();
}
private Long seedListAuthor(int nBooks, int nAwards) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
BagAuthorList author = new BagAuthorList("List Author");
em.persist(author);
for (int i = 0; i < nBooks; i++) {
em.persist(new BagBookL("Book" + i, author));
}
for (int i = 0; i < nAwards; i++) {
em.persist(new BagAwardL("Award" + i, author));
}
em.getTransaction().commit();
Long id = author.getId();
em.close();
return id;
}
private Long seedSetAuthor(int nBooks, int nAwards) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
BagAuthorSet author = new BagAuthorSet("Set Author");
em.persist(author);
for (int i = 0; i < nBooks; i++) {
em.persist(new BagBookS("Book" + i, author));
}
for (int i = 0; i < nAwards; i++) {
em.persist(new BagAwardS("Award" + i, author));
}
em.getTransaction().commit();
Long id = author.getId();
em.close();
return id;
}
@Test
void fetchJoiningTwoListsInOneQuery_throwsMultipleBagFetchException() {
seedListAuthor(4, 3);
EntityManager em = emf.createEntityManager();
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
em.createQuery(
"SELECT a FROM BagAuthorList a JOIN FETCH a.books JOIN FETCH a.awards",
BagAuthorList.class).getResultList());
Throwable root = ex;
while (root.getCause() != null && root.getCause() != root) {
root = root.getCause();
}
DEMO.info("MultipleBagFetchException reproduction -- wrapper class: {}", ex.getClass().getName());
DEMO.info("MultipleBagFetchException reproduction -- root cause class: {}", root.getClass().getName());
DEMO.info("MultipleBagFetchException reproduction -- verbatim message: {}", root.getMessage());
assertThat(ex.getClass().getName()).as("EntityManager.createQuery(...).getResultList() wraps it as IllegalArgumentException, NOT PersistenceException")
.isEqualTo("java.lang.IllegalArgumentException");
assertThat(root.getClass().getName()).isEqualTo("org.hibernate.loader.MultipleBagFetchException");
em.close();
}
@Test
void fix1_useSetsInsteadOfLists_noExceptionOneQuery() {
seedSetAuthor(4, 3);
EntityManager em = emf.createEntityManager();
stats().clear();
List<BagAuthorSet> authors = em.createQuery(
"SELECT DISTINCT a FROM BagAuthorSet a JOIN FETCH a.books JOIN FETCH a.awards",
BagAuthorSet.class)
.getResultList();
long queries = stats().getPrepareStatementCount();
DEMO.info("Fix #1 (Set instead of List): {} distinct authors returned, {} queries fired", authors.size(), queries);
assertThat(authors).hasSize(1);
assertThat(authors.get(0).getBooks()).hasSize(4);
assertThat(authors.get(0).getAwards()).hasSize(3);
assertThat(queries).isEqualTo(1);
em.close();
}
@Test
void fix2_twoSeparateQueries_avoidsBagFetchExceptionEntirely() {
Long id = seedListAuthor(4, 3);
EntityManager em = emf.createEntityManager();
stats().clear();
BagAuthorList author = em.createQuery(
"SELECT a FROM BagAuthorList a JOIN FETCH a.books WHERE a.id = :id", BagAuthorList.class)
.setParameter("id", id)
.getSingleResult();
// second, separate query for the other bag -- no exception because only one JOIN FETCH per query
author = em.createQuery(
"SELECT a FROM BagAuthorList a JOIN FETCH a.awards WHERE a.id = :id", BagAuthorList.class)
.setParameter("id", id)
.getSingleResult();
long queries = stats().getPrepareStatementCount();
DEMO.info("Fix #2 (two queries): {} queries fired, books={}, awards={}",
queries, author.getBooks().size(), author.getAwards().size());
assertThat(author.getBooks()).hasSize(4);
assertThat(author.getAwards()).hasSize(3);
assertThat(queries).as("one JOIN FETCH per query, run twice").isEqualTo(2);
em.close();
}
@Test
void cartesianProduct_fetchJoiningTwoAllowedSetsExplodesRowCount() {
seedSetAuthor(4, 3);
EntityManager em = emf.createEntityManager();
// Raw SQL join without DISTINCT to see the true row count the database returns.
Object rawCount = em.createNativeQuery(
"SELECT COUNT(*) FROM bag_author_set a " +
"JOIN bag_book_s b ON b.author_id = a.id " +
"JOIN bag_award_s w ON w.author_id = a.id")
.getSingleResult();
long rawRowCount = ((Number) rawCount).longValue();
List<BagAuthorSet> entities = em.createQuery(
"SELECT DISTINCT a FROM BagAuthorSet a JOIN FETCH a.books JOIN FETCH a.awards",
BagAuthorSet.class)
.getResultList();
DEMO.info("Cartesian product: 4 books x 3 awards for 1 author -> raw SQL join rows = {}, distinct entities returned = {}",
rawRowCount, entities.size());
assertThat(rawRowCount).as("SQL returns one row per (book, award) pair: 4 x 3").isEqualTo(12);
assertThat(entities).as("Hibernate's DISTINCT root-entity de-duplication collapses this back to 1 entity")
.hasSize(1);
assertThat(entities.get(0).getBooks()).hasSize(4);
assertThat(entities.get(0).getAwards()).hasSize(3);
em.close();
}
}