Skip to content

Commit db39c48

Browse files
GregoireDruantschauder
authored andcommitted
DATAJPA-1061 - Sorting now works with field aliases.
Extract field aliases in QueryUtils and add them in the set of possible sort criteria. Original pull request: #276.
1 parent 37a4ee5 commit db39c48

File tree

2 files changed

+89
-24
lines changed

2 files changed

+89
-24
lines changed

src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

+55-24
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
/**
6464
* Simple utility class to create JPA queries.
65-
*
65+
*
6666
* @author Oliver Gierke
6767
* @author Kevin Raymond
6868
* @author Thomas Darimont
@@ -72,6 +72,7 @@
7272
* @author Sébastien Péralta
7373
* @author Jens Schauder
7474
* @author Florian Lüdiger
75+
* @author Grégoire Druant
7576
*/
7677
public abstract class QueryUtils {
7778

@@ -109,6 +110,7 @@ public abstract class QueryUtils {
109110

110111
private static final Pattern PUNCTATION_PATTERN = Pattern.compile(".*((?![\\._])[\\p{Punct}|\\s])");
111112
private static final Pattern FUNCTION_PATTERN;
113+
private static final Pattern FIELD_ALIAS_PATTERN;
112114

113115
private static final String UNSAFE_PROPERTY_REFERENCE = "Sort expression '%s' must only contain property references or "
114116
+ "aliases used in the select clause. If you really want to use something other than that for sorting, please use "
@@ -165,6 +167,14 @@ public abstract class QueryUtils {
165167
builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
166168

167169
FUNCTION_PATTERN = compile(builder.toString());
170+
171+
builder = new StringBuilder();
172+
builder.append("\\s+"); // at least one space
173+
builder.append("[^\\s\\(\\)]+"); // No white char no bracket
174+
builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); // the potential alias
175+
176+
FIELD_ALIAS_PATTERN = compile(builder.toString());
177+
168178
}
169179

170180
/**
@@ -176,7 +186,7 @@ private QueryUtils() {
176186

177187
/**
178188
* Returns the query string to execute an exists query for the given id attributes.
179-
*
189+
*
180190
* @param entityName the name of the entity to create the query for, must not be {@literal null}.
181191
* @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
182192
* @param idAttributes the id attributes for the entity, must not be {@literal null}.
@@ -201,7 +211,7 @@ public static String getExistsQueryString(String entityName, String countQueryPl
201211

202212
/**
203213
* Returns the query string for the given class name.
204-
*
214+
*
205215
* @param template
206216
* @param entityName
207217
* @return
@@ -215,7 +225,7 @@ public static String getQueryString(String template, String entityName) {
215225

216226
/**
217227
* Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
218-
*
228+
*
219229
* @param query the query string to which sorting is applied
220230
* @param sort the sort specification to apply.
221231
* @return the modified query string.
@@ -226,7 +236,7 @@ public static String applySorting(String query, Sort sort) {
226236

227237
/**
228238
* Adds {@literal order by} clause to the JPQL query.
229-
*
239+
*
230240
* @param query the query string to which sorting is applied. Must not be {@literal null} or empty.
231241
* @param sort the sort specification to apply.
232242
* @param alias the alias to be used in the order by clause. Must not be {@literal null} or empty.
@@ -249,10 +259,11 @@ public static String applySorting(String query, Sort sort, String alias) {
249259
}
250260

251261
Set<String> aliases = getOuterJoinAliases(query);
252-
Set<String> functionAliases = getFunctionAliases(query);
262+
Set<String> fieldAliases = getFunctionAliases(query);
263+
fieldAliases.addAll(getFieldAliases(query));
253264

254265
for (Order order : sort) {
255-
builder.append(getOrderClause(aliases, functionAliases, alias, order)).append(", ");
266+
builder.append(getOrderClause(aliases, fieldAliases, alias, order)).append(", ");
256267
}
257268

258269
builder.delete(builder.length() - 2, builder.length());
@@ -263,19 +274,19 @@ public static String applySorting(String query, Sort sort, String alias) {
263274
/**
264275
* Returns the order clause for the given {@link Order}. Will prefix the clause with the given alias if the referenced
265276
* property refers to a join alias, i.e. starts with {@code $alias.}.
266-
*
277+
*
267278
* @param joinAliases the join aliases of the original query.
268279
* @param alias the alias for the root entity.
269280
* @param order the order object to build the clause for.
270281
* @return
271282
*/
272-
private static String getOrderClause(Set<String> joinAliases, Set<String> functionAlias, String alias, Order order) {
283+
private static String getOrderClause(Set<String> joinAliases, Set<String> fieldAlias, String alias, Order order) {
273284

274285
String property = order.getProperty();
275286

276287
checkSortExpression(order);
277288

278-
if (functionAlias.contains(property)) {
289+
if (fieldAlias.contains(property)) {
279290
return String.format("%s %s", property, toJpaDirection(order));
280291
}
281292

@@ -297,7 +308,7 @@ private static String getOrderClause(Set<String> joinAliases, Set<String> functi
297308

298309
/**
299310
* Returns the aliases used for {@code left (outer) join}s.
300-
*
311+
*
301312
* @param query
302313
* @return
303314
*/
@@ -317,6 +328,26 @@ static Set<String> getOuterJoinAliases(String query) {
317328
return result;
318329
}
319330

331+
/**
332+
* Returns the aliases used for fields in the query.
333+
*
334+
* @param query a {@literal String} containing a query. Must not be {@literal null}.
335+
* @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
336+
*/
337+
private static Set<String> getFieldAliases(String query) {
338+
Set<String> result = new HashSet<String>();
339+
Matcher matcher = FIELD_ALIAS_PATTERN.matcher(query);
340+
341+
while (matcher.find()) {
342+
String alias = matcher.group(1);
343+
344+
if (StringUtils.hasText(alias)) {
345+
result.add(alias);
346+
}
347+
}
348+
return result;
349+
}
350+
320351
/**
321352
* Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
322353
*
@@ -346,7 +377,7 @@ private static String toJpaDirection(Order order) {
346377

347378
/**
348379
* Resolves the alias for the entity to be retrieved from the given JPA query.
349-
*
380+
*
350381
* @param query
351382
* @return
352383
*/
@@ -360,7 +391,7 @@ public static String detectAlias(String query) {
360391
/**
361392
* Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given
362393
* entities to the query.
363-
*
394+
*
364395
* @param <T>
365396
* @param queryString must not be {@literal null}.
366397
* @param entities must not be {@literal null}.
@@ -410,7 +441,7 @@ public static <T> Query applyAndBind(String queryString, Iterable<T> entities, E
410441

411442
/**
412443
* Creates a count projected query from the given original query.
413-
*
444+
*
414445
* @param originalQuery must not be {@literal null} or empty.
415446
* @return
416447
*/
@@ -420,7 +451,7 @@ public static String createCountQueryFor(String originalQuery) {
420451

421452
/**
422453
* Creates a count projected query from the given original query.
423-
*
454+
*
424455
* @param originalQuery must not be {@literal null}.
425456
* @param countProjection may be {@literal null}.
426457
* @return
@@ -450,7 +481,7 @@ public static String createCountQueryFor(String originalQuery, String countProje
450481

451482
/**
452483
* Returns whether the given {@link Query} contains named parameters.
453-
*
484+
*
454485
* @param query
455486
* @return
456487
*/
@@ -471,7 +502,7 @@ public static boolean hasNamedParameter(Query query) {
471502

472503
/**
473504
* Returns whether the given query contains named parameters.
474-
*
505+
*
475506
* @param query can be {@literal null} or empty.
476507
* @return
477508
*/
@@ -481,7 +512,7 @@ public static boolean hasNamedParameter(String query) {
481512

482513
/**
483514
* Turns the given {@link Sort} into {@link javax.persistence.criteria.Order}s.
484-
*
515+
*
485516
* @param sort the {@link Sort} instance to be transformed into JPA {@link javax.persistence.criteria.Order}s.
486517
* @param from must not be {@literal null}.
487518
* @param cb must not be {@literal null}.
@@ -507,7 +538,7 @@ public static List<javax.persistence.criteria.Order> toOrders(Sort sort, From<?,
507538

508539
/**
509540
* Returns whether the given JPQL query contains a constructor expression.
510-
*
541+
*
511542
* @param query must not be {@literal null} or empty.
512543
* @return
513544
* @since 1.10
@@ -521,7 +552,7 @@ public static boolean hasConstructorExpression(String query) {
521552

522553
/**
523554
* Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
524-
*
555+
*
525556
* @param query must not be {@literal null} or empty.
526557
* @return
527558
* @since 1.10.2
@@ -536,7 +567,7 @@ public static String getProjection(String query) {
536567

537568
/**
538569
* Creates a criteria API {@link javax.persistence.criteria.Order} from the given {@link Order}.
539-
*
570+
*
540571
* @param order the order to transform into a JPA {@link javax.persistence.criteria.Order}
541572
* @param from the {@link From} the {@link Order} expression is based on
542573
* @param cb the {@link CriteriaBuilder} to build the {@link javax.persistence.criteria.Order} with
@@ -587,7 +618,7 @@ static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath p
587618
/**
588619
* Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
589620
* optional association.
590-
*
621+
*
591622
* @param propertyPathModel must not be {@literal null}.
592623
* @param isPluralAttribute is the attribute of Collection type?
593624
* @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
@@ -638,7 +669,7 @@ static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPat
638669

639670
/**
640671
* Returns an existing join for the given attribute if one already exists or creates a new one if not.
641-
*
672+
*
642673
* @param from the {@link From} to get the current joins from.
643674
* @param attribute the {@link Attribute} to look for in the current joins.
644675
* @return will never be {@literal null}.
@@ -659,7 +690,7 @@ static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPat
659690

660691
/**
661692
* Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
662-
*
693+
*
663694
* @param from the {@link From} to check for fetches.
664695
* @param attribute the attribute name to check.
665696
* @return

src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java

+34
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* @author Komi Innocent
3737
* @author Christoph Strobl
3838
* @author Florian Lüdiger
39+
* @author Grégoire Druant
3940
*/
4041
public class QueryUtilsUnitTests {
4142

@@ -421,6 +422,39 @@ public void createCountQuerySupportsWhitespaceCharacters() {
421422
" where user.age = 18\n "));
422423
}
423424

425+
@Test // DATAJPA-1061
426+
public void appliesSortCorrectlyForFieldAliases() {
427+
428+
String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
429+
Sort sort = new Sort("authorName");
430+
431+
String fullQuery = applySorting(query, sort);
432+
433+
assertThat(fullQuery, endsWith("order by authorName asc"));
434+
}
435+
436+
@Test // DATAJPA-1061
437+
public void appliesSortCorrectlyForFunctionAliases() {
438+
439+
String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
440+
Sort sort = new Sort("title");
441+
442+
String fullQuery = applySorting(query, sort);
443+
444+
assertThat(fullQuery, endsWith("order by title asc"));
445+
}
446+
447+
@Test // DATAJPA-1061
448+
public void appliesSortCorrectlyForSimpleField() {
449+
450+
String query = "SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a";
451+
Sort sort = new Sort("price");
452+
453+
String fullQuery = applySorting(query, sort);
454+
455+
assertThat(fullQuery, endsWith("order by m.price asc"));
456+
}
457+
424458
private static void assertCountQuery(String originalQuery, String countQuery) {
425459
assertThat(createCountQueryFor(originalQuery), is(countQuery));
426460
}

0 commit comments

Comments
 (0)