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:
@@ -0,0 +1,275 @@
|
||||
package com.ankurm.hibernatedemo.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaDelete;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.CriteriaUpdate;
|
||||
import jakarta.persistence.criteria.Join;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.hibernate.stat.Statistics;
|
||||
import org.hibernate.SessionFactory;
|
||||
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 4880 (Criteria API). Docs: docs/16-criteria-queries.md.
|
||||
*
|
||||
* <p>Shares {@link Employee}/{@link Department} with {@link HqlQueryTest}. The static metamodel
|
||||
* classes ({@link Employee_}, {@link Department_}) are real, generated by {@code
|
||||
* hibernate-jpamodelgen} at build time (see the {@code annotationProcessorPaths} entry in
|
||||
* pom.xml) -- not hand-written stand-ins.
|
||||
*
|
||||
* <p>Run with {@code ./mvnw -Dtest=CriteriaQueryTest test}.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class CriteriaQueryTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private Long engineeringId;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
em.createQuery("DELETE FROM Employee").executeUpdate();
|
||||
em.createQuery("DELETE FROM QueryDept").executeUpdate();
|
||||
Department engineering = new Department("Engineering");
|
||||
Department marketing = new Department("Marketing");
|
||||
em.persist(engineering);
|
||||
em.persist(marketing);
|
||||
em.persist(new Employee("Ada", "Byron", 95_000.0, "ACTIVE", LocalDate.of(2019, 3, 1), engineering));
|
||||
em.persist(new Employee("Grace", "Hopper", 98_000.0, "ACTIVE", LocalDate.of(2018, 6, 15), engineering));
|
||||
em.persist(new Employee("Linus", "Torvalds", 92_000.0, "INACTIVE", LocalDate.of(2015, 1, 10), engineering));
|
||||
em.persist(new Employee("Margaret", "Hamilton", 72_000.0, "ACTIVE", LocalDate.of(2020, 9, 1), marketing));
|
||||
em.persist(new Employee("Katherine", "Johnson", 71_000.0, "ACTIVE", LocalDate.of(2021, 2, 20), marketing));
|
||||
em.getTransaction().commit();
|
||||
engineeringId = engineering.getId();
|
||||
em.close();
|
||||
}
|
||||
|
||||
private Statistics stats() {
|
||||
return emf.unwrap(SessionFactory.class).getStatistics();
|
||||
}
|
||||
|
||||
@Test
|
||||
void basicSelection_returnsEveryRow() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr = cb.createQuery(Employee.class);
|
||||
Root<Employee> root = cr.from(Employee.class);
|
||||
cr.select(root);
|
||||
|
||||
List<Employee> results = em.createQuery(cr).getResultList();
|
||||
assertThat(results).hasSize(5);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringPathPredicates_andCombined_filterCorrectly() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr = cb.createQuery(Employee.class);
|
||||
Root<Employee> root = cr.from(Employee.class);
|
||||
|
||||
Predicate salaryGreater = cb.gt(root.get("salary"), 90_000.0);
|
||||
Predicate nameLike = cb.like(root.get("lastName"), "%o%");
|
||||
|
||||
cr.select(root).where(cb.and(salaryGreater, nameLike)).orderBy(cb.asc(root.get("lastName")));
|
||||
|
||||
List<Employee> filtered = em.createQuery(cr).getResultList();
|
||||
// salary > 90000 keeps Byron/Hopper/Torvalds; lastName LIKE '%o%' does NOT narrow that
|
||||
// set further -- "Byron", "Hopper" and "Torvalds" all contain an 'o'.
|
||||
assertThat(filtered).extracting(Employee::getLastName).containsExactly("Byron", "Hopper", "Torvalds");
|
||||
DEMO.info("stringPathPredicates: {}", filtered.stream().map(Employee::getLastName).toList());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void staticMetamodel_isTypeSafeAndProducesTheSameResultAsStringPath() {
|
||||
// The same query as above, but built through the generated Employee_ metamodel
|
||||
// instead of root.get("salary") string paths. Proves the metamodel classes are real
|
||||
// and functional, not just present on the classpath.
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr = cb.createQuery(Employee.class);
|
||||
Root<Employee> root = cr.from(Employee.class);
|
||||
|
||||
cr.select(root)
|
||||
.where(cb.gt(root.get(Employee_.salary), 90_000.0))
|
||||
.orderBy(cb.asc(root.get(Employee_.lastName)));
|
||||
|
||||
List<Employee> results = em.createQuery(cr).getResultList();
|
||||
assertThat(results).extracting(Employee::getLastName).containsExactly("Byron", "Hopper", "Torvalds");
|
||||
DEMO.info("staticMetamodel: {}", results.stream().map(Employee::getLastName).toList());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinViaMetamodel_filtersByRelatedEntity() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr = cb.createQuery(Employee.class);
|
||||
Root<Employee> root = cr.from(Employee.class);
|
||||
|
||||
Join<Employee, Department> department = root.join(Employee_.department, JoinType.INNER);
|
||||
cr.select(root).where(cb.equal(department.get(Department_.name), "Engineering"));
|
||||
|
||||
List<Employee> engineeringStaff = em.createQuery(cr).getResultList();
|
||||
assertThat(engineeringStaff).hasSize(3);
|
||||
DEMO.info("joinViaMetamodel: {} engineering employees", engineeringStaff.size());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rootJoin_doesNotForceEagerLoad_rootFetchDoes() {
|
||||
// root.join() builds a SQL join for filtering but does NOT initialize the association
|
||||
// for the returned entities -- touching it afterward is a separate SELECT per entity,
|
||||
// the same N+1 risk as HQL's plain JOIN. root.fetch() actually populates it.
|
||||
EntityManager em1 = emf.createEntityManager();
|
||||
CriteriaBuilder cb1 = em1.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr1 = cb1.createQuery(Employee.class);
|
||||
Root<Employee> root1 = cr1.from(Employee.class);
|
||||
root1.join(Employee_.department, JoinType.INNER);
|
||||
cr1.select(root1);
|
||||
stats().clear();
|
||||
List<Employee> viaJoin = em1.createQuery(cr1).getResultList();
|
||||
long afterSelect = stats().getPrepareStatementCount();
|
||||
viaJoin.forEach(e -> e.getDepartment().getName());
|
||||
long afterTouch = stats().getPrepareStatementCount();
|
||||
assertThat(afterTouch).as("root.join() alone still requires extra SELECTs to read the association")
|
||||
.isGreaterThan(afterSelect);
|
||||
em1.close();
|
||||
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
CriteriaBuilder cb2 = em2.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr2 = cb2.createQuery(Employee.class);
|
||||
Root<Employee> root2 = cr2.from(Employee.class);
|
||||
root2.fetch(Employee_.department, JoinType.INNER);
|
||||
cr2.select(root2).distinct(true);
|
||||
stats().clear();
|
||||
List<Employee> viaFetch = em2.createQuery(cr2).getResultList();
|
||||
viaFetch.forEach(e -> e.getDepartment().getName());
|
||||
long totalWithFetch = stats().getPrepareStatementCount();
|
||||
assertThat(totalWithFetch).as("root.fetch() loads the association in the same SELECT")
|
||||
.isEqualTo(1L);
|
||||
DEMO.info("rootJoinVsFetch: join+touch={} statements, fetch+touch={} statement", afterTouch, totalWithFetch);
|
||||
em2.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregation_avgSalary_matchesHandComputedValue() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Double> avgQuery = cb.createQuery(Double.class);
|
||||
Root<Employee> avgRoot = avgQuery.from(Employee.class);
|
||||
avgQuery.select(cb.avg(avgRoot.get(Employee_.salary)));
|
||||
|
||||
Double averageSalary = em.createQuery(avgQuery).getSingleResult();
|
||||
double expected = (95_000.0 + 98_000.0 + 92_000.0 + 72_000.0 + 71_000.0) / 5;
|
||||
assertThat(averageSalary).isCloseTo(expected, org.assertj.core.data.Offset.offset(0.01));
|
||||
DEMO.info("aggregation: average salary = {}", averageSalary);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subquery_findsAboveAverageEarners() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> mainQuery = cb.createQuery(Employee.class);
|
||||
Root<Employee> empRoot = mainQuery.from(Employee.class);
|
||||
|
||||
Subquery<Double> sub = mainQuery.subquery(Double.class);
|
||||
Root<Employee> subRoot = sub.from(Employee.class);
|
||||
sub.select(cb.avg(subRoot.get(Employee_.salary)));
|
||||
|
||||
mainQuery.select(empRoot).where(cb.gt(empRoot.get(Employee_.salary), sub)).orderBy(cb.asc(empRoot.get(Employee_.lastName)));
|
||||
|
||||
List<Employee> highEarners = em.createQuery(mainQuery).getResultList();
|
||||
// Company-wide average is 85600 (428000/5) -- the two Marketing salaries (72000,
|
||||
// 71000) drag it well below every Engineering salary, so all three Engineering
|
||||
// employees clear it, not just the single highest earner.
|
||||
assertThat(highEarners).extracting(Employee::getLastName).containsExactly("Byron", "Hopper", "Torvalds");
|
||||
DEMO.info("subquery: above-average earners (avg=85600) = {}", highEarners.stream().map(Employee::getLastName).toList());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void criteriaUpdate_bulkRaisesSalaryForOneDepartment() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
|
||||
CriteriaUpdate<Employee> update = cb.createCriteriaUpdate(Employee.class);
|
||||
Root<Employee> updRoot = update.from(Employee.class);
|
||||
update.set(Employee_.salary, cb.prod(updRoot.get(Employee_.salary), 1.1))
|
||||
.where(cb.equal(updRoot.get(Employee_.department).get(Department_.id), engineeringId));
|
||||
|
||||
int updated = em.createQuery(update).executeUpdate();
|
||||
assertThat(updated).isEqualTo(3);
|
||||
|
||||
em.clear();
|
||||
Double newAdaSalary = em.createQuery("SELECT e.salary FROM Employee e WHERE e.lastName = 'Byron'", Double.class)
|
||||
.getSingleResult();
|
||||
assertThat(newAdaSalary).isCloseTo(95_000.0 * 1.1, org.assertj.core.data.Offset.offset(0.01));
|
||||
em.getTransaction().commit();
|
||||
DEMO.info("criteriaUpdate: {} rows updated, Ada's new salary = {}", updated, newAdaSalary);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void criteriaDelete_removesEmployeesWithNoDepartment() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
// Insert one departmentless employee to have something real for CriteriaDelete to remove.
|
||||
em.persist(new Employee("Orphan", "NoDept", 50_000.0, "ACTIVE", LocalDate.now(), null));
|
||||
em.flush();
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<Employee> delete = cb.createCriteriaDelete(Employee.class);
|
||||
Root<Employee> delRoot = delete.from(Employee.class);
|
||||
delete.where(cb.isNull(delRoot.get(Employee_.department)));
|
||||
|
||||
int deleted = em.createQuery(delete).executeUpdate();
|
||||
assertThat(deleted).isEqualTo(1);
|
||||
|
||||
long remaining = em.createQuery("SELECT COUNT(e) FROM Employee e", Long.class).getSingleResult();
|
||||
assertThat(remaining).isEqualTo(5L);
|
||||
em.getTransaction().commit();
|
||||
DEMO.info("criteriaDelete: deleted={}, remaining={}", deleted, remaining);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void orPredicate_combinesConditionsWithVarargsOverload() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Employee> cr = cb.createQuery(Employee.class);
|
||||
Root<Employee> root = cr.from(Employee.class);
|
||||
|
||||
cr.select(root).where(cb.or(
|
||||
cb.equal(root.get(Employee_.status), "INACTIVE"),
|
||||
cb.equal(root.get(Employee_.lastName), "Hamilton")));
|
||||
|
||||
List<Employee> results = em.createQuery(cr).getResultList();
|
||||
assertThat(results).extracting(Employee::getLastName).containsExactlyInAnyOrder("Torvalds", "Hamilton");
|
||||
DEMO.info("orPredicate: {}", results.stream().map(Employee::getLastName).toList());
|
||||
em.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.ankurm.hibernatedemo.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.query.SemanticException;
|
||||
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 4879 (HQL). Docs: docs/15-hql-queries.md.
|
||||
*
|
||||
* <p>Run with {@code ./mvnw -Dtest=HqlQueryTest test}. Every assertion here was first observed by
|
||||
* running the same query and reading the log, then pinned down as an assertion.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class HqlQueryTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private Long engineeringId;
|
||||
private Long marketingId;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
// This method-scoped H2 database (jdbc:h2:mem:hibernate-demo) is shared across every
|
||||
// test in the class run, not reset between tests, so each test must clear its own
|
||||
// tables first -- otherwise counts creep upward test by test.
|
||||
em.createQuery("DELETE FROM Employee").executeUpdate();
|
||||
em.createQuery("DELETE FROM QueryDept").executeUpdate();
|
||||
Department engineering = new Department("Engineering");
|
||||
Department marketing = new Department("Marketing");
|
||||
em.persist(engineering);
|
||||
em.persist(marketing);
|
||||
em.persist(new Employee("Ada", "Byron", 95_000.0, "ACTIVE", LocalDate.of(2019, 3, 1), engineering));
|
||||
em.persist(new Employee("Grace", "Hopper", 98_000.0, "ACTIVE", LocalDate.of(2018, 6, 15), engineering));
|
||||
em.persist(new Employee("Linus", "Torvalds", 92_000.0, "INACTIVE", LocalDate.of(2015, 1, 10), engineering));
|
||||
em.persist(new Employee("Margaret", "Hamilton", 72_000.0, "ACTIVE", LocalDate.of(2020, 9, 1), marketing));
|
||||
em.persist(new Employee("Katherine", "Johnson", 71_000.0, "ACTIVE", LocalDate.of(2021, 2, 20), marketing));
|
||||
em.getTransaction().commit();
|
||||
engineeringId = engineering.getId();
|
||||
marketingId = marketing.getId();
|
||||
em.close();
|
||||
}
|
||||
|
||||
private Statistics stats() {
|
||||
return emf.unwrap(SessionFactory.class).getStatistics();
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAll_returnsEveryRow() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
List<Employee> all = em.createQuery("FROM Employee", Employee.class).getResultList();
|
||||
assertThat(all).hasSize(5);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whereWithNamedParameter_filtersCorrectly() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
List<Employee> active = em.createQuery(
|
||||
"SELECT e FROM Employee e WHERE e.status = :status", Employee.class)
|
||||
.setParameter("status", "ACTIVE")
|
||||
.getResultList();
|
||||
assertThat(active).hasSize(4).allMatch(e -> "ACTIVE".equals(e.getStatus()));
|
||||
DEMO.info("whereWithNamedParameter: {} active employees", active.size());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void columnNameInsteadOfFieldName_failsAtQueryTimeNotSilently() {
|
||||
// The classic HQL pitfall: writing the database column name (first_name) instead of
|
||||
// the entity field name (firstName). This is NOT a silent wrong-result bug -- HQL
|
||||
// resolves against the entity model, so an unknown path fails loudly before any SQL
|
||||
// is sent.
|
||||
EntityManager em = emf.createEntityManager();
|
||||
assertThatThrownBy(() -> em.createQuery("SELECT e FROM Employee e WHERE e.first_name = 'Ada'", Employee.class))
|
||||
.isInstanceOfAny(IllegalArgumentException.class, SemanticException.class)
|
||||
.satisfies(ex -> DEMO.info("columnNameInsteadOfFieldName: {}: {}", ex.getClass().getSimpleName(), ex.getMessage()));
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinWithoutFetch_causesOneQueryPerDepartmentAccess() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
stats().clear();
|
||||
List<Employee> withDept = em.createQuery(
|
||||
"SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName", Employee.class)
|
||||
.setParameter("deptName", "Engineering")
|
||||
.getResultList();
|
||||
long afterQuery = stats().getPrepareStatementCount();
|
||||
assertThat(afterQuery).as("plain JOIN issues one SELECT for the employees").isEqualTo(1);
|
||||
// Touching the association now triggers a SEPARATE select per distinct department --
|
||||
// this is the N+1 that JOIN alone (without FETCH) does not prevent.
|
||||
withDept.forEach(e -> e.getDepartment().getName());
|
||||
long afterTouch = stats().getPrepareStatementCount();
|
||||
assertThat(afterTouch).as("touching the lazy association after a plain JOIN fires extra SELECTs")
|
||||
.isGreaterThan(afterQuery);
|
||||
DEMO.info("joinWithoutFetch: {} statements for the query, {} after touching department", afterQuery, afterTouch);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void joinFetch_loadsAssociationInOneQuery() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
stats().clear();
|
||||
List<Employee> withFetch = em.createQuery(
|
||||
"SELECT e FROM Employee e LEFT JOIN FETCH e.department", Employee.class)
|
||||
.getResultList();
|
||||
withFetch.forEach(e -> e.getDepartment().getName());
|
||||
long total = stats().getPrepareStatementCount();
|
||||
assertThat(total).as("JOIN FETCH loads employees AND departments in exactly one SELECT, even after touching department")
|
||||
.isEqualTo(1);
|
||||
DEMO.info("joinFetch: {} statement total, {} rows", total, withFetch.size());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregateCount_matchesSeedSize() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
Long count = em.createQuery("SELECT COUNT(e) FROM Employee e", Long.class).getSingleResult();
|
||||
assertThat(count).isEqualTo(5L);
|
||||
DEMO.info("aggregateCount: {}", count);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void avgSalaryGroupByDepartment_matchesHandComputedAverages() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
List<Object[]> rows = em.createQuery(
|
||||
"SELECT d.name, AVG(e.salary) FROM Employee e JOIN e.department d GROUP BY d.name ORDER BY d.name",
|
||||
Object[].class)
|
||||
.getResultList();
|
||||
assertThat(rows).hasSize(2);
|
||||
assertThat((String) rows.get(0)[0]).isEqualTo("Engineering");
|
||||
assertThat((Double) rows.get(0)[1]).isCloseTo((95_000.0 + 98_000.0 + 92_000.0) / 3, org.assertj.core.data.Offset.offset(0.01));
|
||||
assertThat((String) rows.get(1)[0]).isEqualTo("Marketing");
|
||||
assertThat((Double) rows.get(1)[1]).isCloseTo((72_000.0 + 71_000.0) / 2, org.assertj.core.data.Offset.offset(0.01));
|
||||
rows.forEach(r -> DEMO.info("avgSalaryGroupByDepartment: {} -> {}", r[0], r[1]));
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagination_returnsCorrectSlice() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
List<Employee> page1 = em.createQuery("FROM Employee e ORDER BY e.lastName", Employee.class)
|
||||
.setFirstResult(0)
|
||||
.setMaxResults(2)
|
||||
.getResultList();
|
||||
List<Employee> page2 = em.createQuery("FROM Employee e ORDER BY e.lastName", Employee.class)
|
||||
.setFirstResult(2)
|
||||
.setMaxResults(2)
|
||||
.getResultList();
|
||||
assertThat(page1).hasSize(2);
|
||||
assertThat(page2).hasSize(2);
|
||||
assertThat(page1).extracting(Employee::getLastName).doesNotContainAnyElementsOf(
|
||||
page2.stream().map(Employee::getLastName).toList());
|
||||
DEMO.info("pagination: page1={}, page2={}", page1.stream().map(Employee::getLastName).toList(),
|
||||
page2.stream().map(Employee::getLastName).toList());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkUpdate_changesRowsButNotAlreadyLoadedEntityInSameSession() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
// Load one INACTIVE-bound employee into the persistence context first.
|
||||
Employee loaded = em.createQuery(
|
||||
"SELECT e FROM Employee e WHERE e.lastName = 'Torvalds'", Employee.class)
|
||||
.getSingleResult();
|
||||
assertThat(loaded.getStatus()).isEqualTo("INACTIVE");
|
||||
|
||||
int updated = em.createQuery(
|
||||
"UPDATE Employee e SET e.status = 'ARCHIVED' WHERE e.status = 'INACTIVE'")
|
||||
.executeUpdate();
|
||||
assertThat(updated).isEqualTo(1);
|
||||
|
||||
// The row changed in the database, but the already-loaded managed entity was never
|
||||
// touched by the bulk UPDATE -- it still shows the old in-memory value until refreshed.
|
||||
assertThat(loaded.getStatus()).as("bulk UPDATE bypasses the persistence context for already-loaded entities")
|
||||
.isEqualTo("INACTIVE");
|
||||
|
||||
em.clear();
|
||||
Employee reloaded = em.find(Employee.class, loaded.getId());
|
||||
assertThat(reloaded.getStatus()).as("a fresh load after clear() sees the bulk UPDATE").isEqualTo("ARCHIVED");
|
||||
em.getTransaction().commit();
|
||||
DEMO.info("bulkUpdate: updated={} rows, stale in-memory status={}, reloaded status={}",
|
||||
updated, loaded.getStatus(), reloaded.getStatus());
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkDelete_removesMatchingRowsOnly() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
int deleted = em.createQuery("DELETE FROM Employee e WHERE e.department.id = :deptId")
|
||||
.setParameter("deptId", marketingId)
|
||||
.executeUpdate();
|
||||
assertThat(deleted).isEqualTo(2);
|
||||
long remaining = em.createQuery("SELECT COUNT(e) FROM Employee e", Long.class).getSingleResult();
|
||||
assertThat(remaining).isEqualTo(3L);
|
||||
em.getTransaction().commit();
|
||||
DEMO.info("bulkDelete: deleted={} rows, remaining={}", deleted, remaining);
|
||||
em.close();
|
||||
}
|
||||
|
||||
// ---- Flush-mode tests use an UPDATE to an already-managed entity, not a new insert.
|
||||
// An earlier version of these tests persisted a brand-new Employee and expected FlushMode
|
||||
// to control when it became visible -- that was confounded by GenerationType.IDENTITY
|
||||
// (chapter 03's own finding: IDENTITY forces an immediate INSERT to obtain the generated
|
||||
// key, on every persist(), regardless of flush mode). A dirty UPDATE on a loaded managed
|
||||
// entity is unaffected by id-generation strategy and isolates what flush mode actually
|
||||
// controls.
|
||||
|
||||
@Test
|
||||
void defaultFlushMode_autoFlushesDirtyUpdateBeforeQuery() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Employee ada = em.createQuery("SELECT e FROM Employee e WHERE e.lastName = 'Byron'", Employee.class).getSingleResult();
|
||||
ada.setSalary(999_999.0); // dirtied, not explicitly flushed
|
||||
Double salaryFromFreshQuery = em.createQuery(
|
||||
"SELECT e.salary FROM Employee e WHERE e.lastName = 'Byron'", Double.class)
|
||||
.getSingleResult();
|
||||
assertThat(salaryFromFreshQuery).as("default AUTO flush mode flushes the dirty UPDATE before the query runs")
|
||||
.isEqualTo(999_999.0);
|
||||
em.getTransaction().rollback();
|
||||
DEMO.info("defaultFlushMode: salary seen by a fresh query after an unflushed dirty change = {}", salaryFromFreshQuery);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void jakartaCommitFlushMode_suppressesAutoFlushForThisQuery() {
|
||||
// jakarta.persistence.FlushModeType.COMMIT's javadoc only says a provider is
|
||||
// "permitted, but not required" to flush before a query -- it's implementation
|
||||
// defined. Hibernate 7.4.5's actual choice, verified here: it does NOT flush, so the
|
||||
// query sees the pre-update value.
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Employee grace = em.createQuery("SELECT e FROM Employee e WHERE e.lastName = 'Hopper'", Employee.class).getSingleResult();
|
||||
grace.setSalary(111_111.0);
|
||||
Double salaryFromFreshQuery = em.createQuery(
|
||||
"SELECT e.salary FROM Employee e WHERE e.lastName = 'Hopper'", Double.class)
|
||||
.setFlushMode(jakarta.persistence.FlushModeType.COMMIT)
|
||||
.getSingleResult();
|
||||
assertThat(salaryFromFreshQuery).as("Hibernate 7.4.5 chooses not to flush under FlushModeType.COMMIT -- the query still sees the old value")
|
||||
.isEqualTo(98_000.0);
|
||||
em.getTransaction().rollback();
|
||||
DEMO.info("jakartaCommitFlushMode: salary seen by query under FlushModeType.COMMIT = {} (pre-update value was 98000.0)", salaryFromFreshQuery);
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nativeManualFlushMode_actuallySuppressesAutoFlush() {
|
||||
// Hibernate's own native org.hibernate.FlushMode has a fourth value the jakarta enum
|
||||
// doesn't: MANUAL, which means never auto-flush, full stop -- the one that genuinely
|
||||
// suppresses it, matching the "set FlushMode.MANUAL for performance-sensitive loops"
|
||||
// advice, as long as what you're deferring is an UPDATE, not an IDENTITY-generated
|
||||
// INSERT (see the note above this block).
|
||||
EntityManager em = emf.createEntityManager();
|
||||
org.hibernate.Session session = em.unwrap(org.hibernate.Session.class);
|
||||
em.getTransaction().begin();
|
||||
Employee linus = em.createQuery("SELECT e FROM Employee e WHERE e.lastName = 'Torvalds'", Employee.class).getSingleResult();
|
||||
session.setHibernateFlushMode(org.hibernate.FlushMode.MANUAL);
|
||||
linus.setSalary(123_123.0);
|
||||
Double beforeFlush = em.createQuery(
|
||||
"SELECT e.salary FROM Employee e WHERE e.lastName = 'Torvalds'", Double.class)
|
||||
.getSingleResult();
|
||||
assertThat(beforeFlush).as("FlushMode.MANUAL genuinely suppresses auto-flush before this query -- the old salary is still what the DB has")
|
||||
.isEqualTo(92_000.0);
|
||||
session.flush();
|
||||
Double afterFlush = em.createQuery(
|
||||
"SELECT e.salary FROM Employee e WHERE e.lastName = 'Torvalds'", Double.class)
|
||||
.getSingleResult();
|
||||
assertThat(afterFlush).as("an explicit flush() makes the change visible").isEqualTo(123_123.0);
|
||||
em.getTransaction().rollback();
|
||||
DEMO.info("nativeManualFlushMode: before explicit flush={}, after={}", beforeFlush, afterFlush);
|
||||
em.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user