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,156 @@
|
||||
package com.ankurm.hibernatedemo.aggregate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.HibernateDemoApplication;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Proves the article's aggregate-function claims against a real H2 database: what an aggregate
|
||||
* query returns over an empty table, {@code select new} record construction under GROUP BY /
|
||||
* HAVING, the Criteria API equivalent, and HQL's {@code row_number()} window function.
|
||||
*
|
||||
* <p>Docs: docs/21-aggregate-functions.md.
|
||||
*/
|
||||
@SpringBootTest(classes = HibernateDemoApplication.class)
|
||||
class AggregateFunctionsTest {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void emptyResultSet_countIsZero_sumAndAvgAreNull_neitherThrows() {
|
||||
// Scoped to a category that matches zero rows -- rather than relying on the whole table
|
||||
// being empty, which would make this test's outcome depend on execution order against
|
||||
// the other test methods in this class that share the same EntityManagerFactory/database.
|
||||
String noSuchCategory = "no-such-category-zzz";
|
||||
EntityManager em = emf.createEntityManager();
|
||||
|
||||
Long count = em.createQuery(
|
||||
"select count(p) from Product p where p.category = :c", Long.class)
|
||||
.setParameter("c", noSuchCategory)
|
||||
.getSingleResult();
|
||||
Double sum = em.createQuery(
|
||||
"select sum(p.price) from Product p where p.category = :c", Double.class)
|
||||
.setParameter("c", noSuchCategory)
|
||||
.getSingleResult();
|
||||
Double avg = em.createQuery(
|
||||
"select avg(p.price) from Product p where p.category = :c", Double.class)
|
||||
.setParameter("c", noSuchCategory)
|
||||
.getSingleResult();
|
||||
|
||||
em.close();
|
||||
|
||||
System.out.println("RESULT[aggregate-empty-result-set]: over 0 matching rows -- "
|
||||
+ "count(p)=" + count + " (never null) | sum(p.price)=" + sum + " | avg(p.price)="
|
||||
+ avg + " -- getSingleResult() returned normally for all three, no "
|
||||
+ "NoResultException, because SQL's aggregate functions over zero rows still "
|
||||
+ "produce exactly one result row.");
|
||||
|
||||
assertThat(count).isZero();
|
||||
assertThat(sum).isNull();
|
||||
assertThat(avg).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupByHaving_selectNewRecord_producesTypedSummaries() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
em.persist(new Product("Keyboards", "Compact 65%", 89.0, 40));
|
||||
em.persist(new Product("Keyboards", "Full-size", 59.0, 15));
|
||||
em.persist(new Product("Keyboards", "Ergonomic split", 149.0, 5));
|
||||
em.persist(new Product("Monitors", "27in 4K", 399.0, 8));
|
||||
em.getTransaction().commit();
|
||||
|
||||
// Scoped to this test's own two categories -- the entity manager factory's H2 database is
|
||||
// shared across every test method in this class, so an unscoped query here would also
|
||||
// pick up Cables/Mice from the other test methods and make this assertion order-dependent.
|
||||
List<CategorySummary> summaries = em.createQuery(
|
||||
"select new com.ankurm.hibernatedemo.aggregate.CategorySummary("
|
||||
+ "p.category, count(p), avg(p.price)) "
|
||||
+ "from Product p where p.category in ('Keyboards', 'Monitors') "
|
||||
+ "group by p.category having count(p) > 1 "
|
||||
+ "order by p.category",
|
||||
CategorySummary.class).getResultList();
|
||||
em.close();
|
||||
|
||||
String rendered = summaries.stream()
|
||||
.map(s -> s.category() + "(count=" + s.productCount() + ", avg=" + s.averagePrice() + ")")
|
||||
.reduce((a, b) -> a + ", " + b)
|
||||
.orElse("<none>");
|
||||
System.out.println("RESULT[aggregate-groupby-having-record]: HAVING count(p) > 1 kept only "
|
||||
+ "categories with more than one product -- " + rendered
|
||||
+ " -- Monitors (1 product) was correctly excluded by HAVING, not just by GROUP BY.");
|
||||
|
||||
assertThat(summaries).hasSize(1);
|
||||
assertThat(summaries.get(0).category()).isEqualTo("Keyboards");
|
||||
assertThat(summaries.get(0).productCount()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void criteriaApi_avgWithGroupBy_matchesHqlEquivalent() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
em.persist(new Product("Cables", "USB-C 1m", 9.0, 200));
|
||||
em.persist(new Product("Cables", "USB-C 2m", 13.0, 120));
|
||||
em.getTransaction().commit();
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
|
||||
Root<Product> root = cq.from(Product.class);
|
||||
cq.multiselect(root.get("category"), cb.avg(root.get("price")))
|
||||
.where(cb.equal(root.get("category"), "Cables"))
|
||||
.groupBy(root.get("category"));
|
||||
|
||||
TypedQuery<Object[]> query = em.createQuery(cq);
|
||||
Object[] row = query.getSingleResult();
|
||||
em.close();
|
||||
|
||||
System.out.println("RESULT[aggregate-criteria-avg]: Criteria API cb.avg(root.get(\"price\")) "
|
||||
+ "for category='" + row[0] + "' -- average=" + row[1]
|
||||
+ " -- same numeric result as the equivalent HQL avg(p.price), just built without a"
|
||||
+ " string query.");
|
||||
|
||||
assertThat((Double) row[1]).isEqualTo(11.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void windowFunction_rowNumberOverPartitionByCategory() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
em.persist(new Product("Mice", "Wireless A", 49.0, 30));
|
||||
em.persist(new Product("Mice", "Wireless B", 79.0, 12));
|
||||
em.persist(new Product("Mice", "Wired C", 19.0, 60));
|
||||
em.getTransaction().commit();
|
||||
|
||||
List<Object[]> ranked = em.createQuery(
|
||||
"select p.name, p.price, "
|
||||
+ "row_number() over (partition by p.category order by p.price desc) "
|
||||
+ "from Product p where p.category = 'Mice' order by p.price desc",
|
||||
Object[].class).getResultList();
|
||||
em.close();
|
||||
|
||||
StringBuilder rendered = new StringBuilder();
|
||||
for (Object[] row : ranked) {
|
||||
rendered.append(row[0]).append("=rank").append(row[2]).append(" ");
|
||||
}
|
||||
System.out.println("RESULT[aggregate-window-row-number]: row_number() over "
|
||||
+ "(partition by category order by price desc) for the Mice category -- "
|
||||
+ rendered.toString().trim()
|
||||
+ " -- HQL's window-function support (the OVER clause), present since Hibernate "
|
||||
+ "6.2 and still current in 7.4.5.Final, not a Hibernate-7-only feature.");
|
||||
|
||||
assertThat(ranked).hasSize(3);
|
||||
assertThat(((Number) ranked.get(0)[2]).intValue()).isEqualTo(1);
|
||||
assertThat(ranked.get(0)[0]).isEqualTo("Wireless B");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user