Skip to content

Commit 9f73b6c

Browse files
DiegoKrupitzagregturn
authored andcommitted
Create @QueryEnhancerOverride to let developes override the automatically selected QueryEnhancer.
Sometimes queryies contain content that cannot be pasrsed by special implementations such as `JSqlParserQueryEnhancer` (or other potential solutions). We implemented a means for developers to deliberately override the automatic choice with their own via annotation. Resolves #2564.
1 parent 0846027 commit 9f73b6c

19 files changed

+665
-84
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

+27-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* @author Diego Krupitza
2828
* @since 2.0.3
2929
*/
30-
interface DeclaredQuery {
30+
public interface DeclaredQuery {
3131

3232
/**
3333
* Creates a {@literal DeclaredQuery} from a query {@literal String}.
@@ -105,10 +105,35 @@ default boolean usesPaging() {
105105

106106
/**
107107
* Return whether the query is a native query of not.
108-
*
108+
*
109109
* @return <code>true</code> if native query otherwise <code>false</code>
110110
*/
111111
default boolean isNativeQuery() {
112112
return false;
113113
}
114+
115+
/**
116+
* Gets the {@link QueryEnhancer} used for this Query.
117+
*
118+
* @return the concrete {@link QueryEnhancer} implementation used for this given Query
119+
*/
120+
@Nullable
121+
QueryEnhancer getQueryEnhancer();
122+
123+
/**
124+
* Returns the method's {@link QueryEnhancerOverride} annotation
125+
*
126+
* @return
127+
*/
128+
@Nullable
129+
QueryEnhancerOverride getQueryEnhancerOverride();
130+
131+
/**
132+
* Returns whether the method has a {@link QueryEnhancerOverride} or not
133+
*
134+
* @return
135+
*/
136+
default boolean hasQueryEnhancerOverride() {
137+
return getQueryEnhancerOverride() != null;
138+
}
114139
}

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

+7-13
Original file line numberDiff line numberDiff line change
@@ -26,41 +26,35 @@
2626
* @author Diego Krupitza
2727
* @since 2.7.0
2828
*/
29-
public class DefaultQueryEnhancer implements QueryEnhancer {
30-
31-
private final DeclaredQuery query;
29+
public class DefaultQueryEnhancer extends QueryEnhancer {
3230

3331
public DefaultQueryEnhancer(DeclaredQuery query) {
34-
this.query = query;
32+
super(query);
3533
}
3634

3735
@Override
3836
public String applySorting(Sort sort, @Nullable String alias) {
39-
return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
37+
return QueryUtils.applySorting(getQuery().getQueryString(), sort, alias);
4038
}
4139

4240
@Override
4341
public String detectAlias() {
44-
return QueryUtils.detectAlias(this.query.getQueryString());
42+
return QueryUtils.detectAlias(getQuery().getQueryString());
4543
}
4644

4745
@Override
4846
public String createCountQueryFor(@Nullable String countProjection) {
49-
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection);
47+
return QueryUtils.createCountQueryFor(getQuery().getQueryString(), countProjection);
5048
}
5149

5250
@Override
5351
public String getProjection() {
54-
return QueryUtils.getProjection(this.query.getQueryString());
52+
return QueryUtils.getProjection(getQuery().getQueryString());
5553
}
5654

5755
@Override
5856
public Set<String> getJoinAliases() {
59-
return QueryUtils.getOuterJoinAliases(this.query.getQueryString());
57+
return QueryUtils.getOuterJoinAliases(getQuery().getQueryString());
6058
}
6159

62-
@Override
63-
public DeclaredQuery getQuery() {
64-
return this.query;
65-
}
6660
}

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

+11
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 {
@@ -76,4 +77,14 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable Str
7677
public boolean usesJdbcStyleParameters() {
7778
return false;
7879
}
80+
81+
@Override
82+
public QueryEnhancer getQueryEnhancer() {
83+
return null;
84+
}
85+
86+
@Override
87+
public QueryEnhancerOverride getQueryEnhancerOverride() {
88+
return null;
89+
}
7990
}

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-22
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,15 @@
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) {
62-
63-
this.query = query;
73+
super(query);
6474
this.parsedType = detectParsedType();
6575
}
6676

@@ -72,7 +82,7 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) {
7282
private ParsedType detectParsedType() {
7383

7484
try {
75-
Statement statement = CCJSqlParserUtil.parse(this.query.getQueryString());
85+
Statement statement = CCJSqlParserUtil.parse(getQuery().getQueryString());
7686

7787
if (statement instanceof Insert) {
7888
return ParsedType.INSERT;
@@ -95,7 +105,7 @@ private ParsedType detectParsedType() {
95105
@Override
96106
public String applySorting(Sort sort, @Nullable String alias) {
97107

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

101111
if (this.parsedType != ParsedType.SELECT) {
@@ -192,7 +202,7 @@ Set<String> getSelectionAliases() {
192202
return new HashSet<>();
193203
}
194204

195-
Select selectStatement = parseSelectStatement(this.query.getQueryString());
205+
Select selectStatement = parseSelectStatement(getQuery().getQueryString());
196206
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
197207
return this.getSelectionAliases(selectBody);
198208
}
@@ -280,7 +290,7 @@ private OrderByElement getOrderClause(final Set<String> joinAliases, final Set<S
280290

281291
@Override
282292
public String detectAlias() {
283-
return detectAlias(this.query.getQueryString());
293+
return detectAlias(getQuery().getQueryString());
284294
}
285295

286296
/**
@@ -354,18 +364,18 @@ private String detectAlias(Merge mergeStatement) {
354364
public String createCountQueryFor(@Nullable String countProjection) {
355365

356366
if (this.parsedType != ParsedType.SELECT) {
357-
return this.query.getQueryString();
367+
return getQuery().getQueryString();
358368
}
359369

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

362-
Select selectStatement = parseSelectStatement(this.query.getQueryString());
372+
Select selectStatement = parseSelectStatement(getQuery().getQueryString());
363373

364374
/*
365375
We only support count queries for {@link PlainSelect}.
366376
*/
367377
if (!(selectStatement.getSelectBody() instanceof PlainSelect)) {
368-
return this.query.getQueryString();
378+
return getQuery().getQueryString();
369379
}
370380

371381
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
@@ -423,9 +433,9 @@ public String getProjection() {
423433
return "";
424434
}
425435

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

428-
Select selectStatement = parseSelectStatement(query.getQueryString());
438+
Select selectStatement = parseSelectStatement(getQuery().getQueryString());
429439

430440
if (selectStatement.getSelectBody() instanceof ValuesStatement) {
431441
return "";
@@ -451,7 +461,7 @@ public String getProjection() {
451461

452462
@Override
453463
public Set<String> getJoinAliases() {
454-
return this.getJoinAliases(this.query.getQueryString());
464+
return this.getJoinAliases(getQuery().getQueryString());
455465
}
456466

457467
/**
@@ -492,11 +502,6 @@ private boolean onlyASingleColumnProjection(List<SelectItem> projection) {
492502
&& (((SelectExpressionItem) projection.get(0)).getExpression()) instanceof Column;
493503
}
494504

495-
@Override
496-
public DeclaredQuery getQuery() {
497-
return this.query;
498-
}
499-
500505
/**
501506
* An enum to represent the top level parsed statement of the provided query.
502507
* <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+
// First, look at the method itself for an override. Otherwise, check the enclosing class.
147+
this.queryEnhancerOverride = Lazy.of(() -> Optional //
148+
.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, QueryEnhancerOverride.class)) //
149+
.orElseGet(
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)