tokens = introspector.getProjection();
+ this.projection = tokens.isEmpty() ? "" : render(tokens);
+ }
+
+ static ParserRuleContext parse(String query, Function lexerFactoryFunction,
+ Function parserFactoryFunction, Function parseFunction) {
+
+ Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query));
+ P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer));
+
+ configureParser(query, lexer, parser);
+
+ return parseFunction.apply(parser);
}
/**
- * Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using JPQL grammar.
+ * Apply common configuration (SLL prediction for performance, our own error listeners).
+ *
+ * @param query
+ * @param lexer
+ * @param parser
+ */
+ static void configureParser(String query, Lexer lexer, Parser parser) {
+
+ BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query);
+
+ lexer.removeErrorListeners();
+ lexer.addErrorListener(errorListener);
+
+ parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
+
+ parser.removeErrorListeners();
+ parser.addErrorListener(errorListener);
+ }
+
+ /**
+ * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using JPQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using JPQL.
@@ -58,11 +109,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
- return new JpaQueryEnhancer(query, JpqlQueryParser.parseQuery(query.getQueryString()));
+ return JpqlQueryParser.parseQuery(query.getQueryString());
}
/**
- * Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using HQL grammar.
+ * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using HQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using HQL.
@@ -71,11 +122,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
- return new JpaQueryEnhancer(query, HqlQueryParser.parseQuery(query.getQueryString()));
+ return HqlQueryParser.parseQuery(query.getQueryString());
}
/**
- * Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using EQL grammar.
+ * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using EQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using EQL.
@@ -85,11 +136,54 @@ class JpaQueryEnhancer implements QueryEnhancer {
Assert.notNull(query, "DeclaredQuery must not be null!");
- return new JpaQueryEnhancer(query, EqlQueryParser.parseQuery(query.getQueryString()));
+ return EqlQueryParser.parseQuery(query.getQueryString());
}
- protected JpaQueryParser getQueryParsingStrategy() {
- return queryParser;
+ /**
+ * Checks if the select clause has a new constructor instantiation in the JPA query.
+ *
+ * @return Guaranteed to return {@literal true} or {@literal false}.
+ */
+ @Override
+ public boolean hasConstructorExpression() {
+ return this.introspector.hasConstructorExpression();
+ }
+
+ /**
+ * Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can
+ * already find the alias when generating sorted and count queries, this is mainly to serve test cases.
+ */
+ @Override
+ public String detectAlias() {
+ return this.introspector.getAlias();
+ }
+
+ /**
+ * Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when
+ * generating sorted and count queries, this is mainly to serve test cases.
+ */
+ @Override
+ public String getProjection() {
+ return this.projection;
+ }
+
+ /**
+ * Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a
+ * placeholder method.
+ *
+ * @return empty set
+ */
+ @Override
+ public Set getJoinAliases() {
+ return Set.of();
+ }
+
+ /**
+ * Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}.
+ */
+ @Override
+ public DeclaredQuery getQuery() {
+ throw new UnsupportedOperationException();
}
/**
@@ -100,7 +194,7 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String applySorting(Sort sort) {
- return queryParser.renderSortedQuery(sort);
+ return render(sortFunction.apply(sort, detectAlias()).visit(context));
}
/**
@@ -115,15 +209,6 @@ class JpaQueryEnhancer implements QueryEnhancer {
return applySorting(sort);
}
- /**
- * Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can
- * already find the alias when generating sorted and count queries, this is mainly to serve test cases.
- */
- @Override
- public String detectAlias() {
- return queryParser.findAlias();
- }
-
/**
* Creates a count query from the original query, with no count projection.
*
@@ -141,44 +226,89 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String createCountQueryFor(@Nullable String countProjection) {
- return queryParser.createCountQuery(countProjection);
+ return render(countQueryFunction.apply(countProjection, detectAlias()).visit(context));
}
/**
- * Checks if the select clause has a new constructor instantiation in the JPA query.
+ * Implements the {@code HQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated
+ * {@link HqlParser} and {@link HqlSortedQueryTransformer}.
*
- * @return Guaranteed to return {@literal true} or {@literal false}.
+ * @author Greg Turnquist
+ * @author Mark Paluch
+ * @since 3.1
*/
- @Override
- public boolean hasConstructorExpression() {
- return queryParser.hasConstructorExpression();
+ static class HqlQueryParser extends JpaQueryEnhancer {
+
+ private HqlQueryParser(String query) {
+ super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(),
+ HqlSortedQueryTransformer::new, HqlCountQueryTransformer::new);
+ }
+
+ /**
+ * Parse a HQL query.
+ *
+ * @param query
+ * @return the query parser.
+ * @throws BadJpqlGrammarException
+ */
+ public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
+ return new HqlQueryParser(query);
+ }
+
}
/**
- * Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when
- * generating sorted and count queries, this is mainly to serve test cases.
- */
- @Override
- public String getProjection() {
- return queryParser.getProjection();
- }
-
- /**
- * Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a
- * placeholder method.
+ * Implements the {@code EQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated
+ * {@link EqlParser}.
*
- * @return empty set
+ * @author Greg Turnquist
+ * @author Mark Paluch
+ * @since 3.2
*/
- @Override
- public Set getJoinAliases() {
- return Set.of();
+ static class EqlQueryParser extends JpaQueryEnhancer {
+
+ private EqlQueryParser(String query) {
+ super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(),
+ EqlSortedQueryTransformer::new, EqlCountQueryTransformer::new);
+ }
+
+ /**
+ * Parse a EQL query.
+ *
+ * @param query
+ * @return the query parser.
+ * @throws BadJpqlGrammarException
+ */
+ public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
+ return new EqlQueryParser(query);
+ }
+
}
/**
- * Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}.
+ * Implements the {@code JPQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated
+ * {@link JpqlParser} and {@link JpqlSortedQueryTransformer}.
+ *
+ * @author Greg Turnquist
+ * @author Mark Paluch
+ * @since 3.1
*/
- @Override
- public DeclaredQuery getQuery() {
- return query;
+ static class JpqlQueryParser extends JpaQueryEnhancer {
+
+ private JpqlQueryParser(String query) {
+ super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(),
+ JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new);
+ }
+
+ /**
+ * Parse a JPQL query.
+ *
+ * @param query
+ * @return the query parser.
+ * @throws BadJpqlGrammarException
+ */
+ public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
+ return new JpqlQueryParser(query);
+ }
}
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java
index d56d43d1f..b299d87c4 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java
@@ -39,7 +39,6 @@ import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
-import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.QueryMethod;
@@ -108,7 +107,7 @@ public class JpaQueryMethod extends QueryMethod {
* @param factory must not be {@literal null}
* @param extractor must not be {@literal null}
*/
- protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
+ public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
QueryExtractor extractor) {
super(method, metadata, factory);
@@ -145,7 +144,6 @@ public class JpaQueryMethod extends QueryMethod {
Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
() -> String.format("Modifying method must not contain %s", Parameters.TYPES));
- assertParameterNamesInAnnotatedQuery();
}
private static Class> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
@@ -160,29 +158,7 @@ public class JpaQueryMethod extends QueryMethod {
return returnType.getType();
}
- private void assertParameterNamesInAnnotatedQuery() {
- String annotatedQuery = getAnnotatedQuery();
-
- if (!DeclaredQuery.hasNamedParameter(annotatedQuery)) {
- return;
- }
-
- for (Parameter parameter : getParameters()) {
-
- if (!parameter.isNamedParameter()) {
- continue;
- }
-
- if (!StringUtils.hasText(annotatedQuery)
- || !annotatedQuery.contains(String.format(":%s", parameter.getName().get()))
- && !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) {
- throw new IllegalStateException(
- String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'",
- method, parameter.getName(), annotatedQuery));
- }
- }
- }
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParser.java
deleted file mode 100644
index 835b71a7b..000000000
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParser.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2022-2024 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.jpa.repository.query;
-
-import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
-
-import java.util.List;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-import org.antlr.v4.runtime.CharStream;
-import org.antlr.v4.runtime.CharStreams;
-import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.Lexer;
-import org.antlr.v4.runtime.Parser;
-import org.antlr.v4.runtime.ParserRuleContext;
-import org.antlr.v4.runtime.TokenStream;
-import org.antlr.v4.runtime.atn.PredictionMode;
-import org.antlr.v4.runtime.tree.ParseTreeVisitor;
-
-import org.springframework.data.domain.Sort;
-import org.springframework.lang.Nullable;
-
-/**
- * Operations needed to parse a JPA query.
- *
- * @author Greg Turnquist
- * @author Mark Paluch
- * @since 3.1
- */
-abstract class JpaQueryParser {
-
- private final ParserRuleContext context;
- private final ParsedQueryIntrospector introspector;
- private final String projection;
- private final BiFunction> sortFunction;
- private final BiFunction> countQueryFunction;
-
- JpaQueryParser(ParserRuleContext context, ParsedQueryIntrospector introspector,
- @Nullable BiFunction> sortFunction,
- @Nullable BiFunction> countQueryFunction) {
-
- this.context = context;
- this.introspector = introspector;
- this.sortFunction = sortFunction;
- this.countQueryFunction = countQueryFunction;
- this.introspector.visit(context);
-
- List tokens = introspector.getProjection();
- this.projection = tokens.isEmpty() ? "" : render(tokens);
- }
-
- static ParserRuleContext parse(String query, Function lexerFactoryFunction,
- Function parserFactoryFunction, Function parseFunction) {
-
- Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query));
- P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer));
-
- configureParser(query, lexer, parser);
-
- return parseFunction.apply(parser);
- }
-
- /**
- * Generate a query using the original query with an {@literal order by} clause added (or amended) based upon the
- * provider {@link Sort} parameter.
- *
- * @param sort can be {@literal null}
- */
- String renderSortedQuery(Sort sort) {
- return render(sortFunction.apply(sort, findAlias()).visit(context));
- }
-
- /**
- * Generate a count-based query derived from the original query.
- *
- * @param countProjection
- */
- String createCountQuery(@Nullable String countProjection) {
- return render(countQueryFunction.apply(countProjection, findAlias()).visit(context));
- }
-
- /**
- * Find the projection of the query.
- */
- String getProjection() {
- return this.projection;
- }
-
- /**
- * Find the alias of the query's primary FROM clause
- *
- * @return can be {@literal null}
- */
- @Nullable
- String findAlias() {
- return this.introspector.getAlias();
- }
-
- /**
- * Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause.
- *
- * @return Guaranteed to be {@literal true} or {@literal false}.
- */
- boolean hasConstructorExpression() {
- return this.introspector.hasConstructorExpression();
- }
-
- /**
- * Apply common configuration (SLL prediction for performance, our own error listeners).
- *
- * @param query
- * @param lexer
- * @param parser
- */
- static void configureParser(String query, Lexer lexer, Parser parser) {
-
- BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query);
-
- lexer.removeErrorListeners();
- lexer.addErrorListener(errorListener);
-
- parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
-
- parser.removeErrorListeners();
- parser.addErrorListener(errorListener);
- }
-
-}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java
deleted file mode 100644
index 39276bcf5..000000000
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2022-2024 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.jpa.repository.query;
-
-/**
- * Implements the {@code JPQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated
- * {@link JpqlParser} and {@link JpqlSortedQueryTransformer}.
- *
- * @author Greg Turnquist
- * @author Mark Paluch
- * @since 3.1
- */
-class JpqlQueryParser extends JpaQueryParser {
-
- private JpqlQueryParser(String query) {
- super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(),
- JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new);
- }
-
- /**
- * Parse a JPQL query.
- *
- * @param query
- * @return the query parser.
- * @throws BadJpqlGrammarException
- */
- public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException {
- return new JpqlQueryParser(query);
- }
-}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
index ebe0278b0..99bf03c43 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java
@@ -28,6 +28,7 @@ import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
+import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
/**
@@ -50,7 +51,7 @@ final class NamedQuery extends AbstractJpaQuery {
private final String countQueryName;
private final @Nullable String countProjection;
private final boolean namedCountQueryIsPresent;
- private final DeclaredQuery declaredQuery;
+ private final Lazy declaredQuery;
private final QueryParameterSetter.QueryMetadataCache metadataCache;
/**
@@ -75,11 +76,6 @@ final class NamedQuery extends AbstractJpaQuery {
this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName);
Query query = em.createNamedQuery(queryName);
- String queryString = extractor.extractQueryString(query);
-
- // TODO: Detect whether a named query is a named one.
- this.declaredQuery = DeclaredQuery.of(queryString, query != null && query.toString().contains("NativeQuery"));
-
boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters();
boolean cantExtractQuery = !extractor.canExtractQuery();
@@ -93,6 +89,10 @@ final class NamedQuery extends AbstractJpaQuery {
method));
}
+ String queryString = extractor.extractQueryString(query);
+
+ // TODO: Detect whether a named query is a native one.
+ this.declaredQuery = Lazy.of(() -> DeclaredQuery.of(queryString, query.toString().contains("NativeQuery")));
this.metadataCache = new QueryParameterSetter.QueryMetadataCache();
}
@@ -188,7 +188,7 @@ final class NamedQuery extends AbstractJpaQuery {
} else {
- String countQueryString = declaredQuery.deriveCountQuery(countProjection).getQueryString();
+ String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString();
cacheKey = countQueryString;
countQuery = em.createQuery(countQueryString, Long.class);
}
@@ -220,7 +220,7 @@ final class NamedQuery extends AbstractJpaQuery {
return type.isInterface() ? Tuple.class : null;
}
- return declaredQuery.hasConstructorExpression() //
+ return declaredQuery.get().hasConstructorExpression() //
? null //
: super.getTypeToRead(returnedType);
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java
index 52dfd37e2..a2bb681e1 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java
@@ -42,6 +42,8 @@ import org.springframework.lang.Nullable;
*/
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
+ private final boolean queryForEntity;
+
/**
* Creates a new {@link NativeJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}.
*
@@ -57,6 +59,8 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser);
+ this.queryForEntity = getQueryMethod().isQueryForEntity();
+
Parameters, ?> parameters = method.getParameters();
if (parameters.hasSortParameter() && !queryString.contains("#sort")) {
@@ -77,9 +81,9 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
@Nullable
private Class> getTypeToQueryFor(ReturnedType returnedType) {
- Class> result = getQueryMethod().isQueryForEntity() ? returnedType.getDomainType() : null;
+ Class> result = queryForEntity ? returnedType.getDomainType() : null;
- if (this.getQuery().hasConstructorExpression() || this.getQuery().isDefaultProjection()) {
+ if (getQuery().hasConstructorExpression() || getQuery().isDefaultProjection()) {
return result;
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
index ec28f9e5a..2a84cce83 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java
@@ -30,23 +30,11 @@ import org.springframework.lang.Nullable;
public interface QueryEnhancer {
/**
- * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
+ * Returns whether the given JPQL query contains a constructor expression.
*
- * @param sort the sort specification to apply.
- * @return the modified query string.
+ * @return whether the given JPQL query contains a constructor expression.
*/
- default String applySorting(Sort sort) {
- return applySorting(sort, detectAlias());
- }
-
- /**
- * Adds {@literal order by} clause to the JPQL query.
- *
- * @param sort the sort specification to apply.
- * @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
- * @return the modified query string.
- */
- String applySorting(Sort sort, @Nullable String alias);
+ boolean hasConstructorExpression();
/**
* Resolves the alias for the entity to be retrieved from the given JPA query.
@@ -56,6 +44,47 @@ public interface QueryEnhancer {
@Nullable
String detectAlias();
+ /**
+ * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
+ *
+ * @return the projection part of the query.
+ */
+ String getProjection();
+
+ /**
+ * Returns the join aliases of the query.
+ *
+ * @return the join aliases of the query.
+ */
+ @Deprecated(forRemoval = true)
+ Set getJoinAliases();
+
+ /**
+ * Gets the query we want to use for enhancements.
+ *
+ * @return non-null {@link DeclaredQuery} that wraps the query
+ */
+ @Deprecated(forRemoval = true)
+ DeclaredQuery getQuery();
+
+ /**
+ * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
+ *
+ * @param sort the sort specification to apply.
+ * @return the modified query string.
+ */
+ String applySorting(Sort sort);
+
+ /**
+ * Adds {@literal order by} clause to the JPQL query.
+ *
+ * @param sort the sort specification to apply.
+ * @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
+ * @return the modified query string.
+ */
+ @Deprecated
+ String applySorting(Sort sort, @Nullable String alias);
+
/**
* Creates a count projected query from the given original query.
*
@@ -72,29 +101,4 @@ public interface QueryEnhancer {
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
*/
String createCountQueryFor(@Nullable String countProjection);
-
- /**
- * Returns whether the given JPQL query contains a constructor expression.
- *
- * @return whether the given JPQL query contains a constructor expression.
- */
- default boolean hasConstructorExpression() {
- return QueryUtils.hasConstructorExpression(getQuery().getQueryString());
- }
-
- /**
- * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
- *
- * @return the projection part of the query.
- */
- String getProjection();
-
- Set getJoinAliases();
-
- /**
- * Gets the query we want to use for enhancements.
- *
- * @return non-null {@link DeclaredQuery} that wraps the query
- */
- DeclaredQuery getQuery();
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
index ed57263e8..d8308d136 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
@@ -56,7 +56,7 @@ import org.springframework.util.StringUtils;
* @author Greg Turnquist
* @author Yuriy Tsarkov
*/
-class StringQuery implements DeclaredQuery {
+public class StringQuery implements DeclaredQuery {
private final String query;
private final List bindings;
@@ -64,13 +64,14 @@ class StringQuery implements DeclaredQuery {
private final boolean usesJdbcStyleParameters;
private final boolean isNative;
private final QueryEnhancer queryEnhancer;
+ private final boolean hasNamedParameters;
/**
* Creates a new {@link StringQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
- StringQuery(String query, boolean isNative) {
+ public StringQuery(String query, boolean isNative) {
Assert.hasText(query, "Query must not be null or empty");
@@ -83,24 +84,17 @@ class StringQuery implements DeclaredQuery {
this.bindings, queryMeta);
this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
-
this.queryEnhancer = QueryEnhancerFactory.forQuery(this);
- }
- // TODO: Conflict with eager JpaQueryMethod.assertParameterNamesInAnnotatedQuery validation that attempts parsing
- // without pre-processing the query leaving #{#entityName} substitution to a later time.
- public static boolean hasNamedParameter(String query) {
-
- if (ObjectUtils.isEmpty(query)) {
- return false;
+ boolean hasNamedParameters = false;
+ for (ParameterBinding parameterBinding : getParameterBindings()) {
+ if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) {
+ hasNamedParameters = true;
+ break;
+ }
}
- List parameterBindings = new ArrayList<>();
- Metadata queryMeta = new Metadata();
- ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
- parameterBindings, queryMeta);
-
- return parameterBindings.stream().anyMatch(b -> b.getIdentifier().hasName());
+ this.hasNamedParameters = hasNamedParameters;
}
/**
@@ -161,7 +155,7 @@ class StringQuery implements DeclaredQuery {
@Override
public boolean hasNamedParameter() {
- return bindings.stream().anyMatch(b -> b.getIdentifier().hasName());
+ return hasNamedParameters;
}
@Override
@@ -233,8 +227,8 @@ class StringQuery implements DeclaredQuery {
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
* the cleaned up query.
*/
- String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
- List bindings, Metadata queryMeta) {
+ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings,
+ Metadata queryMeta) {
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java
index 4f6752c17..241a5310b 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java
@@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
- * TCK Tests for {@link EqlQueryParser} mixed into {@link JpaQueryEnhancer}.
+ * TCK Tests for {@link JpaQueryEnhancer.EqlQueryParser} mixed into {@link JpaQueryEnhancer}.
*
* @author Greg Turnquist
*/
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java
index 18c42c875..52bdcd82d 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java
@@ -30,7 +30,8 @@ import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
/**
- * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link EqlQueryParser}.
+ * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
+ * {@link JpaQueryEnhancer.EqlQueryParser}.
*
* @author Greg Turnquist
*/
@@ -718,7 +719,7 @@ class EqlQueryTransformerTests {
@MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864
void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) {
- EqlQueryParser.parseQuery(String.format("""
+ JpaQueryEnhancer.EqlQueryParser.parseQuery(String.format("""
select u
from UserAccountEntity u
join u.lossInspectorLimitConfiguration lil
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java
index f19c4acc7..7d57ed37a 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java
@@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
- * TCK Tests for {@link HqlQueryParser} mixed into {@link JpaQueryEnhancer}.
+ * TCK Tests for {@link JpaQueryEnhancer.HqlQueryParser} mixed into {@link JpaQueryEnhancer}.
*
* @author Greg Turnquist
*/
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java
index 94f3d2691..5cc6b6ddb 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java
@@ -31,7 +31,8 @@ import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
/**
- * Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link HqlQueryParser}.
+ * Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
+ * {@link JpaQueryEnhancer.HqlQueryParser}.
*
* @author Greg Turnquist
* @author Christoph Strobl
@@ -869,7 +870,7 @@ class HqlQueryTransformerTests {
@MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864
void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) {
- HqlQueryParser.parseQuery(String.format("""
+ JpaQueryEnhancer.HqlQueryParser.parseQuery(String.format("""
select u
from UserAccountEntity u
join fetch u.lossInspectorLimitConfiguration lil
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java
index 58b5b6589..d5dc2b859 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java
@@ -45,14 +45,7 @@ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@ParameterizedTest // GH-2773
@MethodSource("jpqlCountQueries")
void shouldDeriveJpqlCountQuery(String query, String expected) {
-
- assumeThat(query).as("JSQLParser does not support simple JPQL syntax").doesNotStartWithIgnoringCase("FROM");
-
- assumeThat(query).as("JSQLParser does not support constructor JPQL syntax").doesNotContain(" new ");
-
- assumeThat(query).as("JSQLParser does not support MOD JPQL syntax").doesNotContain("MOD(");
-
- super.shouldDeriveJpqlCountQuery(query, expected);
+ assumeThat(query).as("JSQLParser does not support JPQL").isNull();
}
@Test
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java
index eee309518..4d54b1950 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java
@@ -34,6 +34,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
+
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -282,20 +283,6 @@ class JpaQueryMethodUnitTests {
assertThat(method.getNamedCountQueryName()).isEqualTo("HateoasAwareSpringDataWebConfiguration.bar.count");
}
- @Test // DATAJPA-185
- void rejectsInvalidNamedParameter() {
-
- assertThatThrownBy(() -> getQueryMethod(InvalidRepository.class, "findByAnnotatedQuery", String.class))
- .isInstanceOf(IllegalStateException.class)
- // Parameter from query
- .hasMessageContaining("foo")
- // Parameter name from annotation
- .hasMessageContaining("param")
- // Method name
- .hasMessageContaining("findByAnnotatedQuery");
-
- }
-
@Test // DATAJPA-207
@SuppressWarnings({ "rawtypes", "unchecked" })
void returnsTrueIfReturnTypeIsEntity() {
@@ -529,9 +516,6 @@ class JpaQueryMethodUnitTests {
@Modifying
void updateMethod(String firstname, Sort sort);
- // Typo in named parameter
- @Query("select u from User u where u.firstname = :foo")
- List findByAnnotatedQuery(@Param("param") String param);
}
interface ValidRepository extends Repository {
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java
index a026e9036..b867aba84 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java
@@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
- * TCK Tests for {@link JpqlQueryParser} mixed into {@link JpaQueryEnhancer}.
+ * TCK Tests for {@link JpaQueryEnhancer.JpqlQueryParser} mixed into {@link JpaQueryEnhancer}.
*
* @author Greg Turnquist
*/
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java
index 6bce914c7..eb04b8de0 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java
@@ -31,7 +31,7 @@ import org.springframework.lang.Nullable;
/**
* Verify that JPQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
- * {@link JpqlQueryParser}.
+ * {@link JpaQueryEnhancer.JpqlQueryParser}.
*
* @author Greg Turnquist
* @author Mark Paluch
@@ -736,7 +736,7 @@ class JpqlQueryTransformerTests {
@MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864
void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) {
- JpqlQueryParser.parseQuery(String.format("""
+ JpaQueryEnhancer.JpqlQueryParser.parseQuery(String.format("""
select u
from UserAccountEntity u
join u.lossInspectorLimitConfiguration lil
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java
index 61fe6a2aa..78fda1311 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java
@@ -39,7 +39,7 @@ class QueryEnhancerFactoryUnitTests {
JpaQueryEnhancer queryParsingEnhancer = (JpaQueryEnhancer) queryEnhancer;
- assertThat(queryParsingEnhancer.getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class);
+ assertThat(queryParsingEnhancer).isInstanceOf(JpaQueryEnhancer.HqlQueryParser.class);
}
@Test
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java
index 558dc747e..234ea4ec8 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java
@@ -50,6 +50,7 @@ import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.util.TypeInformation;
@@ -328,6 +329,9 @@ class SimpleJpaQueryUnitTests {
@Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
List findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable);
+ // Typo in named parameter
+ @Query("select u from User u where u.firstname = :foo")
+ List findByAnnotatedQuery(@Param("param") String param);
}
interface UserProjection {}