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,105 @@
|
||||
# 22 — Sorting
|
||||
|
||||
[← Previous: 21 — Aggregate functions](21-aggregate-functions.md) | [Next: 23 — Pagination →](23-pagination.md) | [Back to README →](../README.md)
|
||||
|
||||
Backs the rewrite of ankurm.com post 4889 (sorting).
|
||||
|
||||
## `@OrderBy` names the property, not the column
|
||||
|
||||
```java
|
||||
@Column(name = "song_title")
|
||||
private String title;
|
||||
```
|
||||
```java
|
||||
@OneToMany(mappedBy = "playlist")
|
||||
@OrderBy("title asc")
|
||||
private List<Song> songs;
|
||||
```
|
||||
`title` is the entity property; the column it's stored under is `song_title`, deliberately
|
||||
different from the property name. Hibernate resolves `@OrderBy`'s value against the entity's
|
||||
metamodel, not the mapped table, so it translates the property to the right column itself — a
|
||||
raw column name here would be a coincidence at best.
|
||||
[`Playlist.java`](../src/main/java/com/ankurm/hibernatedemo/sorting/Playlist.java) — [test](../src/test/java/com/ankurm/hibernatedemo/sorting/SortingTest.java) —
|
||||
[output](output/22-orderby-property-name.txt)
|
||||
|
||||
## `@SortNatural` and `@SortComparator` on element collections
|
||||
|
||||
```java
|
||||
@SortNatural
|
||||
private SortedSet<String> tags = new TreeSet<>();
|
||||
|
||||
@SortComparator(LengthThenAlphaComparator.class)
|
||||
private SortedSet<String> genres = new TreeSet<>(new LengthThenAlphaComparator());
|
||||
```
|
||||
Both rebuild a real `java.util.TreeSet` in memory when the collection is loaded — this is not an
|
||||
`ORDER BY` added to the collection's own SQL fetch. `@SortComparator`'s class needs a no-arg
|
||||
constructor; Hibernate instantiates it by reflection.
|
||||
[`Playlist.java`](../src/main/java/com/ankurm/hibernatedemo/sorting/Playlist.java),
|
||||
[`LengthThenAlphaComparator.java`](../src/main/java/com/ankurm/hibernatedemo/sorting/LengthThenAlphaComparator.java)
|
||||
— [output](output/22-sort-natural-and-comparator.txt)
|
||||
|
||||
## Dynamic sorting: the injection risk, and the fix
|
||||
|
||||
Concatenating a caller-supplied field name directly into an `order by` clause hands that caller a
|
||||
way to inject arbitrary HQL — a path onto an unrelated entity, a nested expression, or simply a
|
||||
string that breaks the query as a denial-of-service. The fix is a whitelist checked *before* the
|
||||
string ever reaches the query, not an attempt to sanitize it:
|
||||
|
||||
```java
|
||||
private static final Set<String> ALLOWED = Set.of("title", "artist", "rating");
|
||||
|
||||
public static String toHqlPropertyOrThrow(String requested) {
|
||||
if (!ALLOWED.contains(requested)) {
|
||||
throw new IllegalArgumentException(...);
|
||||
}
|
||||
return requested;
|
||||
}
|
||||
```
|
||||
[`SongSortField.java`](../src/main/java/com/ankurm/hibernatedemo/sorting/SongSortField.java) —
|
||||
[test](../src/test/java/com/ankurm/hibernatedemo/sorting/SortingTest.java) —
|
||||
[output](output/22-dynamic-injection-guard.txt)
|
||||
|
||||
## Criteria `Order` across a join
|
||||
|
||||
`Order` is not limited to the query root's own columns — a joined entity's property works the
|
||||
same way:
|
||||
|
||||
```java
|
||||
Join<Song, Playlist> playlistJoin = root.join("playlist");
|
||||
cq.select(playlistJoin.get("name")).orderBy(cb.asc(playlistJoin.get("name")));
|
||||
```
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/sorting/SortingTest.java) —
|
||||
[output](output/22-criteria-order-join.txt)
|
||||
|
||||
## Null precedence via `jakarta.persistence.criteria.Nulls`
|
||||
|
||||
Jakarta Persistence 3.2 (Hibernate 7's baseline) added `jakarta.persistence.criteria.Nulls`
|
||||
(`FIRST`, `LAST`, `NONE`) and the matching `CriteriaBuilder.asc(Expression, Nulls)` /
|
||||
`.desc(Expression, Nulls)` overloads — confirmed with `javap` against
|
||||
`jakarta.persistence-api-3.2.0.jar`, not assumed from documentation prose:
|
||||
|
||||
```java
|
||||
cq.orderBy(cb.asc(root.get("rating"), Nulls.LAST), cb.asc(root.get("title")));
|
||||
```
|
||||
This makes null precedence explicit in the generated SQL's `ORDER BY`, independent of whatever a
|
||||
given dialect's own default null-ordering would otherwise do.
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/sorting/SortingTest.java) —
|
||||
[output](output/22-null-precedence.txt)
|
||||
|
||||
## Case-insensitive sorting via `cb.lower()`
|
||||
|
||||
```java
|
||||
cq.orderBy(cb.asc(cb.lower(root.get("title"))));
|
||||
```
|
||||
The comparison happens on the lower-cased *value*, computed by the database, not on the raw
|
||||
column — `"Apple"` sorts before `"banana"` despite the capital letter.
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/sorting/SortingTest.java) —
|
||||
[output](output/22-case-insensitive.txt)
|
||||
|
||||
## Going deeper
|
||||
|
||||
- `@OrderBy` with no value defaults to the collection's primary key, ascending — easy to miss
|
||||
when a collection appears correctly ordered by accident and then isn't after a schema change.
|
||||
- A `SortedMap` supports the same `@SortNatural`/`@SortComparator` pair as a `SortedSet`, sorting
|
||||
by key.
|
||||
- [Jakarta Persistence 3.2 specification, §7.2 (`Nulls`, `Order`)](https://jakarta.ee/specifications/persistence/3.2/)
|
||||
Reference in New Issue
Block a user