182 lines
7.3 KiB
Java
Executable File
182 lines
7.3 KiB
Java
Executable File
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();
|
|
}
|
|
}
|