4.6 KiB
22 — Sorting
← Previous: 21 — Aggregate functions | Next: 23 — Pagination → | Back to README →
Backs the rewrite of ankurm.com post 4889 (sorting).
@OrderBy names the property, not the column
@Column(name = "song_title")
private String title;
@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 — test —
output
@SortNatural and @SortComparator on element collections
@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,
LengthThenAlphaComparator.java
— output
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:
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 —
test —
output
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:
Join<Song, Playlist> playlistJoin = root.join("playlist");
cq.select(playlistJoin.get("name")).orderBy(cb.asc(playlistJoin.get("name")));
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:
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 —
output
Case-insensitive sorting via cb.lower()
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 —
output
Going deeper
@OrderBywith 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
SortedMapsupports the same@SortNatural/@SortComparatorpair as aSortedSet, sorting by key. - Jakarta Persistence 3.2 specification, §7.2 (
Nulls,Order)