Skip to content

Commit ca6b650

Browse files
DiegoKrupitzagregturn
authored andcommitted
Provide a more sophisticated QueryEnhancer selection algorithm.
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation. Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, available at the repository level and on a per-method basis. Resolves #2564.
1 parent 864c7c4 commit ca6b650

20 files changed

+825
-90
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java

+3-6
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
import jakarta.persistence.EntityManager;
1919
import jakarta.persistence.Query;
2020

21-
import org.apache.commons.logging.Log;
22-
import org.apache.commons.logging.LogFactory;
23-
2421
import org.springframework.data.domain.Pageable;
2522
import org.springframework.data.domain.Sort;
2623
import org.springframework.data.jpa.repository.QueryRewriter;
@@ -65,8 +62,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
6562
* @param queryRewriter must not be {@literal null}.
6663
*/
6764
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
68-
@Nullable String countQueryString, QueryRewriter queryRewriter, QueryMethodEvaluationContextProvider evaluationContextProvider,
69-
SpelExpressionParser parser) {
65+
@Nullable String countQueryString, QueryRewriter queryRewriter,
66+
QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
7067

7168
super(method, em);
7269

@@ -77,7 +74,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
7774

7875
this.evaluationContextProvider = evaluationContextProvider;
7976
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser,
80-
method.isNativeQuery());
77+
method.isNativeQuery(), method.getQueryEnhancerOverride());
8178

8279
DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection());
8380
this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser,

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java

+33-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
*
2626
* @author Jens Schauder
2727
* @author Diego Krupitza
28+
* @author Greg Turnquist
2829
* @since 2.0.3
2930
*/
30-
interface DeclaredQuery {
31+
public interface DeclaredQuery {
3132

3233
/**
3334
* Creates a {@literal DeclaredQuery} from a query {@literal String}.
@@ -105,10 +106,40 @@ default boolean usesPaging() {
105106

106107
/**
107108
* Return whether the query is a native query of not.
108-
*
109+
*
109110
* @return <code>true</code> if native query otherwise <code>false</code>
110111
*/
111112
default boolean isNativeQuery() {
112113
return false;
113114
}
115+
116+
/**
117+
* Gets the {@link QueryEnhancer} used for this Query.
118+
*
119+
* @return the concrete {@link QueryEnhancer} implementation used for this given Query
120+
* @since 3.1
121+
*/
122+
@Nullable
123+
default QueryEnhancer getQueryEnhancer() {
124+
return null;
125+
}
126+
127+
/**
128+
* Returns the method's {@link QueryEnhancerOverride} annotation
129+
*
130+
* @since 3.1
131+
*/
132+
@Nullable
133+
default QueryEnhancerOverride getQueryEnhancerOverride() {
134+
return null;
135+
}
136+
137+
/**
138+
* Returns whether the method has a {@link QueryEnhancerOverride} or not
139+
*
140+
* @since 3.1
141+
*/
142+
default boolean hasQueryEnhancerOverride() {
143+
return getQueryEnhancerOverride() != null;
144+
}
114145
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java

+8-14
Original file line numberDiff line numberDiff line change
@@ -24,43 +24,37 @@
2424
* The implementation of {@link QueryEnhancer} using {@link QueryUtils}.
2525
*
2626
* @author Diego Krupitza
27+
* @author Greg Turnquist
2728
* @since 2.7.0
2829
*/
29-
public class DefaultQueryEnhancer implements QueryEnhancer {
30-
31-
private final DeclaredQuery query;
30+
public class DefaultQueryEnhancer extends QueryEnhancer {
3231

3332
public DefaultQueryEnhancer(DeclaredQuery query) {
34-
this.query = query;
33+
super(query);
3534
}
3635

3736
@Override
3837
public String applySorting(Sort sort, @Nullable String alias) {
39-
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
38+
return QueryUtils.applySorting(getQuery().getQueryString(), sort, alias);
4039
}
4140

4241
@Override
4342
public String detectAlias() {
44-
return QueryUtils.detectAlias(this.query.getQueryString());
43+
return QueryUtils.detectAlias(getQuery().getQueryString());
4544
}
4645

4746
@Override
4847
public String createCountQueryFor(@Nullable String countProjection) {
49-
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection);
48+
return QueryUtils.createCountQueryFor(getQuery().getQueryString(), countProjection);
5049
}
5150

5251
@Override
5352
public String getProjection() {
54-
return QueryUtils.getProjection(this.query.getQueryString());
53+
return QueryUtils.getProjection(getQuery().getQueryString());
5554
}
5655

5756
@Override
5857
public Set<String> getJoinAliases() {
59-
return QueryUtils.getOuterJoinAliases(this.query.getQueryString());
60-
}
61-
62-
@Override
63-
public DeclaredQuery getQuery() {
64-
return this.query;
58+
return QueryUtils.getOuterJoinAliases(getQuery().getQueryString());
6559
}
6660
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* NULL-Object pattern implementation for {@link DeclaredQuery}.
2626
*
2727
* @author Jens Schauder
28+
* @author Diego Krupitza
2829
* @since 2.0.3
2930
*/
3031
class EmptyDeclaredQuery implements DeclaredQuery {

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@
2222
import org.springframework.expression.ParserContext;
2323
import org.springframework.expression.spel.standard.SpelExpressionParser;
2424
import org.springframework.expression.spel.support.StandardEvaluationContext;
25+
import org.springframework.lang.Nullable;
2526
import org.springframework.util.Assert;
2627

2728
/**
2829
* Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression.
2930
* <p>
30-
* Currently the following template variables are available:
31+
* Currently, the following template variables are available:
3132
* <ol>
3233
* <li>{@code #entityName} - the simple class name of the given entity</li>
3334
* <ol>
@@ -60,7 +61,23 @@ class ExpressionBasedStringQuery extends StringQuery {
6061
*/
6162
public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, SpelExpressionParser parser,
6263
boolean nativeQuery) {
63-
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query));
64+
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query),
65+
null);
66+
}
67+
68+
/**
69+
* Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link EntityMetadata}.
70+
*
71+
* @param query must not be {@literal null} or empty.
72+
* @param metadata must not be {@literal null}.
73+
* @param parser must not be {@literal null}.
74+
* @param nativeQuery is a given query is native or not
75+
* @param queryEnhancerOverride may be {@literal null}.
76+
*/
77+
public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, SpelExpressionParser parser,
78+
boolean nativeQuery, @Nullable QueryEnhancerOverride queryEnhancerOverride) {
79+
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query),
80+
queryEnhancerOverride);
6481
}
6582

