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,81 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user