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. * *

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 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(""); 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 cq = cb.createQuery(Object[].class); Root 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 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 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"); } }