6683
/**
@@ -74,7 +91,8 @@ public ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, S
7491
*/
7592
static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata<?> metadata,
7693
SpelExpressionParser parser, boolean nativeQuery) {
77-
return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery);
94+
return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery,
95+
query.getQueryEnhancerOverride());
7896
}
7997

8098
/**

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java

+27-21
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,23 @@
2929
import net.sf.jsqlparser.statement.delete.Delete;
3030
import net.sf.jsqlparser.statement.insert.Insert;
3131
import net.sf.jsqlparser.statement.merge.Merge;
32-
import net.sf.jsqlparser.statement.select.*;
32+
import net.sf.jsqlparser.statement.select.OrderByElement;
33+
import net.sf.jsqlparser.statement.select.PlainSelect;
34+
import net.sf.jsqlparser.statement.select.Select;
35+
import net.sf.jsqlparser.statement.select.SelectBody;
36+
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
37+
import net.sf.jsqlparser.statement.select.SelectItem;
38+
import net.sf.jsqlparser.statement.select.SetOperationList;
39+
import net.sf.jsqlparser.statement.select.WithItem;
3340
import net.sf.jsqlparser.statement.update.Update;
3441
import net.sf.jsqlparser.statement.values.ValuesStatement;
3542

36-
import java.util.*;
43+
import java.util.ArrayList;
44+
import java.util.Collections;
45+
import java.util.HashSet;
46+
import java.util.List;
47+
import java.util.Objects;
48+
import java.util.Set;
3749
import java.util.stream.Collectors;
3850

3951
import org.springframework.data.domain.Sort;
@@ -50,17 +62,16 @@
5062
* @author Geoffrey Deremetz
5163
* @since 2.7.0
5264
*/
53-
public class JSqlParserQueryEnhancer implements QueryEnhancer {
65+
public class JSqlParserQueryEnhancer extends QueryEnhancer {
5466

55-
private final DeclaredQuery query;
5667
private final ParsedType parsedType;
5768

5869
/**
5970
* @param query the query we want to enhance. Must not be {@literal null}.
6071
*/
6172
public JSqlParserQueryEnhancer(DeclaredQuery query) {
6273

63-
this.query = query;
74+
super(query);
6475
this.parsedType = detectParsedType();
6576
}
6677

@@ -72,7 +83,7 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) {
7283
private ParsedType detectParsedType() {
7384

7485
try {
75-
Statement statement = CCJSqlParserUtil.parse(this.query.getQueryString());
86+
Statement statement = CCJSqlParserUtil.parse(getQuery().getQueryString());
7687

7788
if (statement instanceof Insert) {
7889
return ParsedType.INSERT;
@@ -95,7 +106,7 @@ private ParsedType detectParsedType() {
95106
@Override
96107
public String applySorting(Sort sort, @Nullable String alias) {
97108

98-
String queryString = query.getQueryString();
109+
String queryString = getQuery().getQueryString();
99110
Assert.hasText(queryString, "Query must not be null or empty");
100111

101112
if (this.parsedType != ParsedType.SELECT) {
@@ -192,7 +203,7 @@ Set<String> getSelectionAliases() {
192203
return new HashSet<>();
193204
}
194205

195-
Select selectStatement = parseSelectStatement(this.query.getQueryString());
206+
Select selectStatement = parseSelectStatement(getQuery().getQueryString());
196207
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
197208
return this.getSelectionAliases(selectBody);
198209
}
@@ -280,7 +291,7 @@ private OrderByElement getOrderClause(final Set<String> joinAliases, final Set<S
280291

281292
@Override
282293
public String detectAlias() {
283-
return detectAlias(this.query.getQueryString());
294+
return detectAlias(getQuery().getQueryString());
284295
}
285296

286297
/**
@@ -354,18 +365,18 @@ private String detectAlias(Merge mergeStatement) {
354365
public String createCountQueryFor(@Nullable String countProjection) {
355366

356367
if (this.parsedType != ParsedType.SELECT) {
357-
return this.query.getQueryString();
368+
return getQuery().getQueryString();
358369
}
359370

360-
Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty");
371+
Assert.hasText(getQuery().getQueryString(), "OriginalQuery must not be null or empty");
361372

362-
Select selectStatement = parseSelectStatement(this.query.getQueryString());
373+
Select selectStatement = parseSelectStatement(getQuery().getQueryString());
363374

364375
/*
365376
We only support count queries for {@link PlainSelect}.
366377
*/
367378
if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
368-
return this.query.getQueryString();
379+
return getQuery().getQueryString();
369380
}
370381

371382
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
@@ -423,9 +434,9 @@ public String getProjection() {
423434
return "";
424435
}
425436

426-
Assert.hasText(query.getQueryString(), "Query must not be null or empty");
437+
Assert.hasText(getQuery().getQueryString(), "Query must not be null or empty");
427438

428-
Select selectStatement = parseSelectStatement(query.getQueryString());
439+
Select selectStatement = parseSelectStatement(getQuery().getQueryString());
429440

430441
if (selectStatement.getSelectBody() instanceof ValuesStatement) {
431442
return "";
@@ -451,7 +462,7 @@ public String getProjection() {
451462

452463
@Override
453464
public Set<String> getJoinAliases() {
454-
return this.getJoinAliases(this.query.getQueryString());
465+
return this.getJoinAliases(getQuery().getQueryString());
455466
}
456467

457468
/**
@@ -492,11 +503,6 @@ private boolean onlyASingleColumnProjection(List<SelectItem> projection) {
492503
&& (((SelectExpressionItem) projection.get(0)).getExpression()) instanceof Column;
493504
}
494505

495-
@Override
496-
public DeclaredQuery getQuery() {
497-
return this.query;
498-
}
499-
500506
/**
501507
* An enum to represent the top level parsed statement of the provided query.
502508
* <ul>

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java

+28
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
* @author Сергей Цыпанов
6363
* @author Réda Housni Alaoui
6464
* @author Greg Turnquist
65+
* @author Diego Krupitza
6566
*/
6667
public class JpaQueryMethod extends QueryMethod {
6768

@@ -98,6 +99,7 @@ public class JpaQueryMethod extends QueryMethod {
9899
private final Lazy<Boolean> isProcedureQuery;
99100
private final Lazy<JpaEntityMetadata<?>> entityMetadata;
100101
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
102+
private final Lazy<QueryEnhancerOverride> queryEnhancerOverride;
101103

102104
/**
103105
* Creates a {@link JpaQueryMethod}.
@@ -141,6 +143,13 @@ protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionF
141143
this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass()));
142144
this.annotationCache = new ConcurrentReferenceHashMap<>();
143145

146+
this.queryEnhancerOverride = Lazy.of(() -> Optional //
147+
.ofNullable( // First check the method itself for an override
148+
AnnotatedElementUtils.findMergedAnnotation(method, QueryEnhancerOverride.class)) //
149+
.orElseGet( // Otherwise, check the enclosing class
150+
() -> AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), QueryEnhancerOverride.class)) //
151+
);
152+
144153
Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
145154
String.format("Modifying method must not contain %s", Parameters.TYPES));
146155
assertParameterNamesInAnnotatedQuery();
@@ -387,6 +396,25 @@ boolean isNativeQuery() {
387396
return this.isNativeQuery.get();
388397
}
389398

399+
/**
400+
* Returns the methods {@link QueryEnhancerOverride} annotation
401+
*
402+
* @return
403+
*/
404+
@Nullable
405+
QueryEnhancerOverride getQueryEnhancerOverride() {
406+
return this.queryEnhancerOverride.getNullable();
407+
}
408+
409+
/**
410+
* Returns whether the method has a {@link QueryEnhancerOverride} or not
411+
*
412+
* @return
413+
*/
414+
boolean hasQueryEnhancerOverride() {
415+
return this.queryEnhancerOverride.getOptional().isPresent();
416+
}
417+
390418
@Override
391419
public String getNamedQueryName() {
392420

0 commit comments

Comments
 (0)