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,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();
}
}