82 lines
4.2 KiB
Markdown
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)
|