Files
hibernate-demo/docs/21-aggregate-functions.md
T

82 lines
4.2 KiB
Markdown

# 21 — Aggregate functions
[← Previous: 20 — Hibernate Validator CDI integration](20-hibernate-validator-cdi.md) | [Next: 22 — Sorting →](22-sorting.md) | [Back to README →](../README.md)
Backs the rewrite of ankurm.com post 4888 (aggregate functions).
## What an aggregate query returns over zero rows
`count`, `sum`, `avg`, `min`, `max` all still return exactly one result row when the underlying
table (or the filtered subset) has zero matching rows — SQL's aggregate functions are defined
over the whole result set, not per-row, so there is always one row to return. `count(*)` on an
empty set is `0`; every other aggregate is `NULL`. Neither case throws
`NoResultException` — `getSingleResult()` on an aggregate query only throws that when the query
itself is malformed, never because the aggregate happened to be computed over nothing.
```java
Long count = em.createQuery("select count(p) from Product p where p.category = :c", Long.class)
.setParameter("c", "no-such-category-zzz").getSingleResult();
Double sum = em.createQuery("select sum(p.price) from Product p where p.category = :c", Double.class)
.setParameter("c", "no-such-category-zzz").getSingleResult();
```
[`AggregateFunctionsTest.java`](../src/test/java/com/ankurm/hibernatedemo/aggregate/AggregateFunctionsTest.java)
— [captured output](output/21-empty-result-set.txt)
## `select new` with a Java record
Hibernate 7 accepts a Java record's canonical constructor as a `select new` target exactly like
it accepts a class constructor — no special annotation, no adapter, just a record whose
constructor parameter types and order match the query's projection:
```java
public record CategorySummary(String category, long productCount, double averagePrice) {}
```
[`CategorySummary.java`](../src/main/java/com/ankurm/hibernatedemo/aggregate/CategorySummary.java)
```sql
select new com.ankurm.hibernatedemo.aggregate.CategorySummary(p.category, count(p), avg(p.price))
from Product p group by p.category having count(p) > 1 order by p.category
```
`HAVING count(p) > 1` filters on the *aggregated* group, after `GROUP BY` has collapsed the rows
— a category with exactly one product is excluded by `HAVING`, not merely left ungrouped.
[Test](../src/test/java/com/ankurm/hibernatedemo/aggregate/AggregateFunctionsTest.java) —
[output](output/21-groupby-having-record.txt)
## The Criteria API equivalent
`CriteriaBuilder.avg(...)`, `.sum(...)`, `.count(...)` and friends build the same aggregate SQL
without a string query:
```java
cq.multiselect(root.get("category"), cb.avg(root.get("price")))
.where(cb.equal(root.get("category"), "Cables"))
.groupBy(root.get("category"));
```
[Test](../src/test/java/com/ankurm/hibernatedemo/aggregate/AggregateFunctionsTest.java) —
[output](output/21-criteria-avg.txt)
## Window functions — not a Hibernate 7 feature
HQL's `OVER` clause (`row_number()`, `rank()`, `lead()`, `lag()`, and the ordered-set / inverse
distribution aggregates) is real and works exactly as SQL's window functions do. It is **not**
new in Hibernate 7, though it is sometimes described that way: `javap` against
`hibernate-core-7.4.5.Final.jar`'s `CommonFunctionFactory` shows a `windowFunctions()`
registration method that has existed since Hibernate 6.2, when HQL's window-function support was
first added. Hibernate 7 continues to support it; it did not introduce it.
```sql
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
```
[Test](../src/test/java/com/ankurm/hibernatedemo/aggregate/AggregateFunctionsTest.java) —
[output](output/21-window-row-number.txt)
## Going deeper
- `getSingleResult()` vs `getResultList()` on an aggregate query: prefer `getSingleResult()` only
when the query has no `GROUP BY` — a grouped aggregate can legitimately return many rows.
- `StatelessSession` skips the persistence context entirely for a pure read-and-aggregate
workload, avoiding the memory overhead of first-level cache entries the aggregate result
itself never needs.
- [Hibernate ORM Criteria API documentation](https://docs.jboss.org/hibernate/orm/7.4/userguide/html_single/Hibernate_User_Guide.html#criteria)