diff --git a/pom.xml b/pom.xml
index 9c462b755b..e996e76b01 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2989-SNAPSHOTpomSpring Data JPA Parent
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index 0bdf2c8e7e..7b73fe5cb8 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -5,12 +5,12 @@
org.springframework.dataspring-data-envers
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2989-SNAPSHOTorg.springframework.dataspring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2989-SNAPSHOT../pom.xml
diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml
index af5244a230..a74120da66 100644
--- a/spring-data-jpa-distribution/pom.xml
+++ b/spring-data-jpa-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.dataspring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2989-SNAPSHOT../pom.xml
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index b6470bdc89..11516db6ff 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -7,7 +7,7 @@
org.springframework.dataspring-data-jpa
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2989-SNAPSHOTSpring Data JPASpring Data module for JPA repositories.
@@ -16,7 +16,7 @@
org.springframework.dataspring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-2989-SNAPSHOT../pom.xml
diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java
index fd46a3f6c2..d1465ed1bc 100644
--- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java
+++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java
@@ -27,6 +27,8 @@
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
/**
* @author Mark Paluch
@@ -44,6 +46,7 @@ public static class BenchmarkParameters {
DeclaredQuery query;
Sort sort = Sort.by("foo");
QueryEnhancer enhancer;
+ QueryEnhancer.QueryRewriteInformation rewriteInformation;
@Setup(Level.Iteration)
public void doSetup() {
@@ -55,14 +58,16 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%'
OR p.description LIKE "cost overrun"
""";
- query = DeclaredQuery.of(s, false);
- enhancer = QueryEnhancerFactory.forQuery(query);
+ query = DeclaredQuery.jpqlQuery(s);
+ enhancer = QueryEnhancerFactory.forQuery(query).create(query);
+ rewriteInformation = new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()));
}
}
@Benchmark
public Object measure(BenchmarkParameters parameters) {
- return parameters.enhancer.applySorting(parameters.sort);
+ return parameters.enhancer.rewrite(parameters.rewriteInformation);
}
}
diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java
index 845282e319..f4121c28ed 100644
--- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java
+++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java
@@ -29,6 +29,8 @@
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
/**
* @author Mark Paluch
@@ -46,6 +48,7 @@ public static class BenchmarkParameters {
JSqlParserQueryEnhancer enhancer;
Sort sort = Sort.by("foo");
private byte[] serialized;
+ private QueryEnhancer.QueryRewriteInformation rewriteInformation;
@Setup(Level.Iteration)
public void doSetup() throws IOException {
@@ -56,13 +59,15 @@ public void doSetup() throws IOException {
select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE
union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE""";
- enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.of(s, true));
+ enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.nativeQuery(s));
+ rewriteInformation = new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()));
}
}
@Benchmark
public Object applySortWithParsing(BenchmarkParameters p) {
- return p.enhancer.applySorting(p.sort);
+ return p.enhancer.rewrite(p.rewriteInformation);
}
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java
index ffd6f55529..536ff5bca2 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java
@@ -15,27 +15,17 @@
*/
package org.springframework.data.jpa.repository;
-import jakarta.persistence.criteria.CriteriaBuilder;
-import jakarta.persistence.criteria.CriteriaQuery;
-import jakarta.persistence.criteria.Root;
-
-import java.util.Arrays;
-import java.util.Collection;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
-import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.jspecify.annotations.Nullable;
import org.springframework.dao.InvalidDataAccessApiUsageException;
-
-import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.DeleteSpecification;
import org.springframework.data.jpa.domain.PredicateSpecification;
@@ -115,7 +105,6 @@ default List findAll(PredicateSpecification spec) {
* Returns a {@link Page} of entities matching the given {@link Specification}.
*
* Supports counting the total number of entities matching the {@link Specification}.
- *
*
* @param spec can be {@literal null}, if no {@link Specification} is given all entities matching {@code } will be
* selected.
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java
index d10c90b68c..d12036c74b 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java
@@ -94,4 +94,5 @@
* Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query.
*/
String sqlResultSetMapping() default "";
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java
index 12ff41bb71..4405d29bbb 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java
@@ -90,4 +90,5 @@
* @since 3.0
*/
Class extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java
index 3ff333ea7c..22f32ed2de 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java
@@ -28,6 +28,7 @@
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
+import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
@@ -83,46 +84,39 @@
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
* for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
* for {@code PersonRepositoryImpl}.
- *
- * @return
*/
String repositoryImplementationPostfix() default "Impl";
/**
* Configures the location of where to find the Spring Data named queries properties file. Will default to
* {@code META-INF/jpa-named-queries.properties}.
- *
- * @return
*/
String namedQueriesLocation() default "";
/**
* Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
* {@link Key#CREATE_IF_NOT_FOUND}.
- *
- * @return
*/
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
/**
* Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
* {@link JpaRepositoryFactoryBean}.
- *
- * @return
*/
Class> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;
/**
* Configure the repository base class to be used to create repository proxies for this particular configuration.
*
- * @return
* @since 1.9
*/
Class> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
/**
* Configure a specific {@link BeanNameGenerator} to be used when creating the repository beans.
- * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate context default.
+ *
+ * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate
+ * context default.
* @since 3.4
*/
Class extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@@ -132,22 +126,18 @@
/**
* Configures the name of the {@link EntityManagerFactory} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {@code entityManagerFactory}.
- *
- * @return
*/
String entityManagerFactoryRef() default "entityManagerFactory";
/**
* Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {@code transactionManager}.
- *
- * @return
*/
String transactionManagerRef() default "transactionManager";
/**
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
- * repositories infrastructure.
+ * repository infrastructure.
*/
boolean considerNestedRepositories() default false;
@@ -169,7 +159,6 @@
* completed its bootstrap. {@link BootstrapMode#DEFERRED} is fundamentally the same as {@link BootstrapMode#LAZY},
* but triggers repository initialization when the application context finishes its bootstrap.
*
- * @return
* @since 2.1
*/
BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT;
@@ -181,4 +170,13 @@
* @return a single character used for escaping.
*/
char escapeCharacter() default '\\';
+
+ /**
+ * Configures the {@link QueryEnhancerSelector} to select a query enhancer for query introspection and transformation.
+ *
+ * @return a {@link QueryEnhancerSelector} class providing a no-args constructor.
+ * @since 4.0
+ */
+ Class extends QueryEnhancerSelector> queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class;
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
index 32b8670802..7abdd4758e 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
@@ -122,6 +122,11 @@ public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSo
}
builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\'));
builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME);
+
+ if (source instanceof AnnotationRepositoryConfigurationSource) {
+ builder.addPropertyValue("queryEnhancerSelector",
+ source.getAttribute("queryEnhancerSelector", Class.class).orElse(null));
+ }
}
@Override
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
index 055483523a..f8f26e48cc 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java
@@ -20,9 +20,9 @@
import java.util.Objects;
-import org.springframework.data.domain.Pageable;
-
import org.jspecify.annotations.Nullable;
+
+import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.jpa.repository.QueryRewriter;
@@ -32,7 +32,6 @@
import org.springframework.data.util.Lazy;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentLruCache;
-import org.springframework.util.StringUtils;
/**
* Base class for {@link String} based JPA queries.
@@ -49,8 +48,8 @@
*/
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
- private final DeclaredQuery query;
- private final Lazy countQuery;
+ private final EntityQuery query;
+ private final Lazy countQuery;
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryRewriter queryRewriter;
private final QuerySortRewriter querySortRewriter;
@@ -64,38 +63,50 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null}.
- * @param countQueryString must not be {@literal null}.
- * @param queryRewriter must not be {@literal null}.
- * @param valueExpressionDelegate must not be {@literal null}.
+ * @param countQuery can be {@literal null} if not defined.
+ * @param queryConfiguration must not be {@literal null}.
*/
- public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
- @Nullable String countQueryString, QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) {
+ AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
+ @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) {
+ this(method, em, method.getDeclaredQuery(queryString),
+ countQueryString != null ? method.getDeclaredQuery(countQueryString) : null, queryConfiguration);
+ }
+
+ /**
+ * Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
+ * query {@link String}.
+ *
+ * @param method must not be {@literal null}.
+ * @param em must not be {@literal null}.
+ * @param query must not be {@literal null}.
+ * @param countQuery can be {@literal null}.
+ * @param queryConfiguration must not be {@literal null}.
+ */
+ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query,
+ @Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) {
super(method, em);
- Assert.hasText(queryString, "Query string must not be null or empty");
- Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null");
- Assert.notNull(queryRewriter, "QueryRewriter must not be null");
+ Assert.notNull(query, "Query must not be null");
+ Assert.notNull(queryConfiguration, "JpaQueryConfiguration must not be null");
- this.valueExpressionDelegate = valueExpressionDelegate;
+ this.valueExpressionDelegate = queryConfiguration.getValueExpressionDelegate();
this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters());
- this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), valueExpressionDelegate,
- method.isNativeQuery());
- this.countQuery = Lazy.of(() -> {
+ this.query = TemplatedQuery.create(query, method.getEntityInformation(), queryConfiguration);
- if (StringUtils.hasText(countQueryString)) {
+ this.countQuery = Lazy.of(() -> {
- return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), valueExpressionDelegate,
- method.isNativeQuery());
+ if (countQuery != null) {
+ return TemplatedQuery.create(countQuery, method.getEntityInformation(), queryConfiguration);
}
- return query.deriveCountQuery(method.getCountQueryProjection());
+ return this.query.deriveCountQuery(method.getCountQueryProjection());
});
this.countParameterBinder = Lazy.of(() -> this.createBinder(this.countQuery.get()));
- this.queryRewriter = queryRewriter;
+ this.queryRewriter = queryConfiguration.getQueryRewriter(method);
JpaParameters parameters = method.getParameters();
@@ -109,25 +120,29 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
}
}
- Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(),
+ Assert.isTrue(method.isNativeQuery() || !this.query.usesJdbcStyleParameters(),
"JDBC style parameters (?) are not supported for JPA queries");
}
+ private DeclaredQuery createQuery(String queryString, boolean nativeQuery) {
+ return nativeQuery ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString);
+ }
+
@Override
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
Sort sort = accessor.getSort();
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
ReturnedType returnedType = processor.getReturnedType();
- String sortedQueryString = getSortedQueryString(sort, returnedType);
- Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), returnedType);
+ QueryProvider sortedQuery = getSortedQuery(sort, returnedType);
+ Query query = createJpaQuery(sortedQuery, sort, accessor.getPageable(), returnedType);
// it is ok to reuse the binding contained in the ParameterBinder, although we create a new query String because the
// parameters in the query do not change.
return parameterBinder.get().bindAndPrepare(query, accessor);
}
- String getSortedQueryString(Sort sort, ReturnedType returnedType) {
+ QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) {
return querySortRewriter.getSorted(query, sort, returnedType);
}
@@ -136,7 +151,7 @@ protected ParameterBinder createBinder() {
return createBinder(query);
}
- protected ParameterBinder createBinder(DeclaredQuery query) {
+ protected ParameterBinder createBinder(ParametrizedQuery query) {
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query,
valueExpressionDelegate, valueExpressionContextProvider);
}
@@ -160,14 +175,14 @@ protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) {
/**
* @return the query
*/
- public DeclaredQuery getQuery() {
+ public EntityQuery getQuery() {
return query;
}
/**
* @return the countQuery
*/
- public DeclaredQuery getCountQuery() {
+ public ParametrizedQuery getCountQuery() {
return countQuery.get();
}
@@ -175,20 +190,20 @@ public DeclaredQuery getCountQuery() {
* Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery}
* type.
*/
- protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
+ protected Query createJpaQuery(QueryProvider query, Sort sort, @Nullable Pageable pageable,
ReturnedType returnedType) {
EntityManager em = getEntityManager();
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
- return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable));
+ return em.createQuery(potentiallyRewriteQuery(query.getQueryString(), sort, pageable));
}
Class> typeToRead = getTypeToRead(returnedType);
return typeToRead == null //
- ? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable)) //
- : em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead);
+ ? em.createQuery(potentiallyRewriteQuery(query.getQueryString(), sort, pageable)) //
+ : em.createQuery(potentiallyRewriteQuery(query.getQueryString(), sort, pageable), typeToRead);
}
/**
@@ -207,9 +222,8 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla
: queryRewriter.rewrite(originalQuery, sort);
}
- String applySorting(CachableQuery cachableQuery) {
-
- return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery())
+ QueryProvider applySorting(CachableQuery cachableQuery) {
+ return cachableQuery.getDeclaredQuery()
.rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType()));
}
@@ -217,7 +231,7 @@ String applySorting(CachableQuery cachableQuery) {
* Query Sort Rewriter interface.
*/
interface QuerySortRewriter {
- String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType);
+ QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType);
}
/**
@@ -227,29 +241,28 @@ enum SimpleQuerySortRewriter implements QuerySortRewriter {
INSTANCE;
- public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
-
- return QueryEnhancerFactory.forQuery(query).rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
+ public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) {
+ return query.rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}
}
static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter {
- private volatile @Nullable String cachedQueryString;
+ private volatile @Nullable QueryProvider cachedQuery;
- public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
+ public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) {
if (sort.isSorted()) {
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
}
- String cachedQueryString = this.cachedQueryString;
- if (cachedQueryString == null) {
- this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query)
+ QueryProvider cachedQuery = this.cachedQuery;
+ if (cachedQuery == null) {
+ this.cachedQuery = cachedQuery = query
.rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}
- return cachedQueryString;
+ return cachedQuery;
}
}
@@ -258,22 +271,22 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp
*/
class CachingQuerySortRewriter implements QuerySortRewriter {
- private final ConcurrentLruCache queryCache = new ConcurrentLruCache<>(16,
+ private final ConcurrentLruCache queryCache = new ConcurrentLruCache<>(16,
AbstractStringBasedJpaQuery.this::applySorting);
- private volatile @Nullable String cachedQueryString;
+ private volatile @Nullable QueryProvider cachedQuery;
@Override
- public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
+ public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) {
if (sort.isUnsorted()) {
- String cachedQueryString = this.cachedQueryString;
- if (cachedQueryString == null) {
- this.cachedQueryString = cachedQueryString = queryCache.get(new CachableQuery(query, sort, returnedType));
+ QueryProvider cachedQuery = this.cachedQuery;
+ if (cachedQuery == null) {
+ this.cachedQuery = cachedQuery = queryCache.get(new CachableQuery(query, sort, returnedType));
}
- return cachedQueryString;
+ return cachedQuery;
}
return queryCache.get(new CachableQuery(query, sort, returnedType));
@@ -289,21 +302,21 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp
*/
static class CachableQuery {
- private final DeclaredQuery declaredQuery;
+ private final EntityQuery query;
private final String queryString;
private final Sort sort;
private final ReturnedType returnedType;
- CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) {
+ CachableQuery(EntityQuery query, Sort sort, ReturnedType returnedType) {
- this.declaredQuery = query;
+ this.query = query;
this.queryString = query.getQueryString();
this.sort = sort;
this.returnedType = returnedType;
}
- DeclaredQuery getDeclaredQuery() {
- return declaredQuery;
+ EntityQuery getDeclaredQuery() {
+ return query;
}
Sort getSort() {
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java
new file mode 100644
index 0000000000..2f6db9c5f7
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2025 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 org.springframework.util.ObjectUtils;
+
+/**
+ * Utility class encapsulating {@code DeclaredQuery} implementations.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 4.0
+ */
+class DeclaredQueries {
+
+ static final class JpqlQuery implements DeclaredQuery {
+
+ private final String jpql;
+
+ JpqlQuery(String jpql) {
+ this.jpql = jpql;
+ }
+
+ @Override
+ public boolean isNative() {
+ return false;
+ }
+
+ @Override
+ public String getQueryString() {
+ return jpql;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof JpqlQuery jpqlQuery)) {
+ return false;
+ }
+ return ObjectUtils.nullSafeEquals(jpql, jpqlQuery.jpql);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.nullSafeHashCode(jpql);
+ }
+
+ @Override
+ public String toString() {
+ return "JPQL[" + jpql + "]";
+ }
+
+ }
+
+ static final class NativeQuery implements DeclaredQuery {
+
+ private final String sql;
+
+ NativeQuery(String sql) {
+ this.sql = sql;
+ }
+
+ @Override
+ public boolean isNative() {
+ return true;
+ }
+
+ @Override
+ public String getQueryString() {
+ return sql;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof NativeQuery that)) {
+ return false;
+ }
+ return ObjectUtils.nullSafeEquals(sql, that.sql);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.nullSafeHashCode(sql);
+ }
+
+ @Override
+ public String toString() {
+ return "Native[" + sql + "]";
+ }
+
+ }
+
+ /**
+ * A rewritten {@link DeclaredQuery} holding a reference to its original query.
+ */
+ static class RewrittenQuery implements DeclaredQuery {
+
+ private final DeclaredQuery source;
+ private final String queryString;
+
+ public RewrittenQuery(DeclaredQuery source, String queryString) {
+ this.source = source;
+ this.queryString = queryString;
+ }
+
+ @Override
+ public boolean isNative() {
+ return source.isNative();
+ }
+
+ @Override
+ public String getQueryString() {
+ return queryString;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RewrittenQuery that)) {
+ return false;
+ }
+ return ObjectUtils.nullSafeEquals(queryString, that.queryString);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.nullSafeHashCode(queryString);
+ }
+
+ @Override
+ public String toString() {
+ return isNative() ? "Rewritten Native[" + queryString + "]" : "Rewritten JPQL[" + queryString + "]";
+ }
+
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
index 0e6f760ed3..2cea734dbc 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java
@@ -15,100 +15,71 @@
*/
package org.springframework.data.jpa.repository.query;
-import java.util.List;
-
-import org.springframework.util.ObjectUtils;
-
-import org.jspecify.annotations.Nullable;
-
/**
- * A wrapper for a String representation of a query offering information about the query.
+ * Interface defining the contract to represent a declared query.
+ *
+ * Declared queries consist of a query string and a flag whether the query is a native (SQL) one or a JPQL query.
+ * Queries can be rewritten to contain a different query string (i.e. count query derivation, sorting, projection
+ * updates) while retaining their {@link #isNative() native} flag.
*
* @author Jens Schauder
* @author Diego Krupitza
+ * @author Mark Paluch
* @since 2.0.3
*/
-interface DeclaredQuery {
+public interface DeclaredQuery extends QueryProvider {
/**
- * Creates a {@literal DeclaredQuery} from a query {@literal String}.
+ * Creates a DeclaredQuery for a JPQL query.
*
- * @param query might be {@literal null} or empty.
- * @param nativeQuery is a given query is native or not
- * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument.
+ * @param jpql the JPQL query string.
+ * @return new instance of {@link DeclaredQuery}.
*/
- static DeclaredQuery of(@Nullable String query, boolean nativeQuery) {
- return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery);
+ static DeclaredQuery jpqlQuery(String jpql) {
+ return new DeclaredQueries.JpqlQuery(jpql);
}
/**
- * @return whether the underlying query has at least one named parameter.
- */
- boolean hasNamedParameter();
-
- /**
- * Returns the query string.
- */
- String getQueryString();
-
- /**
- * Returns the main alias used in the query.
+ * Creates a DeclaredQuery for a native query.
*
- * @return the alias
+ * @param sql the native query string.
+ * @return new instance of {@link DeclaredQuery}.
*/
- @Nullable
- String getAlias();
+ static DeclaredQuery nativeQuery(String sql) {
+ return new DeclaredQueries.NativeQuery(sql);
+ }
/**
- * Returns whether the query is using a constructor expression.
+ * Return whether the query is a native query of not.
*
- * @since 1.10
- */
- boolean hasConstructorExpression();
-
- /**
- * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
- */
- boolean isDefaultProjection();
-
- /**
- * Returns the {@link ParameterBinding}s registered.
+ * @return {@literal true} if native query; {@literal false} if it is a JPQL query.
*/
- List getParameterBindings();
+ boolean isNative();
/**
- * Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to be
- * expected from the original query, either derived from the query wrapped by this instance or from the information
- * passed as arguments.
+ * Return whether the query is a JPQL query of not.
*
- * @param countQueryProjection an optional return type for the query.
- * @return a new {@literal DeclaredQuery} instance.
- */
- DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection);
-
- /**
- * @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
- * @since 2.0.6
+ * @return {@literal true} if JPQL query; {@literal false} if it is a native query.
+ * @since 4.0
*/
- default boolean usesPaging() {
- return false;
+ default boolean isJpql() {
+ return !isNative();
}
/**
- * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
- * name.
+ * Rewrite a query string using a new query string retaining its source and {@link #isNative() native} flag.
*
- * @return Whether the query uses JDBC style parameters.
- * @since 2.0.6
+ * @param newQueryString the new query string.
+ * @return the rewritten {@link DeclaredQuery}.
+ * @since 4.0
*/
- boolean usesJdbcStyleParameters();
+ default DeclaredQuery rewrite(String newQueryString) {
- /**
- * Return whether the query is a native query of not.
- *
- * @return true if native query otherwise false
- */
- default boolean isNativeQuery() {
- return false;
+ if (getQueryString().equals(newQueryString)) {
+ return this;
+ }
+
+ return new DeclaredQueries.RewrittenQuery(this, newQueryString);
}
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java
new file mode 100644
index 0000000000..bde36d1535
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2025 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 java.util.List;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Encapsulation of a JPA query string, typically returning entities or DTOs. Provides access to parameter bindings.
+ *
+ * The internal {@link PreprocessedQuery query string} is cleaned from decorated parameters like {@literal %:lastname%}
+ * and the matching bindings take care of applying the decorations in the {@link ParameterBinding#prepare(Object)}
+ * method. Note that this class also handles replacing SpEL expressions with synthetic bind parameters.
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Oliver Wehrens
+ * @author Mark Paluch
+ * @author Jens Schauder
+ * @author Diego Krupitza
+ * @author Greg Turnquist
+ * @author Yuriy Tsarkov
+ * @since 4.0
+ */
+class DefaultEntityQuery implements EntityQuery, DeclaredQuery {
+
+ private final PreprocessedQuery query;
+ private final QueryEnhancer queryEnhancer;
+
+ DefaultEntityQuery(PreprocessedQuery query, QueryEnhancerFactory queryEnhancerFactory) {
+ this.query = query;
+ this.queryEnhancer = queryEnhancerFactory.create(query);
+ }
+
+ @Override
+ public boolean isNative() {
+ return query.isNative();
+ }
+
+ @Override
+ public String getQueryString() {
+ return query.getQueryString();
+ }
+
+ /**
+ * Returns whether we have found some like bindings.
+ */
+ @Override
+ public boolean hasParameterBindings() {
+ return this.query.hasBindings();
+ }
+
+ @Override
+ public boolean usesJdbcStyleParameters() {
+ return query.usesJdbcStyleParameters();
+ }
+
+ @Override
+ public boolean hasNamedParameter() {
+ return query.hasNamedBindings();
+ }
+
+ @Override
+ public List getParameterBindings() {
+ return this.query.getBindings();
+ }
+
+ @Override
+ public boolean hasConstructorExpression() {
+ return queryEnhancer.hasConstructorExpression();
+ }
+
+ @Override
+ public boolean isDefaultProjection() {
+ return queryEnhancer.getProjection().equalsIgnoreCase(getAlias());
+ }
+
+ @Nullable
+ String getAlias() {
+ return queryEnhancer.detectAlias();
+ }
+
+ @Override
+ public boolean usesPaging() {
+ return query.containsPageableInSpel();
+ }
+
+ String getProjection() {
+ return this.queryEnhancer.getProjection();
+ }
+
+ @Override
+ public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) {
+ return new SimpleParametrizedQuery(this.query.rewrite(queryEnhancer.createCountQueryFor(countQueryProjection)));
+ }
+
+ @Override
+ public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) {
+ return this.query.rewrite(queryEnhancer.rewrite(rewriteInformation));
+ }
+
+ @Override
+ public String toString() {
+ return "EntityQuery[" + getQueryString() + ", " + getParameterBindings() + ']';
+ }
+
+ /**
+ * Simple {@link ParametrizedQuery} variant forwarding to {@link PreprocessedQuery}.
+ */
+ static class SimpleParametrizedQuery implements ParametrizedQuery {
+
+ private final PreprocessedQuery query;
+
+ SimpleParametrizedQuery(PreprocessedQuery query) {
+ this.query = query;
+ }
+
+ @Override
+ public String getQueryString() {
+ return query.getQueryString();
+ }
+
+ @Override
+ public boolean hasParameterBindings() {
+ return query.hasBindings();
+ }
+
+ @Override
+ public boolean usesJdbcStyleParameters() {
+ return query.usesJdbcStyleParameters();
+ }
+
+ @Override
+ public boolean hasNamedParameter() {
+ return query.hasNamedBindings();
+ }
+
+ @Override
+ public List getParameterBindings() {
+ return query.getBindings();
+ }
+
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
index 1fe6236621..456c3139b3 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java
@@ -15,10 +15,6 @@
*/
package org.springframework.data.jpa.repository.query;
-import java.util.Set;
-
-import org.springframework.data.domain.Sort;
-
import org.jspecify.annotations.Nullable;
/**
@@ -27,30 +23,18 @@
* @author Diego Krupitza
* @since 2.7.0
*/
-public class DefaultQueryEnhancer implements QueryEnhancer {
+class DefaultQueryEnhancer implements QueryEnhancer {
- private final DeclaredQuery query;
+ private final QueryProvider query;
private final boolean hasConstructorExpression;
private final @Nullable String alias;
private final String projection;
- private final Set joinAliases;
- public DefaultQueryEnhancer(DeclaredQuery query) {
+ public DefaultQueryEnhancer(QueryProvider query) {
this.query = query;
this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString());
this.alias = QueryUtils.detectAlias(query.getQueryString());
this.projection = QueryUtils.getProjection(this.query.getQueryString());
- this.joinAliases = QueryUtils.getOuterJoinAliases(this.query.getQueryString());
- }
-
- @Override
- public String applySorting(Sort sort) {
- return QueryUtils.applySorting(this.query.getQueryString(), sort, this.alias);
- }
-
- @Override
- public String applySorting(Sort sort, @Nullable String alias) {
- return QueryUtils.applySorting(this.query.getQueryString(), sort, alias);
}
@Override
@@ -60,7 +44,9 @@ public String rewrite(QueryRewriteInformation rewriteInformation) {
@Override
public String createCountQueryFor(@Nullable String countProjection) {
- return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery());
+
+ boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNative() : true;
+ return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, nativeQuery);
}
@Override
@@ -79,12 +65,8 @@ public String getProjection() {
}
@Override
- public Set getJoinAliases() {
- return this.joinAliases;
- }
-
- @Override
- public DeclaredQuery getQuery() {
+ public QueryProvider getQuery() {
return this.query;
}
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java
similarity index 72%
rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java
index 95693e8808..a0ef2363b6 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java
@@ -21,29 +21,38 @@
import org.jspecify.annotations.Nullable;
/**
- * NULL-Object pattern implementation for {@link DeclaredQuery}.
+ * NULL-Object pattern implementation for {@link ParametrizedQuery}.
*
* @author Jens Schauder
+ * @author Mark Paluch
* @since 2.0.3
*/
-class EmptyDeclaredQuery implements DeclaredQuery {
+enum EmptyIntrospectedQuery implements EntityQuery {
- /**
- * An implementation implementing the NULL-Object pattern for situations where there is no query.
- */
- static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery();
+ INSTANCE;
+
+ EmptyIntrospectedQuery() {}
@Override
- public boolean hasNamedParameter() {
+ public boolean hasParameterBindings() {
return false;
}
@Override
- public String getQueryString() {
- return "";
+ public boolean usesJdbcStyleParameters() {
+ return false;
}
@Override
+ public boolean hasNamedParameter() {
+ return false;
+ }
+
+ @Override
+ public List getParameterBindings() {
+ return Collections.emptyList();
+ }
+
public @Nullable String getAlias() {
return null;
}
@@ -59,17 +68,23 @@ public boolean isDefaultProjection() {
}
@Override
- public List getParameterBindings() {
- return Collections.emptyList();
+ public String getQueryString() {
+ return "";
}
@Override
- public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) {
- return EMPTY_QUERY;
+ public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) {
+ return INSTANCE;
}
@Override
- public boolean usesJdbcStyleParameters() {
- return false;
+ public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) {
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "";
}
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java
new file mode 100644
index 0000000000..b28fa9f10d
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018-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 org.jspecify.annotations.Nullable;
+
+/**
+ * An extension to {@link ParametrizedQuery} exposing query information about its inner structure such as whether
+ * constructor expressions (JPQL) are used or the default projection is used.
+ *
+ * Entity Queries support derivation of {@link #deriveCountQuery(String) count queries} from the original query. They
+ * also can be used to rewrite the query using sorting and projection selection.
+ *
+ * @author Jens Schauder
+ * @author Diego Krupitza
+ * @since 4.0
+ */
+interface EntityQuery extends ParametrizedQuery {
+
+ /**
+ * Create a new {@link EntityQuery} given {@link DeclaredQuery} and {@link QueryEnhancerSelector}.
+ *
+ * @param query must not be {@literal null}.
+ * @param selector must not be {@literal null}.
+ * @return a new {@link EntityQuery}.
+ */
+ static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) {
+
+ PreprocessedQuery preparsed = PreprocessedQuery.parse(query);
+ QueryEnhancerFactory enhancerFactory = selector.select(preparsed);
+
+ return new DefaultEntityQuery(preparsed, enhancerFactory);
+ }
+
+ /**
+ * Returns whether the query is using a constructor expression.
+ *
+ * @since 1.10
+ */
+ boolean hasConstructorExpression();
+
+ /**
+ * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
+ */
+ boolean isDefaultProjection();
+
+ /**
+ * @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
+ * @since 2.0.6
+ */
+ default boolean usesPaging() {
+ return false;
+ }
+
+ /**
+ * Creates a new {@literal IntrospectedQuery} representing a count query, i.e. a query returning the number of rows to
+ * be expected from the original query, either derived from the query wrapped by this instance or from the information
+ * passed as arguments.
+ *
+ * @param countQueryProjection an optional return type for the query.
+ * @return a new {@literal IntrospectedQuery} instance.
+ */
+ ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection);
+
+ /**
+ * Rewrite the query using the given
+ * {@link org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation} into a sorted query or
+ * using a different projection. The rewritten query retains parameter binding characteristics.
+ *
+ * @param rewriteInformation query rewrite information (sorting, projection) to use.
+ * @return the rewritten query.
+ */
+ QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation);
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
index f68443adda..82cae525c1 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java
@@ -70,7 +70,7 @@
*/
public class JSqlParserQueryEnhancer implements QueryEnhancer {
- private final DeclaredQuery query;
+ private final QueryProvider query;
private final Statement statement;
private final ParsedType parsedType;
private final boolean hasConstructorExpression;
@@ -83,7 +83,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
/**
* @param query the query we want to enhance. Must not be {@literal null}.
*/
- public JSqlParserQueryEnhancer(DeclaredQuery query) {
+ public JSqlParserQueryEnhancer(QueryProvider query) {
this.query = query;
this.statement = parseStatement(query.getQueryString(), Statement.class);
@@ -285,35 +285,20 @@ public String getProjection() {
return this.projection;
}
- @Override
- public Set getJoinAliases() {
- return joinAliases;
- }
-
public Set getSelectionAliases() {
return selectAliases;
}
@Override
- public DeclaredQuery getQuery() {
+ public QueryProvider getQuery() {
return this.query;
}
- @Override
- public String applySorting(Sort sort) {
- return doApplySorting(sort, detectAlias());
- }
-
@Override
public String rewrite(QueryRewriteInformation rewriteInformation) {
return doApplySorting(rewriteInformation.getSort(), primaryAlias);
}
- @Override
- public String applySorting(Sort sort, @Nullable String alias) {
- return doApplySorting(sort, alias);
- }
-
private String doApplySorting(Sort sort, @Nullable String alias) {
String queryString = query.getQueryString();
Assert.hasText(queryString, "Query must not be null or empty");
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java
new file mode 100644
index 0000000000..788c977f25
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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 org.springframework.data.jpa.repository.QueryRewriter;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+
+/**
+ * Configuration object holding configuration information for JPA queries within a repository.
+ *
+ * @author Mark Paluch
+ */
+public class JpaQueryConfiguration {
+
+ private final QueryRewriterProvider queryRewriter;
+ private final QueryEnhancerSelector selector;
+ private final EscapeCharacter escapeCharacter;
+ private final ValueExpressionDelegate valueExpressionDelegate;
+
+ public JpaQueryConfiguration(QueryRewriterProvider queryRewriter, QueryEnhancerSelector selector,
+ ValueExpressionDelegate valueExpressionDelegate, EscapeCharacter escapeCharacter) {
+
+ this.queryRewriter = queryRewriter;
+ this.selector = selector;
+ this.escapeCharacter = escapeCharacter;
+ this.valueExpressionDelegate = valueExpressionDelegate;
+ }
+
+ public QueryRewriter getQueryRewriter(JpaQueryMethod queryMethod) {
+ return queryRewriter.getQueryRewriter(queryMethod);
+ }
+
+ public QueryEnhancerSelector getSelector() {
+ return selector;
+ }
+
+ public EscapeCharacter getEscapeCharacter() {
+ return escapeCharacter;
+ }
+
+ public ValueExpressionDelegate getValueExpressionDelegate() {
+ return valueExpressionDelegate;
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java
index 1b57f4beb0..04d134c0ad 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java
@@ -16,7 +16,6 @@
package org.springframework.data.jpa.repository.query;
import java.util.List;
-import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -36,7 +35,6 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.ReturnedType;
-import org.springframework.util.Assert;
/**
* Implementation of {@link QueryEnhancer} to enhance JPA queries using ANTLR parsers.
@@ -55,11 +53,11 @@ class JpaQueryEnhancer implements QueryEnhancer {
private final Q queryInformation;
private final String projection;
private final SortedQueryRewriteFunction sortFunction;
- private final BiFunction> countQueryFunction;
+ private final BiFunction<@Nullable String, Q, ParseTreeVisitor> countQueryFunction;
JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector,
SortedQueryRewriteFunction sortFunction,
- BiFunction> countQueryFunction) {
+ BiFunction<@Nullable String, Q, ParseTreeVisitor> countQueryFunction) {
this.context = context;
this.sortFunction = sortFunction;
@@ -142,43 +140,34 @@ static void configureParser(String query, String grammar, Lexer lexer, Parser pa
}
/**
- * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using JPQL grammar.
+ * Factory method to create a {@link JpaQueryEnhancer} for {@link ParametrizedQuery} using JPQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using JPQL.
*/
- public static JpaQueryEnhancer forJpql(DeclaredQuery query) {
-
- Assert.notNull(query, "DeclaredQuery must not be null!");
-
- return JpqlQueryParser.parseQuery(query.getQueryString());
+ public static JpaQueryEnhancer forJpql(String query) {
+ return JpqlQueryParser.parseQuery(query);
}
/**
- * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using HQL grammar.
+ * Factory method to create a {@link JpaQueryEnhancer} for {@link ParametrizedQuery} using HQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using HQL.
*/
- public static JpaQueryEnhancer forHql(DeclaredQuery query) {
-
- Assert.notNull(query, "DeclaredQuery must not be null!");
-
- return HqlQueryParser.parseQuery(query.getQueryString());
+ public static JpaQueryEnhancer forHql(String query) {
+ return HqlQueryParser.parseQuery(query);
}
/**
- * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using EQL grammar.
+ * Factory method to create a {@link JpaQueryEnhancer} for {@link ParametrizedQuery} using EQL grammar.
*
* @param query must not be {@literal null}.
* @return a new {@link JpaQueryEnhancer} using EQL.
* @since 3.2
*/
- public static JpaQueryEnhancer forEql(DeclaredQuery query) {
-
- Assert.notNull(query, "DeclaredQuery must not be null!");
-
- return EqlQueryParser.parseQuery(query.getQueryString());
+ public static JpaQueryEnhancer forEql(String query) {
+ return EqlQueryParser.parseQuery(query);
}
/**
@@ -206,8 +195,7 @@ public boolean 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.
+ * Resolves the alias for the entity in the FROM clause from the JPA query.
*/
@Override
public @Nullable String detectAlias() {
@@ -215,24 +203,13 @@ public boolean hasConstructorExpression() {
}
/**
- * 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.
+ * Looks up the projection of the JPA query.
*/
@Override
public String getProjection() {
return this.projection;
}
- /**
- * Since the parser 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 query parser.
*/
@@ -241,17 +218,6 @@ public DeclaredQuery getQuery() {
throw new UnsupportedOperationException();
}
- /**
- * Adds an {@literal order by} clause to the JPA query.
- *
- * @param sort the sort specification to apply.
- * @return
- */
- @Override
- public String applySorting(Sort sort) {
- return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, this.queryInformation, null).visit(context));
- }
-
@Override
public String rewrite(QueryRewriteInformation rewriteInformation) {
return QueryRenderer.TokenRenderer.render(
@@ -259,28 +225,6 @@ public String rewrite(QueryRewriteInformation rewriteInformation) {
.visit(context));
}
- /**
- * Because the parser can find the alias of the FROM clause, there is no need to "find it" in advance.
- *
- * @param sort the sort specification to apply.
- * @param alias IGNORED
- * @return
- */
- @Override
- public String applySorting(Sort sort, @Nullable String alias) {
- return applySorting(sort);
- }
-
- /**
- * Creates a count query from the original query, with no count projection.
- *
- * @return Guaranteed to be not {@literal null};
- */
- @Override
- public String createCountQueryFor() {
- return createCountQueryFor(null);
- }
-
/**
* Create a count query from the original query, with potential custom projection.
*
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java
deleted file mode 100644
index 384330af14..0000000000
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2013-2025 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 jakarta.persistence.EntityManager;
-
-import org.springframework.data.jpa.repository.QueryRewriter;
-
-import org.jspecify.annotations.Nullable;
-import org.springframework.data.repository.query.QueryCreationException;
-import org.springframework.data.repository.query.RepositoryQuery;
-import org.springframework.data.repository.query.ValueExpressionDelegate;
-
-/**
- * Factory to create the appropriate {@link RepositoryQuery} for a {@link JpaQueryMethod}.
- *
- * @author Thomas Darimont
- * @author Mark Paluch
- */
-enum JpaQueryFactory {
-
- INSTANCE;
-
- /**
- * Creates a {@link RepositoryQuery} from the given {@link String} query.
- */
- AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString,
- @Nullable String countQueryString, QueryRewriter queryRewriter,
- ValueExpressionDelegate valueExpressionDelegate) {
-
- if (method.isScrollQuery()) {
- throw QueryCreationException.create(method, "Scroll queries are not supported using String-based queries");
- }
-
- return method.isNativeQuery()
- ? new NativeJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate)
- : new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate);
- }
-
- /**
- * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query.
- *
- * @param method must not be {@literal null}.
- * @param em must not be {@literal null}.
- * @return
- */
- public StoredProcedureJpaQuery fromProcedureAnnotation(JpaQueryMethod method, EntityManager em) {
-
- if (method.isScrollQuery()) {
- throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures");
- }
-
- return new StoredProcedureJpaQuery(method, em);
- }
-}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
index db4c492eb7..719e838fe0 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
@@ -24,15 +24,14 @@
import org.jspecify.annotations.Nullable;
import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
-import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -70,33 +69,31 @@ private abstract static class AbstractQueryLookupStrategy implements QueryLookup
private final EntityManager em;
private final JpaQueryMethodFactory queryMethodFactory;
- private final QueryRewriterProvider queryRewriterProvider;
+ private final JpaQueryConfiguration configuration;
/**
* Creates a new {@link AbstractQueryLookupStrategy}.
*
* @param em must not be {@literal null}.
* @param queryMethodFactory must not be {@literal null}.
+ * @param configuration must not be {@literal null}.
*/
public AbstractQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
- QueryRewriterProvider queryRewriterProvider) {
-
- Assert.notNull(em, "EntityManager must not be null");
- Assert.notNull(queryMethodFactory, "JpaQueryMethodFactory must not be null");
+ JpaQueryConfiguration configuration) {
this.em = em;
this.queryMethodFactory = queryMethodFactory;
- this.queryRewriterProvider = queryRewriterProvider;
+ this.configuration = configuration;
}
@Override
public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
JpaQueryMethod queryMethod = queryMethodFactory.build(method, metadata, factory);
- return resolveQuery(queryMethod, queryRewriterProvider.getQueryRewriter(queryMethod), em, namedQueries);
+ return resolveQuery(queryMethod, configuration, em, namedQueries);
}
- protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter,
+ protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration,
EntityManager em, NamedQueries namedQueries);
}
@@ -109,20 +106,16 @@ protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewr
*/
private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy {
- private final EscapeCharacter escape;
-
public CreateQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
- QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) {
+ JpaQueryConfiguration configuration) {
- super(em, queryMethodFactory, queryRewriterProvider);
-
- this.escape = escape;
+ super(em, queryMethodFactory, configuration);
}
@Override
- protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em,
+ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em,
NamedQueries namedQueries) {
- return new PartTreeJpaQuery(method, em, escape);
+ return new PartTreeJpaQuery(method, em, configuration.getEscapeCharacter());
}
}
@@ -134,59 +127,62 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer
* @author Thomas Darimont
* @author Jens Schauder
*/
- private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {
-
- private final ValueExpressionDelegate valueExpressionDelegate;
+ static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy {
/**
* Creates a new {@link DeclaredQueryLookupStrategy}.
*
* @param em must not be {@literal null}.
* @param queryMethodFactory must not be {@literal null}.
- * @param delegate must not be {@literal null}.
- * @param queryRewriterProvider must not be {@literal null}.
+ * @param configuration must not be {@literal null}.
*/
public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
- ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider) {
+ JpaQueryConfiguration configuration) {
- super(em, queryMethodFactory, queryRewriterProvider);
-
- this.valueExpressionDelegate = delegate;
+ super(em, queryMethodFactory, configuration);
}
@Override
- protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em,
+ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em,
NamedQueries namedQueries) {
if (method.isProcedureQuery()) {
- return JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);
+ return createProcedureQuery(method, em);
}
- if (StringUtils.hasText(method.getAnnotatedQuery())) {
+ if (method.hasAnnotatedQuery()) {
if (method.hasAnnotatedQueryName()) {
LOG.warn(String.format(
"Query method %s is annotated with both, a query and a query name; Using the declared query", method));
}
- return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(),
- getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate);
+ return createStringQuery(method, em, method.getRequiredDeclaredQuery(),
+ getCountQuery(method, namedQueries, em), configuration);
}
String name = method.getNamedQueryName();
+
if (namedQueries.hasQuery(name)) {
- return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
- getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate);
+ return createStringQuery(method, em, method.getDeclaredQuery(namedQueries.getQuery(name)),
+ getCountQuery(method, namedQueries, em),
+ configuration);
}
- RepositoryQuery query = NamedQuery.lookupFrom(method, em);
+ RepositoryQuery query = NamedQuery.lookupFrom(method, em, configuration.getSelector());
- return query != null //
- ? query //
- : NO_QUERY;
+ return query != null ? query : NO_QUERY;
}
- private @Nullable String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) {
+ private @Nullable DeclaredQuery getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) {
+
+ String query = doGetCountQuery(method, namedQueries, em);
+
+ return StringUtils.hasText(query) ? method.getDeclaredQuery(query) : null;
+ }
+
+ private static @Nullable String doGetCountQuery(JpaQueryMethod method, NamedQueries namedQueries,
+ EntityManager em) {
if (StringUtils.hasText(method.getCountQuery())) {
return method.getCountQuery();
@@ -210,6 +206,44 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer
return null;
}
+
+ /**
+ * Creates a {@link RepositoryQuery} from the given {@link String} query.
+ *
+ * @param method must not be {@literal null}.
+ * @param em must not be {@literal null}.
+ * @param query must not be {@literal null}.
+ * @param countQuery can be {@literal null} if not defined.
+ * @param configuration must not be {@literal null}.
+ * @return
+ */
+ static AbstractJpaQuery createStringQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query,
+ @Nullable DeclaredQuery countQuery, JpaQueryConfiguration configuration) {
+
+ if (method.isScrollQuery()) {
+ throw QueryCreationException.create(method, "Scroll queries are not supported using String-based queries");
+ }
+
+ return method.isNativeQuery() ? new NativeJpaQuery(method, em, query, countQuery, configuration)
+ : new SimpleJpaQuery(method, em, query, countQuery, configuration);
+ }
+
+ /**
+ * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query.
+ *
+ * @param method must not be {@literal null}.
+ * @param em must not be {@literal null}.
+ * @return
+ */
+ static StoredProcedureJpaQuery createProcedureQuery(JpaQueryMethod method, EntityManager em) {
+
+ if (method.isScrollQuery()) {
+ throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures");
+ }
+
+ return new StoredProcedureJpaQuery(method, em);
+ }
+
}
/**
@@ -232,31 +266,29 @@ private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLo
* @param queryMethodFactory must not be {@literal null}.
* @param createStrategy must not be {@literal null}.
* @param lookupStrategy must not be {@literal null}.
+ * @param configuration must not be {@literal null}.
*/
public CreateIfNotFoundQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy,
- QueryRewriterProvider queryRewriterProvider) {
-
- super(em, queryMethodFactory, queryRewriterProvider);
+ JpaQueryConfiguration configuration) {
- Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null");
- Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null");
+ super(em, queryMethodFactory, configuration);
this.createStrategy = createStrategy;
this.lookupStrategy = lookupStrategy;
}
@Override
- protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em,
+ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em,
NamedQueries namedQueries) {
- RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, queryRewriter, em, namedQueries);
+ RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, configuration, em, namedQueries);
if (lookupQuery != NO_QUERY) {
return lookupQuery;
}
- return createStrategy.resolveQuery(method, queryRewriter, em, namedQueries);
+ return createStrategy.resolveQuery(method, configuration, em, namedQueries);
}
}
@@ -266,25 +298,20 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer
* @param em must not be {@literal null}.
* @param queryMethodFactory must not be {@literal null}.
* @param key may be {@literal null}.
- * @param delegate must not be {@literal null}.
- * @param queryRewriterProvider must not be {@literal null}.
- * @param escape must not be {@literal null}.
+ * @param configuration must not be {@literal null}.
*/
public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory,
- @Nullable Key key, ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider,
- EscapeCharacter escape) {
+ @Nullable Key key, JpaQueryConfiguration configuration) {
Assert.notNull(em, "EntityManager must not be null");
- Assert.notNull(delegate, "ValueExpressionDelegate must not be null");
+ Assert.notNull(configuration, "JpaQueryConfiguration must not be null");
return switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
- case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape);
- case USE_DECLARED_QUERY ->
- new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider);
+ case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, configuration);
+ case USE_DECLARED_QUERY -> new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration);
case CREATE_IF_NOT_FOUND -> new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory,
- new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape),
- new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider),
- queryRewriterProvider);
+ new CreateQueryLookupStrategy(em, queryMethodFactory, configuration),
+ new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration), configuration);
default -> throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key));
};
}
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 d6f2fc3d89..6c909446a5 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
@@ -26,9 +26,9 @@
import java.util.Optional;
import java.util.Set;
-import org.springframework.core.annotation.AnnotatedElementUtils;
-
import org.jspecify.annotations.Nullable;
+
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.EntityGraph;
@@ -279,6 +279,13 @@ public org.springframework.data.jpa.repository.query.Meta getQueryMetaAttributes
return metaAttributes;
}
+ /**
+ * @return {@code true} if this method is annotated with {@code @Query(value=…)}.
+ */
+ boolean hasAnnotatedQuery() {
+ return StringUtils.hasText(getAnnotationValue("value", String.class));
+ }
+
/**
* Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found
* nor the attribute was specified.
@@ -317,6 +324,25 @@ public String getRequiredAnnotatedQuery() throws IllegalStateException {
throw new IllegalStateException(String.format("No annotated query found for query method %s", getName()));
}
+ /**
+ * Returns the required {@link DeclaredQuery} from a {@link Query} annotation or throws {@link IllegalStateException}
+ * if neither the annotation found nor the attribute was specified.
+ *
+ * @return
+ * @throws IllegalStateException if no {@link Query} annotation is present or the query is empty.
+ * @since 4.0
+ */
+ public DeclaredQuery getRequiredDeclaredQuery() throws IllegalStateException {
+
+ String query = getAnnotatedQuery();
+
+ if (query != null) {
+ return getDeclaredQuery(query);
+ }
+
+ throw new IllegalStateException(String.format("No annotated query found for query method %s", getName()));
+ }
+
/**
* Returns the countQuery string declared in a {@link Query} annotation or {@literal null} if neither the annotation
* found nor the attribute was specified.
@@ -329,6 +355,19 @@ public String getRequiredAnnotatedQuery() throws IllegalStateException {
return StringUtils.hasText(countQuery) ? countQuery : null;
}
+ /**
+ * Returns the {@link DeclaredQuery declared count query} from a {@link Query} annotation or {@literal null} if
+ * neither the annotation found nor the attribute was specified.
+ *
+ * @return
+ * @since 4.0
+ */
+ public @Nullable DeclaredQuery getDeclaredCountQuery() {
+
+ String countQuery = getAnnotationValue("countQuery", String.class);
+ return StringUtils.hasText(countQuery) ? getDeclaredQuery(countQuery) : null;
+ }
+
/**
* Returns the count query projection string declared in a {@link Query} annotation or {@literal null} if neither the
* annotation found nor the attribute was specified.
@@ -352,6 +391,17 @@ boolean isNativeQuery() {
return this.isNativeQuery.get();
}
+ /**
+ * Utility method that returns a {@link DeclaredQuery} object for the given {@code queryString}.
+ *
+ * @param query the query string to wrap.
+ * @return a {@link DeclaredQuery} object for the given {@code queryString}.
+ * @since 4.0
+ */
+ DeclaredQuery getDeclaredQuery(String query) {
+ return isNativeQuery() ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
+ }
+
@Override
public String getNamedQueryName() {
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 7e3825aad8..ee805c7594 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
@@ -52,12 +52,12 @@ final class NamedQuery extends AbstractJpaQuery {
private final String countQueryName;
private final @Nullable String countProjection;
private final boolean namedCountQueryIsPresent;
- private final Lazy declaredQuery;
+ private final Lazy entityQuery;
/**
* Creates a new {@link NamedQuery}.
*/
- private NamedQuery(JpaQueryMethod method, EntityManager em) {
+ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelector selector) {
super(method, em);
@@ -76,7 +76,7 @@ private NamedQuery(JpaQueryMethod method, EntityManager em) {
this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName);
- Query query = em.createNamedQuery(queryName);
+ Query namedQuery = em.createNamedQuery(queryName);
boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters();
boolean cantExtractQuery = !extractor.canExtractQuery();
@@ -90,10 +90,17 @@ private NamedQuery(JpaQueryMethod method, EntityManager em) {
method, method.isNativeQuery() ? "NativeQuery" : "Query"));
}
- String queryString = extractor.extractQueryString(query);
+ String queryString = extractor.extractQueryString(namedQuery);
- this.declaredQuery = Lazy
- .of(() -> DeclaredQuery.of(queryString, method.isNativeQuery() || query.toString().contains("NativeQuery")));
+ // TODO: What is queryString is null?
+ DeclaredQuery declaredQuery;
+ if (method.isNativeQuery() || (namedQuery != null && namedQuery.toString().contains("NativeQuery"))) {
+ declaredQuery = DeclaredQuery.nativeQuery(queryString);
+ } else {
+ declaredQuery = DeclaredQuery.jpqlQuery(queryString);
+ }
+
+ this.entityQuery = Lazy.of(() -> EntityQuery.create(declaredQuery, selector));
}
/**
@@ -126,8 +133,10 @@ static boolean hasNamedQuery(EntityManager em, String queryName) {
*
* @param method must not be {@literal null}.
* @param em must not be {@literal null}.
+ * @param selector must not be {@literal null}.
*/
- public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em) {
+ public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em,
+ QueryEnhancerSelector selector) {
String queryName = method.getNamedQueryName();
@@ -145,7 +154,7 @@ static boolean hasNamedQuery(EntityManager em, String queryName) {
method.isNativeQuery() ? "NativeQuery" : "Query"));
}
- RepositoryQuery query = new NamedQuery(method, em);
+ RepositoryQuery query = new NamedQuery(method, em, selector);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Found named query '%s'", queryName));
}
@@ -175,15 +184,11 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc
EntityManager em = getEntityManager();
TypedQuery countQuery;
- String cacheKey;
if (namedCountQueryIsPresent) {
- cacheKey = countQueryName;
countQuery = em.createNamedQuery(countQueryName, Long.class);
-
} else {
- String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString();
- cacheKey = countQueryString;
+ String countQueryString = entityQuery.get().deriveCountQuery(countProjection).getQueryString();
countQuery = em.createQuery(countQueryString, Long.class);
}
@@ -212,7 +217,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc
return type.isInterface() ? Tuple.class : null;
}
- return declaredQuery.get().hasConstructorExpression() //
+ return entityQuery.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 ae240942d5..35045c5e25 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
@@ -19,17 +19,15 @@
import jakarta.persistence.Query;
import jakarta.persistence.Tuple;
-import org.springframework.core.annotation.MergedAnnotation;
-
import org.jspecify.annotations.Nullable;
+
+import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.NativeQuery;
-import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
-import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.ObjectUtils;
/**
@@ -43,7 +41,7 @@
* @author Mark Paluch
* @author Greg Turnquist
*/
-final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
+class NativeJpaQuery extends AbstractStringBasedJpaQuery {
private final @Nullable String sqlResultSetMapping;
@@ -56,26 +54,47 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
* @param em must not be {@literal null}.
* @param queryString must not be {@literal null} or empty.
* @param countQueryString must not be {@literal null} or empty.
- * @param rewriter the query rewriter to use.
- * @param valueExpressionDelegate must not be {@literal null}.
+ * @param queryConfiguration must not be {@literal null}.
*/
- public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
- QueryRewriter rewriter, ValueExpressionDelegate valueExpressionDelegate) {
+ NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
+ JpaQueryConfiguration queryConfiguration) {
- super(method, em, queryString, countQueryString, rewriter, valueExpressionDelegate);
+ super(method, em, queryString, countQueryString, queryConfiguration);
MergedAnnotations annotations = MergedAnnotations.from(method.getMethod());
MergedAnnotation annotation = annotations.get(NativeQuery.class);
+
this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null;
+ this.queryForEntity = getQueryMethod().isQueryForEntity();
+ }
+ /**
+ * Creates a new {@link NativeJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}.
+ *
+ * @param method must not be {@literal null}.
+ * @param em must not be {@literal null}.
+ * @param query must not be {@literal null} .
+ * @param countQuery can be {@literal null} if not defined.
+ * @param queryConfiguration must not be {@literal null}.
+ */
+ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query,
+ @Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) {
+
+ super(method, em, query, countQuery, queryConfiguration);
+
+ MergedAnnotations annotations = MergedAnnotations.from(method.getMethod());
+ MergedAnnotation annotation = annotations.get(NativeQuery.class);
+
+ this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null;
this.queryForEntity = getQueryMethod().isQueryForEntity();
}
@Override
- protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable, ReturnedType returnedType) {
+ protected Query createJpaQuery(QueryProvider declaredQuery, Sort sort, @Nullable Pageable pageable,
+ ReturnedType returnedType) {
EntityManager em = getEntityManager();
- String query = potentiallyRewriteQuery(queryString, sort, pageable);
+ String query = potentiallyRewriteQuery(declaredQuery.getQueryString(), sort, pageable);
if (!ObjectUtils.isEmpty(sqlResultSetMapping)) {
return em.createNativeQuery(query, sqlResultSetMapping);
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java
index 384d5c16d7..00aef26195 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java
@@ -78,13 +78,13 @@ static ParameterBinder createBinder(JpaParameters parameters, List bindings = query.getParameterBindings();
QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser,
evaluationContextProvider);
QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters,
query.hasNamedParameter());
- return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory),
- !query.usesPaging());
+ boolean usesPaging = query instanceof EntityQuery eq && eq.usesPaging();
+
+ // TODO: lets maybe obtain the bindable query and pass that on to create the setters?
+ return new ParameterBinder(parameters, createSetters(query.getParameterBindings(), query, expressionSetterFactory, basicSetterFactory),
+ !usesPaging);
}
static List getBindings(JpaParameters parameters) {
@@ -124,26 +126,26 @@ static List getBindings(JpaParameters parameters) {
private static Iterable createSetters(List parameterBindings,
QueryParameterSetterFactory... factories) {
- return createSetters(parameterBindings, EmptyDeclaredQuery.EMPTY_QUERY, factories);
+ return createSetters(parameterBindings, EmptyIntrospectedQuery.INSTANCE, factories);
}
private static Iterable createSetters(List parameterBindings,
- DeclaredQuery declaredQuery, QueryParameterSetterFactory... strategies) {
+ ParametrizedQuery query, QueryParameterSetterFactory... strategies) {
List setters = new ArrayList<>(parameterBindings.size());
for (ParameterBinding parameterBinding : parameterBindings) {
- setters.add(createQueryParameterSetter(parameterBinding, strategies, declaredQuery));
+ setters.add(createQueryParameterSetter(parameterBinding, strategies, query));
}
return setters;
}
private static QueryParameterSetter createQueryParameterSetter(ParameterBinding binding,
- QueryParameterSetterFactory[] strategies, DeclaredQuery declaredQuery) {
+ QueryParameterSetterFactory[] strategies, ParametrizedQuery query) {
for (QueryParameterSetterFactory strategy : strategies) {
- QueryParameterSetter setter = strategy.create(binding);
+ QueryParameterSetter setter = strategy.create(binding, query);
if (setter != null) {
return setter;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java
new file mode 100644
index 0000000000..85a314127d
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018-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 java.util.List;
+
+/**
+ * A parsed and structured representation of a query providing introspection details about parameter bindings.
+ *
+ * Structured queries can be either created from {@link EntityQuery} introspection or through
+ * {@link EntityQuery#deriveCountQuery(String) count query derivation}.
+ *
+ * @author Jens Schauder
+ * @author Diego Krupitza
+ * @since 4.0
+ * @see EntityQuery
+ * @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector)
+ * @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration)
+ */
+interface ParametrizedQuery extends QueryProvider {
+
+ /**
+ * @return whether the underlying query has at least one parameter.
+ */
+ boolean hasParameterBindings();
+
+ /**
+ * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
+ * name.
+ *
+ * @return Whether the query uses JDBC style parameters.
+ * @since 2.0.6
+ */
+ boolean usesJdbcStyleParameters();
+
+ /**
+ * @return whether the underlying query has at least one named parameter.
+ */
+ boolean hasNamedParameter();
+
+ /**
+ * @return the registered {@link ParameterBinding}s.
+ */
+ List getParameterBindings();
+
+}
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/PreprocessedQuery.java
similarity index 65%
rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java
rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java
index b0b50cecb8..0c5061b529 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/PreprocessedQuery.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2025 the original author or authors.
+ * Copyright 2025 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.
@@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
@@ -29,17 +30,12 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.springframework.data.expression.ValueExpression;
-
import org.jspecify.annotations.Nullable;
+
+import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
-import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
-import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding;
-import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding;
-import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
-import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin;
import org.springframework.data.repository.query.ValueExpressionQueryRewriter;
-import org.springframework.data.repository.query.parser.Part.Type;
+import org.springframework.data.repository.query.parser.Part;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -47,229 +43,126 @@
import org.springframework.util.StringUtils;
/**
- * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned
- * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations
- * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
- * with synthetic bind parameters.
+ * A pre-parsed query implementing {@link DeclaredQuery} providing information about parameter bindings.
+ *
+ * Query-preprocessing transforms queries using Spring Data-specific syntax such as {@link TemplatedQuery query
+ * templating}, extended {@code LIKE} syntax and usage of {@link ValueExpression value expressions} into a syntax that
+ * is valid for JPA queries (JPQL and native).
+ *
+ * Preprocessing consists of parsing and rewriting so that no extension elements interfere with downstream parsers.
+ * However, pre-processing is a lossy procedure because the resulting {@link #getQueryString() query string} only
+ * contains parameter binding markers and so the original query cannot be restored. Any query derivation must align its
+ * {@link ParameterBinding parameter bindings} to ensure the derived query uses the same binding semantics instead of
+ * plain parameters. See {@link ParameterBinding#isCompatibleWith(ParameterBinding)} for further reference.
*
- * @author Oliver Gierke
- * @author Thomas Darimont
- * @author Oliver Wehrens
+ * @author Christoph Strobl
* @author Mark Paluch
- * @author Jens Schauder
- * @author Diego Krupitza
- * @author Greg Turnquist
- * @author Yuriy Tsarkov
+ * @since 4.0
*/
-class StringQuery implements DeclaredQuery {
+final class PreprocessedQuery implements DeclaredQuery {
- private final String query;
+ private final DeclaredQuery source;
private final List bindings;
- private final boolean containsPageableInSpel;
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.
- */
- public StringQuery(String query, boolean isNative) {
- this(query, isNative, it -> {});
+ private final boolean containsPageableInSpel;
+ private final boolean hasNamedBindings;
+
+ private PreprocessedQuery(DeclaredQuery query, List bindings, boolean usesJdbcStyleParameters,
+ boolean containsPageableInSpel) {
+ this.source = query;
+ this.bindings = bindings;
+ this.usesJdbcStyleParameters = usesJdbcStyleParameters;
+ this.containsPageableInSpel = containsPageableInSpel;
+ this.hasNamedBindings = containsNamedParameter(bindings);
}
- /**
- * Creates a new {@link StringQuery} from the given JPQL query.
- *
- * @param query must not be {@literal null} or empty.
- */
- private StringQuery(String query, boolean isNative, Consumer> parameterPostProcessor) {
-
- Assert.hasText(query, "Query must not be null or empty");
-
- this.isNative = isNative;
- this.bindings = new ArrayList<>();
- this.containsPageableInSpel = query.contains("#pageable");
-
- Metadata queryMeta = new Metadata();
- this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
- this.bindings, queryMeta);
-
- this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
- this.queryEnhancer = QueryEnhancerFactory.forQuery(this);
+ private static boolean containsNamedParameter(List bindings) {
- parameterPostProcessor.accept(this.bindings);
-
- boolean hasNamedParameters = false;
- for (ParameterBinding parameterBinding : getParameterBindings()) {
- if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) {
- hasNamedParameters = true;
- break;
+ for (ParameterBinding parameterBinding : bindings) {
+ if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin()
+ .isMethodArgument()) {
+ return true;
}
}
-
- this.hasNamedParameters = hasNamedParameters;
+ return false;
}
/**
- * Returns whether we have found some like bindings.
+ * Parse a {@link DeclaredQuery query} into its parametrized form by identifying anonymous, named, indexed and SpEL
+ * parameters. Query parsing applies special treatment to {@code IN} and {@code LIKE} parameter bindings.
+ *
+ * @param declaredQuery the source query to parse.
+ * @return a parsed {@link PreprocessedQuery}.
*/
- boolean hasParameterBindings() {
- return !bindings.isEmpty();
- }
-
- String getProjection() {
- return this.queryEnhancer.getProjection();
- }
-
- @Override
- public List getParameterBindings() {
- return bindings;
- }
-
- @Override
- public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) {
-
- // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees
- // JPA parameter markers and not the original expressions anymore.
-
- return new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), //
- this.isNative, derivedBindings -> {
-
- // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees
- // JPA
- // parameter markers and not the original expressions anymore.
- if (this.hasParameterBindings() && !this.getParameterBindings().equals(derivedBindings)) {
-
- for (ParameterBinding binding : bindings) {
-
- Predicate identifier = binding::bindsTo;
- Predicate notCompatible = Predicate.not(binding::isCompatibleWith);
-
- // replace incompatible bindings
- if ( derivedBindings.removeIf(
- it -> identifier.test(it) && notCompatible.test(it))) {
- derivedBindings.add(binding);
- }
- }
- }
+ public static PreprocessedQuery parse(DeclaredQuery declaredQuery) {
+ return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite,
+ parameterBindings -> {
});
}
- @Override
- public boolean usesJdbcStyleParameters() {
- return usesJdbcStyleParameters;
- }
-
@Override
public String getQueryString() {
- return query;
+ return source.getQueryString();
}
@Override
- public @Nullable String getAlias() {
- return queryEnhancer.detectAlias();
+ public boolean isNative() {
+ return source.isNative();
}
- @Override
- public boolean hasConstructorExpression() {
- return queryEnhancer.hasConstructorExpression();
+ boolean hasBindings() {
+ return !bindings.isEmpty();
}
- @Override
- public boolean isDefaultProjection() {
- return getProjection().equalsIgnoreCase(getAlias());
+ boolean hasNamedBindings() {
+ return this.hasNamedBindings;
}
- @Override
- public boolean hasNamedParameter() {
- return hasNamedParameters;
+ boolean containsPageableInSpel() {
+ return containsPageableInSpel;
}
- @Override
- public boolean usesPaging() {
- return containsPageableInSpel;
+ boolean usesJdbcStyleParameters() {
+ return usesJdbcStyleParameters;
}
- @Override
- public boolean isNativeQuery() {
- return isNative;
+ List getBindings() {
+ return Collections.unmodifiableList(bindings);
}
/**
- * Value object to track and allocate used parameter index labels in a query.
+ * Derive a query (typically a count query) from the given query string. We need to copy expression bindings from the
+ * declared to the derived query as JPQL query derivation only sees JPA parameter markers and not the original
+ * expressions anymore.
+ *
+ * @return
*/
- static class IndexedParameterLabels {
-
- private final TreeSet usedLabels;
- private final boolean sequential;
-
- public IndexedParameterLabels(Set usedLabels) {
-
- this.usedLabels = usedLabels instanceof TreeSet ts ? ts : new TreeSet(usedLabels);
- this.sequential = isSequential(usedLabels);
- }
-
- private static boolean isSequential(Set usedLabels) {
-
- for (int i = 0; i < usedLabels.size(); i++) {
-
- if (usedLabels.contains(i + 1)) {
- continue;
- }
-
- return false;
- }
-
- return true;
- }
-
- /**
- * Allocate the next index label (1-based).
- *
- * @return the next index label.
- */
- public int allocate() {
-
- if (sequential) {
- int index = usedLabels.size() + 1;
- usedLabels.add(index);
-
- return index;
- }
-
- int attempts = usedLabels.last() + 1;
- int index = attemptAllocate(attempts);
+ @Override
+ public PreprocessedQuery rewrite(String newQueryString) {
- if (index == -1) {
- throw new IllegalStateException(
- "Unable to allocate a unique parameter label. All possible labels have been used.");
- }
+ return ParameterBindingParser.INSTANCE.parse(newQueryString, source::rewrite, derivedBindings -> {
- usedLabels.add(index);
+ // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees
+ // JPA parameter markers and not the original expressions anymore.
+ if (this.hasBindings() && !this.bindings.equals(derivedBindings)) {
- return index;
- }
+ for (ParameterBinding binding : bindings) {
- private int attemptAllocate(int attempts) {
+ Predicate identifier = binding::bindsTo;
+ Predicate notCompatible = Predicate.not(binding::isCompatibleWith);
- for (int i = 0; i < attempts; i++) {
-
- if (usedLabels.contains(i + 1)) {
- continue;
+ // replace incompatible bindings
+ if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) {
+ derivedBindings.add(binding);
+ }
}
-
- return i + 1;
}
+ });
+ }
- return -1;
- }
-
- public boolean hasLabels() {
- return !usedLabels.isEmpty();
- }
+ @Override
+ public String toString() {
+ return "ParametrizedQuery[" + source + ", " + bindings + ']';
}
/**
@@ -293,7 +186,7 @@ enum ParameterBindingParser {
private static final Pattern NAMED_STYLE_PARAM = Pattern.compile("(?!\\\\):\\w+"); // no \ and :[text]
private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type; "
- + "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding";
+ + "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding";
private static final int INDEXED_PARAMETER_GROUP = 4;
private static final int NAMED_PARAMETER_GROUP = 6;
private static final int COMPARISION_TYPE_GROUP = 1;
@@ -331,12 +224,16 @@ enum ParameterBindingParser {
* 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) {
+ PreprocessedQuery parse(String query, Function declaredQueryFactory,
+ Consumer> parameterBindingPostProcessor) {
IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query));
boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels();
+ List bindings = new ArrayList<>();
+ boolean jdbcStyle = false;
+ boolean containsPageableInSpel = query.contains("#pageable");
+
/*
* Prefer indexed access over named parameters if only SpEL Expression parameters are present.
*/
@@ -367,14 +264,15 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
String match = matcher.group(0);
if (JDBC_STYLE_PARAM.matcher(match).find()) {
- queryMeta.usesJdbcStyleParameters = true;
+ jdbcStyle = true;
}
- if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) {
+ if (NUMBERED_STYLE_PARAM.matcher(match)
+ .find() || NAMED_STYLE_PARAM.matcher(match).find()) {
usesJpaStyleParameters = true;
}
- if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
+ if (usesJpaStyleParameters && jdbcStyle) {
throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported");
}
@@ -390,54 +288,64 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
parameterIndex = parameterLabels.allocate();
}
- BindingIdentifier queryParameter;
+ ParameterBinding.BindingIdentifier queryParameter;
if (parameterIndex != null) {
- queryParameter = BindingIdentifier.of(parameterIndex);
- } else if (parameterName != null) {
- queryParameter = BindingIdentifier.of(parameterName);
- } else {
+ queryParameter = ParameterBinding.BindingIdentifier.of(parameterIndex);
+ }
+ else if (parameterName != null) {
+ queryParameter = ParameterBinding.BindingIdentifier.of(parameterName);
+ }
+ else {
throw new IllegalStateException("No bindable expression found");
}
- ParameterOrigin origin = ObjectUtils.isEmpty(expression)
- ? ParameterOrigin.ofParameter(parameterName, parameterIndex)
- : ParameterOrigin.ofExpression(expression);
-
- BindingIdentifier targetBinding = queryParameter;
- Function bindingFactory = switch (ParameterBindingType.of(typeSource)) {
- case LIKE -> {
-
- Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
- yield (identifier) -> new LikeParameterBinding(identifier, origin, likeType);
- }
- case IN -> (identifier) -> new InParameterBinding(identifier, origin); // fall-through we don't need a special parameter queryParameter for the given parameter.
- default -> (identifier) -> new ParameterBinding(identifier, origin);
- };
-
- if (origin.isExpression()) {
+ ParameterBinding.ParameterOrigin origin = ObjectUtils.isEmpty(expression)
+ ? ParameterBinding.ParameterOrigin.ofParameter(parameterName, parameterIndex)
+ : ParameterBinding.ParameterOrigin.ofExpression(expression);
+
+ ParameterBinding.BindingIdentifier targetBinding = queryParameter;
+ Function bindingFactory = switch (ParameterBindingType
+ .of(typeSource)) {
+ case LIKE -> {
+
+ Part.Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
+ yield (identifier) -> new ParameterBinding.LikeParameterBinding(identifier, origin, likeType);
+ }
+ case IN ->
+ (identifier) -> new ParameterBinding.InParameterBinding(identifier, origin); // fall-through we
+ // don't need a special
+ // parameter queryParameter for the
+ // given parameter.
+ default -> (identifier) -> new ParameterBinding(identifier, origin);
+ };
+
+ if (origin.isExpression()) {
parameterBindings.register(bindingFactory.apply(queryParameter));
- } else {
+ }
+ else {
targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels);
}
replacement = targetBinding.hasName() ? ":" + targetBinding.getName()
- : ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?"
- : "?" + targetBinding.getPosition());
+ : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition());
String result;
String substring = matcher.group(2);
int index = resultingQuery.indexOf(substring, currentIndex);
if (index < 0) {
result = resultingQuery;
- } else {
+ }
+ else {
currentIndex = index + replacement.length();
result = resultingQuery.substring(0, index) + replacement
- + resultingQuery.substring(index + substring.length());
+ + resultingQuery.substring(index + substring.length());
}
resultingQuery = result;
}
- return resultingQuery;
+ parameterBindingPostProcessor.accept(bindings);
+ return new PreprocessedQuery(declaredQueryFactory.apply(resultingQuery), bindings, jdbcStyle,
+ containsPageableInSpel);
}
private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel,
@@ -545,20 +453,17 @@ static ParameterBindingType of(String typeSource) {
}
}
- static class Metadata {
- private boolean usesJdbcStyleParameters = false;
- }
-
/**
* Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are
- * bound to potentially unique query parameters for {@link LikeParameterBinding#prepare(Object) LIKE rewrite}.
+ * bound to potentially unique query parameters for {@link ParameterBinding.LikeParameterBinding#prepare(Object) LIKE
+ * rewrite}.
*
* @author Mark Paluch
* @since 3.1.2
*/
- static class ParameterBindings {
+ private static class ParameterBindings {
- private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
+ private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
private final Consumer registration;
@@ -572,21 +477,22 @@ public ParameterBindings(List bindings, Consumer bindingFactory, IndexedParameterLabels parameterLabels) {
+ ParameterBinding.BindingIdentifier register(ParameterBinding.BindingIdentifier identifier,
+ ParameterBinding.ParameterOrigin origin,
+ Function bindingFactory,
+ IndexedParameterLabels parameterLabels) {
- Assert.isInstanceOf(MethodInvocationArgument.class, origin);
+ Assert.isInstanceOf(ParameterBinding.MethodInvocationArgument.class, origin);
- BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier();
+ ParameterBinding.BindingIdentifier methodArgument = ((ParameterBinding.MethodInvocationArgument) origin)
+ .identifier();
List bindingsForOrigin = getBindings(methodArgument);
if (!isBound(identifier)) {
@@ -606,7 +512,7 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
}
}
- BindingIdentifier syntheticIdentifier;
+ ParameterBinding.BindingIdentifier syntheticIdentifier;
if (identifier.hasName() && methodArgument.hasName()) {
int index = 0;
@@ -615,9 +521,10 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
index++;
newName = methodArgument.getName() + "_" + index;
}
- syntheticIdentifier = BindingIdentifier.of(newName);
- } else {
- syntheticIdentifier = BindingIdentifier.of(parameterLabels.allocate());
+ syntheticIdentifier = ParameterBinding.BindingIdentifier.of(newName);
+ }
+ else {
+ syntheticIdentifier = ParameterBinding.BindingIdentifier.of(parameterLabels.allocate());
}
ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier);
@@ -627,11 +534,12 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
}
private boolean existsBoundParameter(String key) {
- return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream)
+ return methodArgumentToLikeBindings.values().stream()
+ .flatMap(Collection::stream)
.anyMatch(it -> key.equals(it.getName()));
}
- private List getBindings(BindingIdentifier identifier) {
+ private List getBindings(ParameterBinding.BindingIdentifier identifier) {
return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>());
}
@@ -639,4 +547,79 @@ public void register(ParameterBinding parameterBinding) {
registration.accept(parameterBinding);
}
}
+
+ /**
+ * Value object to track and allocate used parameter index labels in a query.
+ */
+ static class IndexedParameterLabels {
+
+ private final TreeSet usedLabels;
+ private final boolean sequential;
+
+ public IndexedParameterLabels(Set usedLabels) {
+
+ this.usedLabels = usedLabels instanceof TreeSet ts ? ts : new TreeSet(usedLabels);
+ this.sequential = isSequential(usedLabels);
+ }
+
+ private static boolean isSequential(Set usedLabels) {
+
+ for (int i = 0; i < usedLabels.size(); i++) {
+
+ if (usedLabels.contains(i + 1)) {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Allocate the next index label (1-based).
+ *
+ * @return the next index label.
+ */
+ public int allocate() {
+
+ if (sequential) {
+ int index = usedLabels.size() + 1;
+ usedLabels.add(index);
+
+ return index;
+ }
+
+ int attempts = usedLabels.last() + 1;
+ int index = attemptAllocate(attempts);
+
+ if (index == -1) {
+ throw new IllegalStateException(
+ "Unable to allocate a unique parameter label. All possible labels have been used.");
+ }
+
+ usedLabels.add(index);
+
+ return index;
+ }
+
+ private int attemptAllocate(int attempts) {
+
+ for (int i = 0; i < attempts; i++) {
+
+ if (usedLabels.contains(i + 1)) {
+ continue;
+ }
+
+ return i + 1;
+ }
+
+ return -1;
+ }
+
+ public boolean hasLabels() {
+ return !usedLabels.isEmpty();
+ }
+ }
+
}
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 65304dcbba..2810f957c0 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
@@ -15,11 +15,9 @@
*/
package org.springframework.data.jpa.repository.query;
-import java.util.Set;
+import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Sort;
-
-import org.jspecify.annotations.Nullable;
import org.springframework.data.repository.query.ReturnedType;
/**
@@ -27,10 +25,23 @@
*
* @author Diego Krupitza
* @author Greg Turnquist
- * @since 2.7.0
+ * @author Mark Paluch
+ * @since 2.7
*/
public interface QueryEnhancer {
+ /**
+ * Creates a new {@link QueryEnhancer} for a {@link DeclaredQuery}. Convenience method for
+ * {@link QueryEnhancerFactory#create(QueryProvider)}.
+ *
+ * @param query the query to be enhanced.
+ * @return the new {@link QueryEnhancer}.
+ * @since 4.0
+ */
+ static QueryEnhancer create(DeclaredQuery query) {
+ return QueryEnhancerFactory.forQuery(query).create(query);
+ }
+
/**
* Returns whether the given JPQL query contains a constructor expression.
*
@@ -39,9 +50,9 @@ public interface QueryEnhancer {
boolean hasConstructorExpression();
/**
- * Resolves the alias for the entity to be retrieved from the given JPA query.
+ * Resolves the primary alias for the entity to be retrieved from the given JPA query.
*
- * @return Might return {@literal null}.
+ * @return can return {@literal null}.
*/
@Nullable
String detectAlias();
@@ -53,61 +64,24 @@ public interface QueryEnhancer {
*/
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 since 3.5, use {@link #rewrite(QueryRewriteInformation)} instead.
- */
- @Deprecated(since = "3.5", forRemoval = true)
- String applySorting(Sort sort, @Nullable String alias);
+ QueryProvider getQuery();
/**
* Rewrite the query to include sorting and apply {@link ReturnedType} customizations.
*
* @param rewriteInformation the rewrite information to apply.
* @return the modified query string.
- * @since 3.5
+ * @since 4.0
*/
String rewrite(QueryRewriteInformation rewriteInformation);
/**
- * Creates a count projected query from the given original query.
- *
- * @return Guaranteed to be not {@literal null}.
- */
- default String createCountQueryFor() {
- return createCountQueryFor(null);
- }
-
- /**
- * Creates a count projected query from the given original query using the provided countProjection.
+ * Creates a count projected query from the given original query using the provided {@code countProjection}.
*
* @param countProjection may be {@literal null}.
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
@@ -117,7 +91,7 @@ default String createCountQueryFor() {
/**
* Interface to describe the information needed to rewrite a query.
*
- * @since 3.5
+ * @since 4.0
*/
interface QueryRewriteInformation {
@@ -130,6 +104,7 @@ interface QueryRewriteInformation {
* @return type expected to be returned by the query.
*/
ReturnedType getReturnedType();
+
}
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java
new file mode 100644
index 0000000000..ef7f141246
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.data.jpa.provider.PersistenceProvider;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Pre-defined QueryEnhancerFactories to be used for query enhancement.
+ *
+ * @author Mark Paluch
+ */
+public class QueryEnhancerFactories {
+
+ private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class);
+
+ static final boolean jSqlParserPresent = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser",
+ QueryEnhancerFactory.class.getClassLoader());
+
+ static {
+
+ if (jSqlParserPresent) {
+ LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used");
+ }
+
+ if (PersistenceProvider.ECLIPSELINK.isPresent()) {
+ LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used.");
+ }
+
+ if (PersistenceProvider.HIBERNATE.isPresent()) {
+ LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used.");
+ }
+ }
+
+ enum BuiltinQueryEnhancerFactories implements QueryEnhancerFactory {
+
+ FALLBACK {
+ @Override
+ public boolean supports(DeclaredQuery query) {
+ return true;
+ }
+
+ @Override
+ public QueryEnhancer create(QueryProvider query) {
+ return new DefaultQueryEnhancer(query);
+ }
+ },
+
+ JSQLPARSER {
+ @Override
+ public boolean supports(DeclaredQuery query) {
+ return query.isNative();
+ }
+
+ @Override
+ public QueryEnhancer create(QueryProvider query) {
+ if (jSqlParserPresent) {
+ return new JSqlParserQueryEnhancer(query);
+ }
+
+ throw new IllegalStateException("JSQLParser is not available on the class path");
+ }
+ },
+
+ HQL {
+ @Override
+ public boolean supports(DeclaredQuery query) {
+ return !query.isNative();
+ }
+
+ @Override
+ public QueryEnhancer create(QueryProvider query) {
+ return JpaQueryEnhancer.forHql(query.getQueryString());
+ }
+ },
+ EQL {
+ @Override
+ public boolean supports(DeclaredQuery query) {
+ return !query.isNative();
+ }
+
+ @Override
+ public QueryEnhancer create(QueryProvider query) {
+ return JpaQueryEnhancer.forEql(query.getQueryString());
+ }
+ },
+ JPQL {
+ @Override
+ public boolean supports(DeclaredQuery query) {
+ return !query.isNative();
+ }
+
+ @Override
+ public QueryEnhancer create(QueryProvider query) {
+ return JpaQueryEnhancer.forJpql(query.getQueryString());
+ }
+ }
+ }
+
+ /**
+ * Returns the default fallback {@link QueryEnhancerFactory} using regex-based detection. This factory supports only
+ * simple SQL queries.
+ *
+ * @return fallback {@link QueryEnhancerFactory} using regex-based detection.
+ */
+ public static QueryEnhancerFactory fallback() {
+ return BuiltinQueryEnhancerFactories.FALLBACK;
+ }
+
+ /**
+ * Returns a {@link QueryEnhancerFactory} that uses JSqlParser
+ * if it is available from the class path.
+ *
+ * @return a {@link QueryEnhancerFactory} that uses JSqlParser.
+ * @throws IllegalStateException if JSQLParser is not on the class path.
+ */
+ public static QueryEnhancerFactory jsqlparser() {
+
+ if (!jSqlParserPresent) {
+ throw new IllegalStateException("JSQLParser is not available on the class path");
+ }
+
+ return BuiltinQueryEnhancerFactories.JSQLPARSER;
+ }
+
+ /**
+ * Returns a {@link QueryEnhancerFactory} using HQL (Hibernate Query Language) parser.
+ *
+ * @return a {@link QueryEnhancerFactory} using HQL (Hibernate Query Language) parser.
+ */
+ public static QueryEnhancerFactory hql() {
+ return BuiltinQueryEnhancerFactories.HQL;
+ }
+
+ /**
+ * Returns a {@link QueryEnhancerFactory} using EQL (EclipseLink Query Language) parser.
+ *
+ * @return a {@link QueryEnhancerFactory} using EQL (EclipseLink Query Language) parser.
+ */
+ public static QueryEnhancerFactory eql() {
+ return BuiltinQueryEnhancerFactories.EQL;
+ }
+
+ /**
+ * Returns a {@link QueryEnhancerFactory} using JPQL (Jakarta Persistence Query Language) parser as per the JPA spec.
+ *
+ * @return a {@link QueryEnhancerFactory} using JPQL (Jakarta Persistence Query Language) parser as per the JPA spec.
+ */
+ public static QueryEnhancerFactory jpql() {
+ return BuiltinQueryEnhancerFactories.JPQL;
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java
index 5a2853cb1a..0233798594 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java
@@ -15,133 +15,41 @@
*/
package org.springframework.data.jpa.repository.query;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.core.SpringProperties;
-import org.springframework.data.jpa.provider.PersistenceProvider;
-import org.springframework.util.ClassUtils;
-import org.springframework.util.ObjectUtils;
-import org.springframework.util.StringUtils;
-
/**
- * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link DeclaredQuery}.
+ * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link ParametrizedQuery}.
*
* @author Diego Krupitza
* @author Greg Turnquist
* @author Mark Paluch
* @author Christoph Strobl
- * @since 2.7.0
+ * @since 4.0
*/
-public final class QueryEnhancerFactory {
-
- private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class);
- private static final NativeQueryEnhancer NATIVE_QUERY_ENHANCER;
-
- static {
-
- NATIVE_QUERY_ENHANCER = NativeQueryEnhancer.select();
-
- if (PersistenceProvider.ECLIPSELINK.isPresent()) {
- LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used.");
- }
-
- if (PersistenceProvider.HIBERNATE.isPresent()) {
- LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used.");
- }
- }
-
- private QueryEnhancerFactory() {}
+public interface QueryEnhancerFactory {
/**
- * Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}.
+ * Returns whether this QueryEnhancerFactory supports the given {@link DeclaredQuery}.
*
- * @param query must not be {@literal null}.
- * @return an implementation of {@link QueryEnhancer} that suits the query the most
+ * @param query the query to be enhanced and introspected.
+ * @return {@code true} if this QueryEnhancer supports the given query; {@code false} otherwise.
*/
- public static QueryEnhancer forQuery(DeclaredQuery query) {
-
- if (query.isNativeQuery()) {
- return getNativeQueryEnhancer(query);
- }
-
- if (PersistenceProvider.HIBERNATE.isPresent()) {
- return JpaQueryEnhancer.forHql(query);
- } else if (PersistenceProvider.ECLIPSELINK.isPresent()) {
- return JpaQueryEnhancer.forEql(query);
- } else {
- return JpaQueryEnhancer.forJpql(query);
- }
- }
+ boolean supports(DeclaredQuery query);
/**
- * Get the native query enhancer for the given {@link DeclaredQuery query} based on {@link #NATIVE_QUERY_ENHANCER}.
+ * Creates a new {@link QueryEnhancer} for the given query.
*
- * @param query the declared query.
- * @return new instance of {@link QueryEnhancer}.
+ * @param query the query to be enhanced and introspected.
+ * @return the query enhancer to be used.
*/
- private static QueryEnhancer getNativeQueryEnhancer(DeclaredQuery query) {
-
- if (NATIVE_QUERY_ENHANCER.equals(NativeQueryEnhancer.JSQLPARSER)) {
- return new JSqlParserQueryEnhancer(query);
- }
-
- return new DefaultQueryEnhancer(query);
- }
+ QueryEnhancer create(QueryProvider query);
/**
- * Possible choices for the {@link #NATIVE_PARSER_PROPERTY}. Resolve the parser through {@link #select()}.
+ * Creates a new {@link QueryEnhancerFactory} for the given {@link DeclaredQuery}.
*
- * @since 3.3.5
+ * @param query must not be {@literal null}.
+ * @return an implementation of {@link QueryEnhancer} that suits the query the most
*/
- enum NativeQueryEnhancer {
-
- AUTO, REGEX, JSQLPARSER;
-
- static final String NATIVE_PARSER_PROPERTY = "spring.data.jpa.query.native.parser";
-
- static final boolean JSQLPARSER_PRESENT = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", null);
-
- /**
- * @return the current selection considering classpath availability and user selection via
- * {@link #NATIVE_PARSER_PROPERTY}.
- */
- static NativeQueryEnhancer select() {
-
- NativeQueryEnhancer selected = resolve();
-
- if (selected.equals(NativeQueryEnhancer.JSQLPARSER)) {
- LOG.info("User choice: Using JSqlParser");
- return NativeQueryEnhancer.JSQLPARSER;
- }
-
- if (selected.equals(NativeQueryEnhancer.REGEX)) {
- LOG.info("Using Regex QueryEnhancer");
- return NativeQueryEnhancer.REGEX;
- }
-
- if (!JSQLPARSER_PRESENT) {
- return NativeQueryEnhancer.REGEX;
- }
-
- LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used.");
- return NativeQueryEnhancer.JSQLPARSER;
- }
-
- /**
- * Resolve {@link NativeQueryEnhancer} from {@link SpringProperties}.
- *
- * @return the {@link NativeQueryEnhancer} constant.
- */
- private static NativeQueryEnhancer resolve() {
-
- String name = SpringProperties.getProperty(NATIVE_PARSER_PROPERTY);
-
- if (StringUtils.hasText(name)) {
- return ObjectUtils.caseInsensitiveValueOf(NativeQueryEnhancer.values(), name);
- }
-
- return AUTO;
- }
+ static QueryEnhancerFactory forQuery(DeclaredQuery query) {
+ return QueryEnhancerSelector.DEFAULT_SELECTOR.select(query);
}
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java
new file mode 100644
index 0000000000..fd5f1da6ae
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 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 org.springframework.data.jpa.provider.PersistenceProvider;
+
+/**
+ * Interface declaring a strategy to select a {@link QueryEnhancer} for a given {@link DeclaredQuery query}.
+ *
+ * Enhancers are selected when introspecting a query to determine their selection, joins, aliases and other information
+ * so that query methods can derive count queries, apply sorting and perform other rewrite transformations.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public interface QueryEnhancerSelector {
+
+ /**
+ * Default selector strategy.
+ */
+ QueryEnhancerSelector DEFAULT_SELECTOR = new DefaultQueryEnhancerSelector();
+
+ /**
+ * Select a {@link QueryEnhancer} for a {@link DeclaredQuery query}.
+ *
+ * @param query
+ * @return
+ */
+ QueryEnhancerFactory select(DeclaredQuery query);
+
+ /**
+ * Default {@link QueryEnhancerSelector} implementation using class-path information to determine enhancer
+ * availability. Subclasses may provide a different configuration by using the protected constructor.
+ */
+ class DefaultQueryEnhancerSelector implements QueryEnhancerSelector {
+
+ protected static QueryEnhancerFactory DEFAULT_NATIVE;
+ protected static QueryEnhancerFactory DEFAULT_JPQL;
+
+ static {
+
+ DEFAULT_NATIVE = QueryEnhancerFactories.jSqlParserPresent ? QueryEnhancerFactories.jsqlparser()
+ : QueryEnhancerFactories.fallback();
+
+ if (PersistenceProvider.HIBERNATE.isPresent()) {
+ DEFAULT_JPQL = QueryEnhancerFactories.hql();
+ } else if (PersistenceProvider.ECLIPSELINK.isPresent()) {
+ DEFAULT_JPQL = QueryEnhancerFactories.eql();
+ } else {
+ DEFAULT_JPQL = QueryEnhancerFactories.jpql();
+ }
+ }
+
+ private final QueryEnhancerFactory nativeQuery;
+ private final QueryEnhancerFactory jpql;
+
+ DefaultQueryEnhancerSelector() {
+ this(DEFAULT_NATIVE, DEFAULT_JPQL);
+ }
+
+ protected DefaultQueryEnhancerSelector(QueryEnhancerFactory nativeQuery, QueryEnhancerFactory jpql) {
+ this.nativeQuery = nativeQuery;
+ this.jpql = jpql;
+ }
+
+ /**
+ * Returns the default JPQL {@link QueryEnhancerFactory} based on class path presence of Hibernate and EclipseLink.
+ *
+ * @return the default JPQL {@link QueryEnhancerFactory}.
+ */
+ public static QueryEnhancerFactory jpql() {
+ return DEFAULT_JPQL;
+ }
+
+ @Override
+ public QueryEnhancerFactory select(DeclaredQuery query) {
+ return jpql.supports(query) ? jpql : nativeQuery;
+ }
+
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java
index 3a6bb4c7e9..6d6196b8ef 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java
@@ -20,9 +20,9 @@
import java.util.function.Function;
-import org.springframework.data.expression.ValueEvaluationContext;
-
import org.jspecify.annotations.Nullable;
+
+import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
@@ -54,7 +54,7 @@ abstract class QueryParameterSetterFactory {
* @param binding the parameter binding to create a {@link QueryParameterSetter} for.
* @return
*/
- abstract @Nullable QueryParameterSetter create(ParameterBinding binding);
+ abstract @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery);
/**
* Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}.
@@ -116,8 +116,8 @@ private static QueryParameterSetter createSetter(Function parameters, String name) {
@@ -180,7 +180,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar
}
@Override
- public @Nullable QueryParameterSetter create(ParameterBinding binding) {
+ public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery) {
if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) {
return null;
@@ -212,7 +212,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar
private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory {
@Override
- public @Nullable QueryParameterSetter create(ParameterBinding binding) {
+ public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) {
if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) {
return null;
@@ -248,7 +248,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett
}
@Override
- public @Nullable QueryParameterSetter create(ParameterBinding binding) {
+ public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) {
Assert.notNull(binding, "Binding must not be null");
@@ -294,22 +294,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) {
}
@Override
- public @Nullable QueryParameterSetter create(ParameterBinding binding) {
-
- if (!binding.getOrigin().isMethodArgument()) {
- return null;
- }
-
- int parameterIndex = binding.getRequiredPosition() - 1;
-
- Assert.isTrue( //
- parameterIndex < parameters.getNumberOfParameters(), //
- () -> String.format( //
- "At least %s parameter(s) provided but only %s parameter(s) present in query", //
- binding.getRequiredPosition(), //
- parameters.getNumberOfParameters() //
- ) //
- );
+ public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) {
if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) {
@@ -317,7 +302,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) {
return QueryParameterSetter.NOOP;
}
- return super.create(binding);
+ return super.create(binding, query);
}
return null;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java
new file mode 100644
index 0000000000..98de7da6eb
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 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;
+
+/**
+ * Interface indicating an object that contains and exposes an {@code query string}. This can be either a JPQL query
+ * string or a SQL query string.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 4.0
+ * @see DeclaredQuery#jpqlQuery(String)
+ * @see DeclaredQuery#nativeQuery(String)
+ */
+public interface QueryProvider {
+
+ /**
+ * Return the query string.
+ *
+ * @return the query string.
+ */
+ String getQueryString();
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
index 41c572731e..b16d2ef5dd 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
@@ -445,10 +445,8 @@ private static String toJpaDirection(Order order) {
*
* @param query must not be {@literal null}.
* @return Might return {@literal null}.
- * @deprecated use {@link DeclaredQuery#getAlias()} instead.
*/
- @Deprecated
- public static @Nullable String detectAlias(String query) {
+ static @Nullable String detectAlias(String query) {
String alias = null;
Matcher matcher = ALIAS_MATCH.matcher(removeSubqueries(query));
@@ -554,10 +552,8 @@ public static Query applyAndBind(String queryString, Iterable entities, E
*
* @param originalQuery must not be {@literal null} or empty.
* @return Guaranteed to be not {@literal null}.
- * @deprecated use {@link DeclaredQuery#deriveCountQuery(String)} instead.
*/
- @Deprecated
- public static String createCountQueryFor(String originalQuery) {
+ static String createCountQueryFor(String originalQuery) {
return createCountQueryFor(originalQuery, null);
}
@@ -568,10 +564,8 @@ public static String createCountQueryFor(String originalQuery) {
* @param countProjection may be {@literal null}.
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
* @since 1.6
- * @deprecated use {@link DeclaredQuery#deriveCountQuery(String)} instead.
*/
- @Deprecated
- public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) {
+ static String createCountQueryFor(String originalQuery, @Nullable String countProjection) {
return createCountQueryFor(originalQuery, countProjection, false);
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java
index b43f555c12..b042318b13 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java
@@ -18,11 +18,9 @@
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
-import org.springframework.data.jpa.repository.QueryRewriter;
-
import org.jspecify.annotations.Nullable;
+
import org.springframework.data.repository.query.RepositoryQuery;
-import org.springframework.data.repository.query.ValueExpressionDelegate;
/**
* {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod}
@@ -34,36 +32,21 @@
* @author Mark Paluch
* @author Greg Turnquist
*/
-final class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
-
- /**
- * Creates a new {@link SimpleJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}.
- *
- * @param method must not be {@literal null}
- * @param em must not be {@literal null}
- * @param countQueryString
- * @param queryRewriter must not be {@literal null}
- * @param valueExpressionDelegate must not be {@literal null}
- */
- public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString,
- QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) {
- this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, valueExpressionDelegate);
- }
+class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
/**
* Creates a new {@link SimpleJpaQuery} that encapsulates a simple query string.
*
- * @param method must not be {@literal null}
- * @param em must not be {@literal null}
- * @param queryString must not be {@literal null} or empty
- * @param countQueryString
- * @param queryRewriter
- * @param valueExpressionDelegate must not be {@literal null}
+ * @param method must not be {@literal null}.
+ * @param em must not be {@literal null}.
+ * @param query must not be {@literal null} or empty.
+ * @param countQuery can be {@literal null} if not defined.
+ * @param queryConfiguration must not be {@literal null}.
*/
- public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryRewriter queryRewriter,
- ValueExpressionDelegate valueExpressionDelegate) {
+ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query,
+ @Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) {
- super(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate);
+ super(method, em, query, countQuery, queryConfiguration);
validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", method);
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/TemplatedQuery.java
similarity index 62%
rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/TemplatedQuery.java
index a414b52005..487a7b11f8 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/TemplatedQuery.java
@@ -23,14 +23,13 @@
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
-import org.springframework.data.repository.core.EntityMetadata;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.util.Assert;
/**
- * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression.
+ * Factory methods to obtain {@link EntityQuery} from a declared query using SpEL template-expressions.
*
- * Currently the following template variables are available:
+ * Currently, the following template variables are available:
*
*
{@code #entityName} - the simple class name of the given entity
*
@@ -42,7 +41,7 @@
* @author Diego Krupitza
* @author Greg Turnquist
*/
-class ExpressionBasedStringQuery extends StringQuery {
+class TemplatedQuery {
private static final String EXPRESSION_PARAMETER = "$1#{";
private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{";
@@ -61,30 +60,35 @@ class ExpressionBasedStringQuery extends StringQuery {
}
/**
- * Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link EntityMetadata}.
+ * Create a {@link DefaultEntityQuery} given {@link String query}, {@link JpaQueryMethod} and
+ * {@link JpaQueryConfiguration}.
*
- * @param query must not be {@literal null} or empty.
- * @param metadata must not be {@literal null}.
- * @param parser must not be {@literal null}.
- * @param nativeQuery is a given query is native or not
+ * @param queryString must not be {@literal null}.
+ * @param queryMethod must not be {@literal null}.
+ * @param queryContext must not be {@literal null}.
+ * @return the created {@link DefaultEntityQuery}.
*/
- public ExpressionBasedStringQuery(String query, JpaEntityMetadata> metadata, ValueExpressionParser parser,
- boolean nativeQuery) {
- super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query));
+ public static EntityQuery create(String queryString, JpaQueryMethod queryMethod, JpaQueryConfiguration queryContext) {
+ return create(queryMethod.getDeclaredQuery(queryString), queryMethod.getEntityInformation(), queryContext);
}
/**
- * Creates an {@link ExpressionBasedStringQuery} from a given {@link DeclaredQuery}.
+ * Create a {@link DefaultEntityQuery} given {@link DeclaredQuery query}, {@link JpaEntityMetadata} and
+ * {@link JpaQueryConfiguration}.
*
- * @param query the original query. Must not be {@literal null}.
- * @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}.
- * @param parser Parser for resolving SpEL expressions. Must not be {@literal null}.
- * @param nativeQuery is a given query native or not
- * @return A query supporting SpEL expressions.
+ * @param declaredQuery must not be {@literal null}.
+ * @param entityMetadata must not be {@literal null}.
+ * @param queryContext must not be {@literal null}.
+ * @return the created {@link DefaultEntityQuery}.
*/
- static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata> metadata,
- ValueExpressionParser parser, boolean nativeQuery) {
- return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery);
+ public static EntityQuery create(DeclaredQuery declaredQuery, JpaEntityMetadata> entityMetadata,
+ JpaQueryConfiguration queryContext) {
+
+ ValueExpressionParser expressionParser = queryContext.getValueExpressionDelegate().getValueExpressionParser();
+ String resolvedExpressionQuery = renderQueryIfExpressionOrReturnQuery(declaredQuery.getQueryString(),
+ entityMetadata, expressionParser);
+
+ return EntityQuery.create(declaredQuery.rewrite(resolvedExpressionQuery), queryContext.getSelector());
}
/**
@@ -92,7 +96,7 @@ static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata>
* @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}.
* @param parser Must not be {@literal null}.
*/
- private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEntityMetadata> metadata,
+ static String renderQueryIfExpressionOrReturnQuery(String query, JpaEntityMetadata> metadata,
ValueExpressionParser parser) {
Assert.notNull(query, "query must not be null");
@@ -103,15 +107,14 @@ private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEnti
return query;
}
- StandardEvaluationContext evalContext = new StandardEvaluationContext();
+ SimpleEvaluationContext evalContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
evalContext.setVariable(ENTITY_NAME, metadata.getEntityName());
query = potentiallyQuoteExpressionsParameter(query);
ValueExpression expr = parser.parse(query);
- String result = Objects.toString(
- expr.evaluate(ValueEvaluationContext.of(DEFAULT_ENVIRONMENT, evalContext)));
+ String result = Objects.toString(expr.evaluate(ValueEvaluationContext.of(DEFAULT_ENVIRONMENT, evalContext)));
if (result == null) {
return query;
@@ -131,4 +134,5 @@ private static String potentiallyQuoteExpressionsParameter(String query) {
private static boolean containsExpression(String query) {
return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
}
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
index 96d6277010..2e24577f8f 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java
@@ -35,17 +35,8 @@
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
import org.springframework.data.jpa.provider.PersistenceProvider;
-import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
-import org.springframework.data.jpa.repository.query.BeanFactoryQueryRewriterProvider;
-import org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory;
-import org.springframework.data.jpa.repository.query.EscapeCharacter;
-import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy;
-import org.springframework.data.jpa.repository.query.JpaQueryMethod;
-import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory;
-import org.springframework.data.jpa.repository.query.Procedure;
-import org.springframework.data.jpa.repository.query.QueryRewriterProvider;
+import org.springframework.data.jpa.repository.query.*;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.EntityPathResolver;
@@ -82,12 +73,12 @@
public class JpaRepositoryFactory extends RepositoryFactorySupport {
private final EntityManager entityManager;
- private final QueryExtractor extractor;
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor;
private final CrudMethodMetadata crudMethodMetadata;
private EntityPathResolver entityPathResolver;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
+ private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR;
private JpaQueryMethodFactory queryMethodFactory;
private QueryRewriterProvider queryRewriterProvider;
@@ -101,7 +92,7 @@ public JpaRepositoryFactory(EntityManager entityManager) {
Assert.notNull(entityManager, "EntityManager must not be null");
this.entityManager = entityManager;
- this.extractor = PersistenceProvider.fromEntityManager(entityManager);
+ PersistenceProvider extractor = PersistenceProvider.fromEntityManager(entityManager);
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
this.entityPathResolver = SimpleEntityPathResolver.INSTANCE;
this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);
@@ -179,6 +170,19 @@ public void setQueryMethodFactory(JpaQueryMethodFactory queryMethodFactory) {
this.queryMethodFactory = queryMethodFactory;
}
+ /**
+ * Configures the {@link QueryEnhancerSelector} to be used. Defaults to
+ * {@link QueryEnhancerSelector#DEFAULT_SELECTOR}.
+ *
+ * @param queryEnhancerSelector must not be {@literal null}.
+ */
+ public void setQueryEnhancerSelector(QueryEnhancerSelector queryEnhancerSelector) {
+
+ Assert.notNull(queryEnhancerSelector, "QueryEnhancerSelector must not be null");
+
+ this.queryEnhancerSelector = queryEnhancerSelector;
+ }
+
/**
* Configures the {@link QueryRewriterProvider} to be used. Defaults to instantiate query rewriters through
* {@link BeanUtils#instantiateClass(Class)}.
@@ -243,8 +247,12 @@ protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoad
@Override
protected Optional getQueryLookupStrategy(@Nullable Key key,
ValueExpressionDelegate valueExpressionDelegate) {
+
+ JpaQueryConfiguration queryConfiguration = new JpaQueryConfiguration(queryRewriterProvider, queryEnhancerSelector,
+ new CachingValueExpressionDelegate(valueExpressionDelegate), escapeCharacter);
+
return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key,
- new CachingValueExpressionDelegate(valueExpressionDelegate), queryRewriterProvider, escapeCharacter));
+ queryConfiguration));
}
@Override
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
index e0a1b00e62..ebb24268d1 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
@@ -18,12 +18,18 @@
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
+import java.util.function.Function;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory;
+import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
@@ -46,10 +52,12 @@
public class JpaRepositoryFactoryBean, S, ID>
extends TransactionalRepositoryFactoryBeanSupport {
+ private @Nullable BeanFactory beanFactory;
private @Nullable EntityManager entityManager;
private EntityPathResolver entityPathResolver;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
private @Nullable JpaQueryMethodFactory queryMethodFactory;
+ private @Nullable Function queryEnhancerSelectorSource;
/**
* Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface.
@@ -75,6 +83,12 @@ public void setMappingContext(MappingContext, ?> mappingContext) {
super.setMappingContext(mappingContext);
}
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ super.setBeanFactory(beanFactory);
+ }
+
/**
* Configures the {@link EntityPathResolver} to be used. Will expect a canonical bean to be present but fallback to
* {@link SimpleEntityPathResolver#INSTANCE} in case none is available.
@@ -101,6 +115,43 @@ public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) {
}
}
+ /**
+ * Configures the {@link QueryEnhancerSelector} to be used. Defaults to
+ * {@link QueryEnhancerSelector#DEFAULT_SELECTOR}.
+ *
+ * @param queryEnhancerSelectorSource must not be {@literal null}.
+ */
+ public void setQueryEnhancerSelectorSource(QueryEnhancerSelector queryEnhancerSelectorSource) {
+ this.queryEnhancerSelectorSource = bf -> queryEnhancerSelectorSource;
+ }
+
+ /**
+ * Configures the {@link QueryEnhancerSelector} to be used.
+ *
+ * @param queryEnhancerSelectorType must not be {@literal null}.
+ */
+ public void setQueryEnhancerSelector(Class extends QueryEnhancerSelector> queryEnhancerSelectorType) {
+
+ this.queryEnhancerSelectorSource = bf -> {
+
+ if (bf != null) {
+
+ ObjectProvider extends QueryEnhancerSelector> beanProvider = bf.getBeanProvider(queryEnhancerSelectorType);
+ QueryEnhancerSelector selector = beanProvider.getIfAvailable();
+
+ if (selector != null) {
+ return selector;
+ }
+
+ if (bf instanceof AutowireCapableBeanFactory acbf) {
+ return acbf.createBean(queryEnhancerSelectorType);
+ }
+ }
+
+ return BeanUtils.instantiateClass(queryEnhancerSelectorType);
+ };
+ }
+
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
@@ -114,15 +165,19 @@ protected RepositoryFactorySupport doCreateRepositoryFactory() {
*/
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
- JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
- jpaRepositoryFactory.setEntityPathResolver(entityPathResolver);
- jpaRepositoryFactory.setEscapeCharacter(escapeCharacter);
+ JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager);
+ factory.setEntityPathResolver(entityPathResolver);
+ factory.setEscapeCharacter(escapeCharacter);
if (queryMethodFactory != null) {
- jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory);
+ factory.setQueryMethodFactory(queryMethodFactory);
+ }
+
+ if (queryEnhancerSelectorSource != null) {
+ factory.setQueryEnhancerSelector(queryEnhancerSelectorSource.apply(beanFactory));
}
- return jpaRepositoryFactory;
+ return factory;
}
@Override
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java
index 6590db4022..3d77980fb6 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java
@@ -34,7 +34,6 @@
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@@ -53,6 +52,9 @@
@ContextConfiguration("classpath:infrastructure.xml")
class AbstractStringBasedJpaQueryIntegrationTests {
+ private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(),
+ QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT);
+
@PersistenceContext EntityManager em;
@Autowired BeanFactory beanFactory;
@@ -66,10 +68,10 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception {
when(mock.getMetamodel()).thenReturn(em.getMetamodel());
JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class);
- AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null, QueryRewriter.IdentityQueryRewriter.INSTANCE,
- ValueExpressionDelegate.create());
+ AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, method.getRequiredDeclaredQuery(), null,
+ CONFIG);
- jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null,
+ jpaQuery.createJpaQuery(method.getRequiredDeclaredQuery(), Sort.unsorted(), null,
method.getResultProcessor().getReturnedType());
verify(mock, times(1)).createQuery(anyString());
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java
index 44d061094f..953203134f 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java
@@ -36,7 +36,6 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -56,6 +55,9 @@
*/
class AbstractStringBasedJpaQueryUnitTests {
+ private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(),
+ QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT);
+
@Test // GH-3310
void shouldNotAttemptToAppendSortIfNoSortArgumentPresent() {
@@ -118,8 +120,8 @@ static InvocationCapturingStringQueryStub forMethod(Class> repository, String
Query query = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class);
- return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery());
-
+ return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery(),
+ CONFIG);
}
static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQuery {
@@ -128,7 +130,7 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu
private final MultiValueMap capturedArguments = new LinkedMultiValueMap<>(3);
InvocationCapturingStringQueryStub(Method targetMethod, JpaQueryMethod queryMethod, String queryString,
- @Nullable String countQueryString) {
+ @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) {
super(queryMethod, new Supplier() {
@Override
@@ -142,14 +144,13 @@ public EntityManager get() {
return em;
}
- }.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class),
- ValueExpressionDelegate.create());
+ }.get(), queryString, countQueryString, queryConfiguration);
this.targetMethod = targetMethod;
}
@Override
- protected String applySorting(CachableQuery query) {
+ protected QueryProvider applySorting(CachableQuery query) {
captureInvocation("applySorting", query);
@@ -157,12 +158,13 @@ protected String applySorting(CachableQuery query) {
}
@Override
- protected jakarta.persistence.Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
+ protected jakarta.persistence.Query createJpaQuery(QueryProvider query, Sort sort,
+ @Nullable Pageable pageable,
ReturnedType returnedType) {
- captureInvocation("createJpaQuery", queryString, sort, pageable, returnedType);
+ captureInvocation("createJpaQuery", query, sort, pageable, returnedType);
- jakarta.persistence.Query jpaQuery = super.createJpaQuery(queryString, sort, pageable, returnedType);
+ jakarta.persistence.Query jpaQuery = super.createJpaQuery(query, sort, pageable, returnedType);
return jpaQuery == null ? Mockito.mock(jakarta.persistence.Query.class) : jpaQuery;
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java
similarity index 85%
rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java
rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java
index 41b36b21d7..874ff77c99 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java
@@ -17,7 +17,6 @@
import static org.assertj.core.api.Assertions.*;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -32,7 +31,7 @@
import org.springframework.data.repository.query.parser.Part.Type;
/**
- * Unit tests for {@link StringQuery}.
+ * Unit tests for {@link DefaultEntityQuery}.
*
* @author Oliver Gierke
* @author Thomas Darimont
@@ -43,13 +42,13 @@
* @author Mark Paluch
* @author Aleksei Elin
*/
-class StringQueryUnitTests {
+class DefaultEntityQueryUnitTests {
@Test // DATAJPA-341
void doesNotConsiderPlainLikeABinding() {
String source = "select u from User u where u.firstname like :firstname";
- StringQuery query = new StringQuery(source, false);
+ DefaultEntityQuery query = new TestEntityQuery(source, false);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(source);
@@ -66,8 +65,8 @@ void doesNotConsiderPlainLikeABinding() {
@Test // DATAJPA-292
void detectsPositionalLikeBindings() {
- StringQuery query = new StringQuery("select u from User u where u.firstname like %?1% or u.lastname like %?2",
- true);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "select u from User u where u.firstname like %?1% or u.lastname like %?2", true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString())
@@ -90,7 +89,7 @@ void detectsPositionalLikeBindings() {
@Test // DATAJPA-292, GH-3041
void detectsAnonymousLikeBindings() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %?% or u.lastname like %? or u.lastname=?", true);
assertThat(query.hasParameterBindings()).isTrue();
@@ -116,7 +115,8 @@ void detectsAnonymousLikeBindings() {
@Test // DATAJPA-292, GH-3041
void detectsNamedLikeBindings() {
- StringQuery query = new StringQuery("select u from User u where u.firstname like %:firstname", true);
+ DefaultEntityQuery query = new TestEntityQuery("select u from User u where u.firstname like %:firstname",
+ true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname");
@@ -133,7 +133,7 @@ void detectsNamedLikeBindings() {
@Test // GH-3041
void rewritesNamedLikeToUniqueParametersIfNecessary() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname",
true);
@@ -164,7 +164,7 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() {
@Test // GH-3784
void rewritesNamedLikeToUniqueParametersRetainingCountQuery() {
- DeclaredQuery query = new StringQuery(
+ ParametrizedQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname",
false).deriveCountQuery(null);
@@ -197,7 +197,7 @@ void rewritesNamedLikeToUniqueParametersRetainingCountQuery() {
@Test // GH-3784
void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() {
- DeclaredQuery query = new StringQuery(
+ ParametrizedQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false)
.deriveCountQuery(null);
@@ -224,7 +224,7 @@ void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() {
@Test // GH-3041
void rewritesPositionalLikeToUniqueParametersIfNecessary() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %?1 or u.firstname like ?1% or u.firstname = ?1", true);
assertThat(query.hasParameterBindings()).isTrue();
@@ -238,7 +238,7 @@ void rewritesPositionalLikeToUniqueParametersIfNecessary() {
@Test // GH-3041
void reusesNamedLikeBindingsWherePossible() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %:firstname or u.firstname like %:firstname% or u.firstname like %:firstname% or u.firstname like %:firstname",
true);
@@ -246,7 +246,8 @@ void reusesNamedLikeBindingsWherePossible() {
assertThat(query.getQueryString()).isEqualTo(
"select u from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname like :firstname_1 or u.firstname like :firstname");
- query = new StringQuery("select u from User u where u.firstname like %:firstname or u.firstname =:firstname", true);
+ query = new TestEntityQuery(
+ "select u from User u where u.firstname like %:firstname or u.firstname =:firstname", true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString())
@@ -256,7 +257,7 @@ void reusesNamedLikeBindingsWherePossible() {
@Test // GH-3041
void reusesPositionalLikeBindingsWherePossible() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where u.firstname like %?1 or u.firstname like %?1% or u.firstname like %?1% or u.firstname like %?1",
false);
@@ -264,7 +265,7 @@ void reusesPositionalLikeBindingsWherePossible() {
assertThat(query.getQueryString()).isEqualTo(
"select u from User u where u.firstname like ?1 or u.firstname like ?2 or u.firstname like ?2 or u.firstname like ?1");
- query = new StringQuery("select u from User u where u.firstname like %?1 or u.firstname =?1", false);
+ query = new TestEntityQuery("select u from User u where u.firstname like %?1 or u.firstname =?1", false);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like ?1 or u.firstname =?2");
@@ -273,7 +274,7 @@ void reusesPositionalLikeBindingsWherePossible() {
@Test // GH-3041
void shouldRewritePositionalBindingsWithParameterReuse() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where u.firstname like ?2 or u.firstname like %?2% or u.firstname like %?1% or u.firstname like %?1 OR u.firstname like ?1",
false);
@@ -295,8 +296,8 @@ void shouldRewritePositionalBindingsWithParameterReuse() {
@Test // GH-3758
void createsDistinctBindingsForIndexedSpel() {
- StringQuery query = new StringQuery("select u from User u where u.firstname = ?#{foo} OR u.firstname = ?#{foo}",
- false);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "select u from User u where u.firstname = ?#{foo} OR u.firstname = ?#{foo}", false);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getParameterBindings()).hasSize(2).extracting(ParameterBinding::getRequiredPosition)
@@ -309,8 +310,8 @@ void createsDistinctBindingsForIndexedSpel() {
@Test // GH-3758
void createsDistinctBindingsForNamedSpel() {
- StringQuery query = new StringQuery("select u from User u where u.firstname = :#{foo} OR u.firstname = :#{foo}",
- false);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "select u from User u where u.firstname = :#{foo} OR u.firstname = :#{foo}", false);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getParameterBindings()).hasSize(2).extracting(ParameterBinding::getOrigin)
@@ -322,7 +323,7 @@ void createsDistinctBindingsForNamedSpel() {
void detectsNamedInParameterBindings() {
String queryString = "select u from User u where u.id in :ids";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -337,7 +338,7 @@ void detectsNamedInParameterBindings() {
void detectsMultipleNamedInParameterBindings() {
String queryString = "select u from User u where u.id in :ids and u.name in :names and foo = :bar";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -354,7 +355,7 @@ void detectsMultipleNamedInParameterBindings() {
void deriveCountQueryWithNamedInRetainsOrigin() {
String queryString = "select u from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins)";
- DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);
+ ParametrizedQuery query = new TestEntityQuery(queryString, false).deriveCountQuery(null);
assertThat(query.getQueryString())
.isEqualTo("select count(u) from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins_1)");
@@ -375,7 +376,7 @@ void deriveCountQueryWithNamedInRetainsOrigin() {
void deriveCountQueryWithPositionalInRetainsOrigin() {
String queryString = "select u from User u where (?1) IS NULL OR LOWER(u.login) IN (?1)";
- DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);
+ ParametrizedQuery query = new TestEntityQuery(queryString, false).deriveCountQuery(null);
assertThat(query.getQueryString())
.isEqualTo("select count(u) from User u where (?1) IS NULL OR LOWER(u.login) IN (?2)");
@@ -396,7 +397,7 @@ void deriveCountQueryWithPositionalInRetainsOrigin() {
void detectsPositionalInParameterBindings() {
String queryString = "select u from User u where u.id in ?1";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -410,7 +411,7 @@ void detectsPositionalInParameterBindings() {
@Test // GH-3126
void allowsReuseOfParameterWithInAndRegularBinding() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where COALESCE(?1) is null OR u.id in ?1 OR COALESCE(?1) is null OR u.id in ?1", true);
assertThat(query.hasParameterBindings()).isTrue();
@@ -423,7 +424,7 @@ void allowsReuseOfParameterWithInAndRegularBinding() {
assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0));
assertPositionalBinding(InParameterBinding.class, 2, bindings.get(1));
- query = new StringQuery(
+ query = new TestEntityQuery(
"select u from User u where COALESCE(:foo) is null OR u.id in :foo OR COALESCE(:foo) is null OR u.id in :foo",
true);
@@ -442,7 +443,7 @@ void allowsReuseOfParameterWithInAndRegularBinding() {
void detectsPositionalInParameterBindingsAndExpressions() {
String queryString = "select u from User u where foo = ?#{bar} and bar = ?3 and baz = ?#{baz}";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.getQueryString()).isEqualTo("select u from User u where foo = ?1 and bar = ?3 and baz = ?2");
}
@@ -451,7 +452,7 @@ void detectsPositionalInParameterBindingsAndExpressions() {
void detectsPositionalInParameterBindingsAndExpressionsWithReuse() {
String queryString = "select u from User u where foo = ?#{bar} and bar = ?2 and baz = ?#{bar}";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.getQueryString()).isEqualTo("select u from User u where foo = ?1 and bar = ?2 and baz = ?3");
}
@@ -459,17 +460,17 @@ void detectsPositionalInParameterBindingsAndExpressionsWithReuse() {
@Test // GH-3126
void countQueryDerivationRetainsNamedExpressionParameters() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where foo = :#{bar} ORDER BY CASE WHEN (u.firstname >= :#{name}) THEN 0 ELSE 1 END",
false);
- DeclaredQuery countQuery = query.deriveCountQuery(null);
+ ParametrizedQuery countQuery = query.deriveCountQuery(null);
assertThat(countQuery.getParameterBindings()).hasSize(1);
assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin)
.extracting(ParameterOrigin::isExpression).isEqualTo(List.of(true));
- query = new StringQuery(
+ query = new TestEntityQuery(
"select u from User u where foo = :#{bar} and bar = :bar ORDER BY CASE WHEN (u.firstname >= :bar) THEN 0 ELSE 1 END",
false);
@@ -484,17 +485,17 @@ void countQueryDerivationRetainsNamedExpressionParameters() {
@Test // GH-3126
void countQueryDerivationRetainsIndexedExpressionParameters() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select u from User u where foo = ?#{bar} ORDER BY CASE WHEN (u.firstname >= ?#{name}) THEN 0 ELSE 1 END",
false);
- DeclaredQuery countQuery = query.deriveCountQuery(null);
+ ParametrizedQuery countQuery = query.deriveCountQuery(null);
assertThat(countQuery.getParameterBindings()).hasSize(1);
assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin)
.extracting(ParameterOrigin::isExpression).isEqualTo(List.of(true));
- query = new StringQuery(
+ query = new TestEntityQuery(
"select u from User u where foo = ?#{bar} and bar = ?1 ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END",
false);
@@ -510,7 +511,7 @@ void countQueryDerivationRetainsIndexedExpressionParameters() {
void detectsMultiplePositionalInParameterBindings() {
String queryString = "select u from User u where u.id in ?1 and u.names in ?2 and foo = ?3";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(queryString);
@@ -526,13 +527,13 @@ void detectsMultiplePositionalInParameterBindings() {
@Test // DATAJPA-373
void handlesMultipleNamedLikeBindingsCorrectly() {
- new StringQuery("select u from User u where u.firstname like %:firstname or foo like :bar", true);
+ new TestEntityQuery("select u from User u where u.firstname like %:firstname or foo like :bar", true);
}
@Test // DATAJPA-461
void treatsGreaterThanBindingAsSimpleBinding() {
- StringQuery query = new StringQuery("select u from User u where u.createdDate > ?1", true);
+ DefaultEntityQuery query = new TestEntityQuery("select u from User u where u.createdDate > ?1", true);
List bindings = query.getParameterBindings();
assertThat(bindings).hasSize(1);
@@ -543,8 +544,10 @@ void treatsGreaterThanBindingAsSimpleBinding() {
@Test // DATAJPA-473
void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() {
- StringQuery query = new StringQuery("SELECT a FROM Article a WHERE a.overview LIKE %:escapedWord% ESCAPE '~'"
- + " OR a.content LIKE %:escapedWord% ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC", true);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "SELECT a FROM Article a WHERE a.overview LIKE %:escapedWord% ESCAPE '~'"
+ + " OR a.content LIKE %:escapedWord% ESCAPE '~' OR a.title = :word ORDER BY a.articleId DESC",
+ true);
List bindings = query.getParameterBindings();
@@ -559,7 +562,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() {
@Test // DATAJPA-483
void detectsInBindingWithParentheses() {
- StringQuery query = new StringQuery("select count(we) from MyEntity we where we.status in (:statuses)", true);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "select count(we) from MyEntity we where we.status in (:statuses)", true);
List bindings = query.getParameterBindings();
@@ -570,7 +574,7 @@ void detectsInBindingWithParentheses() {
@Test // DATAJPA-545
void detectsInBindingWithSpecialFrenchCharactersInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where abonnés in (:abonnés)", true);
+ DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where abonnés in (:abonnés)", true);
List bindings = query.getParameterBindings();
@@ -581,7 +585,7 @@ void detectsInBindingWithSpecialFrenchCharactersInParentheses() {
@Test // DATAJPA-545
void detectsInBindingWithSpecialCharactersInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where øre in (:øre)", true);
+ DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where øre in (:øre)", true);
List bindings = query.getParameterBindings();
@@ -592,7 +596,7 @@ void detectsInBindingWithSpecialCharactersInParentheses() {
@Test // DATAJPA-545
void detectsInBindingWithSpecialAsianCharactersInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where 생일 in (:생일)", true);
+ DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where 생일 in (:생일)", true);
List bindings = query.getParameterBindings();
@@ -603,7 +607,7 @@ void detectsInBindingWithSpecialAsianCharactersInParentheses() {
@Test // DATAJPA-545
void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses() {
- StringQuery query = new StringQuery("select * from MyEntity where foo in (:ab1babc생일233)", true);
+ DefaultEntityQuery query = new TestEntityQuery("select * from MyEntity where foo in (:ab1babc생일233)", true);
List bindings = query.getParameterBindings();
@@ -614,7 +618,7 @@ void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses()
@Test // DATAJPA-712, GH-3619
void shouldReplaceAllNamedExpressionParametersWithInClause() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select a from A a where a.b in :#{#bs} and a.c in :#{#cs} and a.d in :${foo.bar}", true);
String queryString = query.getQueryString();
@@ -625,7 +629,7 @@ void shouldReplaceAllNamedExpressionParametersWithInClause() {
@Test // DATAJPA-712
void shouldReplaceExpressionWithLikeParameters() {
- StringQuery query = new StringQuery(
+ DefaultEntityQuery query = new TestEntityQuery(
"select a from A a where a.b LIKE :#{#filter.login}% and a.c LIKE %:#{#filter.login}", true);
String queryString = query.getQueryString();
@@ -636,8 +640,8 @@ void shouldReplaceExpressionWithLikeParameters() {
@Test // DATAJPA-712, GH-3619
void shouldReplaceAllPositionExpressionParametersWithInClause() {
- StringQuery query = new StringQuery("select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs} and a.d in ?${foo}",
- true);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "select a from A a where a.b in ?#{#bs} and a.c in ?#{#cs} and a.d in ?${foo}", true);
String queryString = query.getQueryString();
assertThat(queryString).isEqualTo("select a from A a where a.b in ?1 and a.c in ?2 and a.d in ?3");
@@ -653,12 +657,11 @@ void shouldReplaceAllPositionExpressionParametersWithInClause() {
@Test // DATAJPA-864
void detectsConstructorExpressions() {
- assertThat(
- new StringQuery("select new com.example.Dto(a.foo, a.bar) from A a", false).hasConstructorExpression())
- .isTrue();
- assertThat(new StringQuery("select new com.example.Dto (a.foo, a.bar) from A a", false).hasConstructorExpression())
- .isTrue();
- assertThat(new StringQuery("select a from A a", true).hasConstructorExpression()).isFalse();
+ assertThat(new TestEntityQuery("select new com.example.Dto(a.foo, a.bar) from A a", false)
+ .hasConstructorExpression()).isTrue();
+ assertThat(new TestEntityQuery("select new com.example.Dto (a.foo, a.bar) from A a", false)
+ .hasConstructorExpression()).isTrue();
+ assertThat(new TestEntityQuery("select a from A a", true).hasConstructorExpression()).isFalse();
}
/**
@@ -669,14 +672,16 @@ void detectsConstructorExpressions() {
void detectsConstructorExpressionForDefaultConstructor() {
// Parentheses required
- assertThat(new StringQuery("select new com.example.Dto(a.name) from A a", false).hasConstructorExpression())
+ assertThat(
+ new TestEntityQuery("select new com.example.Dto(a.name) from A a", false).hasConstructorExpression())
.isTrue();
}
@Test // DATAJPA-1179
void bindingsMatchQueryForIdenticalSpelExpressions() {
- StringQuery query = new StringQuery("select a from A a where a.first = :#{#exp} or a.second = :#{#exp}", true);
+ DefaultEntityQuery query = new TestEntityQuery(
+ "select a from A a where a.first = :#{#exp} or a.second = :#{#exp}", true);
List bindings = query.getParameterBindings();
assertThat(bindings).isNotEmpty();
@@ -703,7 +708,7 @@ void getProjection() {
void checkProjection(String query, String expected, String description, boolean nativeQuery) {
- assertThat(new StringQuery(query, nativeQuery).getProjection()) //
+ assertThat(new TestEntityQuery(query, nativeQuery).getProjection()) //
.as("%s (%s)", description, query) //
.isEqualTo(expected);
}
@@ -727,7 +732,7 @@ void getAlias() {
private void checkAlias(String query, String expected, String description, boolean nativeQuery) {
- assertThat(new StringQuery(query, nativeQuery).getAlias()) //
+ assertThat(new TestEntityQuery(query, nativeQuery).getAlias()) //
.as("%s (%s)", description, query) //
.isEqualTo(expected);
}
@@ -780,7 +785,7 @@ void ignoresQuotedNamedParameterLookAlike() {
void detectsMultiplePositionalParameterBindingsWithoutIndex() {
String queryString = "select u from User u where u.id in ? and u.names in ? and foo = ?";
- StringQuery query = new StringQuery(queryString, false);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, false);
assertThat(query.getQueryString()).isEqualTo(queryString);
assertThat(query.hasParameterBindings()).isTrue();
@@ -800,16 +805,18 @@ void failOnMixedBindingsWithoutIndex() {
for (String testQuery : testQueries) {
Assertions.assertThatExceptionOfType(IllegalArgumentException.class) //
- .describedAs(testQuery).isThrownBy(() -> new StringQuery(testQuery, false));
+ .describedAs(testQuery).isThrownBy(() -> new TestEntityQuery(testQuery, false));
}
}
@Test // DATAJPA-1307
void makesUsageOfJdbcStyleParameterAvailable() {
- assertThat(new StringQuery("from Something something where something = ?", false).usesJdbcStyleParameters())
+ assertThat(
+ new TestEntityQuery("from Something something where something = ?", false).usesJdbcStyleParameters())
.isTrue();
- assertThat(new StringQuery("from Something something where something =?", false).usesJdbcStyleParameters())
+ assertThat(
+ new TestEntityQuery("from Something something where something =?", false).usesJdbcStyleParameters())
.isTrue();
List testQueries = Arrays.asList( //
@@ -820,7 +827,7 @@ void makesUsageOfJdbcStyleParameterAvailable() {
for (String testQuery : testQueries) {
- assertThat(new StringQuery(testQuery, false) //
+ assertThat(new TestEntityQuery(testQuery, false) //
.usesJdbcStyleParameters()) //
.describedAs(testQuery) //
.describedAs(testQuery) //
@@ -832,7 +839,7 @@ void makesUsageOfJdbcStyleParameterAvailable() {
void questionMarkInStringLiteral() {
String queryString = "select '? ' from dual";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.getQueryString()).isEqualTo(queryString);
assertThat(query.hasParameterBindings()).isFalse();
@@ -852,7 +859,7 @@ void isNotDefaultProjection() {
"select a, b from C");
for (String queryString : queriesWithoutDefaultProjection) {
- assertThat(new StringQuery(queryString, true).isDefaultProjection()) //
+ assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) //
.describedAs(queryString) //
.isFalse();
}
@@ -869,7 +876,7 @@ void isNotDefaultProjection() {
);
for (String queryString : queriesWithDefaultProjection) {
- assertThat(new StringQuery(queryString, true).isDefaultProjection()) //
+ assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) //
.describedAs(queryString) //
.isTrue();
}
@@ -879,7 +886,7 @@ void isNotDefaultProjection() {
void questionMarkInStringLiteralWithParameters() {
String queryString = "SELECT CAST(REGEXP_SUBSTR(itp.template_as_txt, '(?<=templateId\\\\\\\\=)(\\\\\\\\d+)(?:\\\\\\\\R)') AS INT) AS templateId FROM foo itp WHERE bar = ?1 AND baz = 1";
- StringQuery query = new StringQuery(queryString, false);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, false);
assertThat(query.getQueryString()).isEqualTo(queryString);
assertThat(query.hasParameterBindings()).isTrue();
@@ -891,7 +898,7 @@ void questionMarkInStringLiteralWithParameters() {
void usingPipesWithNamedParameter() {
String queryString = "SELECT u FROM User u WHERE u.lastname LIKE '%'||:name||'%'";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.getParameterBindings()) //
.extracting(ParameterBinding::getName) //
@@ -902,7 +909,7 @@ void usingPipesWithNamedParameter() {
void usingGreaterThanWithNamedParameter() {
String queryString = "SELECT u FROM User u WHERE :age>u.age";
- StringQuery query = new StringQuery(queryString, true);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(query.getParameterBindings()) //
.extracting(ParameterBinding::getName) //
@@ -911,23 +918,24 @@ void usingGreaterThanWithNamedParameter() {
void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) {
- DeclaredQuery declaredQuery = DeclaredQuery.of(query, nativeQuery);
+ DeclaredQuery declaredQuery = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
+ EntityQuery introspectedQuery = EntityQuery.create(declaredQuery, QueryEnhancerSelector.DEFAULT_SELECTOR);
- assertThat(declaredQuery.hasNamedParameter()) //
+ assertThat(introspectedQuery.hasNamedParameter()) //
.describedAs("hasNamed Parameter " + label) //
.isEqualTo(expectedSize > 0);
- assertThat(declaredQuery.getParameterBindings()) //
+ assertThat(introspectedQuery.getParameterBindings()) //
.describedAs("parameterBindings " + label) //
.hasSize(expectedSize);
}
private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) {
- List bindings = new ArrayList<>();
- StringQuery.ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
- bindings, new StringQuery.Metadata());
+ DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
+ PreprocessedQuery bindableQuery = PreprocessedQuery.ParameterBindingParser.INSTANCE.parse(source.getQueryString(),
+ source::rewrite, it -> {});
- assertThat(bindings.stream().anyMatch(it -> it.getIdentifier().hasName())) //
+ assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) //
.describedAs(String.format("<%s> (%s)", query, label)) //
.isEqualTo(expected);
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java
index 6b9c4e2478..7dd6dd757c 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java
@@ -21,6 +21,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
/**
* TCK Tests for {@link DefaultQueryEnhancer}.
@@ -31,8 +33,8 @@
class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
- QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) {
- return new DefaultQueryEnhancer(declaredQuery);
+ QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
+ return new DefaultQueryEnhancer(query);
}
@Override
@@ -43,9 +45,10 @@ void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {}
@Test // GH-3546
void shouldApplySorting() {
- QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true));
+ QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("SELECT e FROM Employee e"));
- String sql = enhancer.applySorting(Sort.by("foo", "bar"));
+ String sql = enhancer.rewrite(new DefaultQueryRewriteInformation(Sort.by("foo", "bar"),
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())));
assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc");
}
@@ -53,9 +56,11 @@ void shouldApplySorting() {
@Test // GH-3811
void shouldApplySortingWithNullHandling() {
- QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true));
+ QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("SELECT e FROM Employee e"));
- String sql = enhancer.applySorting(Sort.by(Sort.Order.asc("foo").nullsFirst(), Sort.Order.asc("bar").nullsLast()));
+ String sql = enhancer.rewrite(new DefaultQueryRewriteInformation(
+ Sort.by(Sort.Order.asc("foo").nullsFirst(), Sort.Order.asc("bar").nullsLast()),
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())));
assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc nulls first, e.bar asc nulls last");
}
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 8895fc4c19..dbe4d45a9f 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
@@ -30,9 +30,9 @@ public class EqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
- assumeThat(query.isNativeQuery()).isFalse();
+ assumeThat(query.isNative()).isFalse();
- return JpaQueryEnhancer.forEql(query);
+ return JpaQueryEnhancer.forEql(query.getQueryString());
}
@Override
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 782c460a24..8f93859699 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
@@ -29,6 +29,8 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
/**
* Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
@@ -221,7 +223,9 @@ void applySortingAccountsForNewlinesInSubselect() {
where exists (select u2
from user u2
)
- """).applySorting(sort)).isEqualToIgnoringWhitespace("""
+ """).rewrite(new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))))
+ .isEqualToIgnoringWhitespace("""
select u
from user u
where exists (select u2
@@ -803,7 +807,8 @@ private void assertCountQuery(String originalQuery, String countQuery) {
}
private String createQueryFor(String query, Sort sort) {
- return newParser(query).applySorting(sort);
+ return newParser(query).rewrite(new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())));
}
private String createCountQueryFor(String query) {
@@ -827,6 +832,6 @@ private String projection(String query) {
}
private QueryEnhancer newParser(String query) {
- return JpaQueryEnhancer.forEql(DeclaredQuery.of(query, false));
+ return JpaQueryEnhancer.forEql(query);
}
}
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 ef7b269115..f25e9fc2ee 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
@@ -30,9 +30,9 @@ public class HqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
- assumeThat(query.isNativeQuery()).isFalse();
+ assumeThat(query.isNative()).isFalse();
- return JpaQueryEnhancer.forHql(query);
+ return JpaQueryEnhancer.forHql(query.getQueryString());
}
@Override
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 1098f6a623..cd2c3987fc 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
@@ -33,6 +33,8 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.StringUtils;
/**
@@ -280,7 +282,9 @@ void applySortingAccountsForNewlinesInSubselect() {
where exists (select u2
from user u2
)
- """).applySorting(sort)).isEqualToIgnoringWhitespace("""
+ """).rewrite(new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))))
+ .isEqualToIgnoringWhitespace("""
select u
from user u
where exists (select u2
@@ -1172,7 +1176,8 @@ private void assertCountQuery(String originalQuery, String countQuery) {
}
private String createQueryFor(String query, Sort sort) {
- return newParser(query).applySorting(sort);
+ return newParser(query).rewrite(new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())));
}
private String createCountQueryFor(String query) {
@@ -1196,6 +1201,6 @@ private String projection(String query) {
}
private QueryEnhancer newParser(String query) {
- return JpaQueryEnhancer.forHql(DeclaredQuery.of(query, false));
+ return JpaQueryEnhancer.forHql(query);
}
}
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 dee9d10d66..4a0be8de58 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
@@ -25,6 +25,8 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
/**
* TCK Tests for {@link JSqlParserQueryEnhancer}.
@@ -37,16 +39,17 @@
public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
- QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) {
- return new JSqlParserQueryEnhancer(declaredQuery);
+ QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
+ return new JSqlParserQueryEnhancer(query);
}
@Test // GH-3546
void shouldApplySorting() {
- QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true));
+ QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery("SELECT e FROM Employee e"));
- String sql = enhancer.applySorting(Sort.by("foo", "bar"));
+ String sql = enhancer.rewrite(new DefaultQueryRewriteInformation(Sort.by("foo", "bar"),
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())));
assertThat(sql).isEqualTo("SELECT e FROM Employee e ORDER BY e.foo ASC, e.bar ASC");
}
@@ -54,15 +57,15 @@ void shouldApplySorting() {
@Test // GH-3707
void countQueriesShouldConsiderPrimaryTableAlias() {
- QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("""
+ QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("""
SELECT DISTINCT a.*, b.b1
FROM TableA a
JOIN TableB b ON a.b = b.b
LEFT JOIN TableC c ON b.c = c.c
ORDER BY b.b1, a.a1, a.a2
- """, true));
+ """));
- String sql = enhancer.createCountQueryFor();
+ String sql = enhancer.createCountQueryFor(null);
assertThat(sql).startsWith("SELECT count(DISTINCT a.*) FROM TableA a");
}
@@ -82,16 +85,16 @@ void setOperationListWorks() {
+ "except \n" //
+ "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE";
- StringQuery stringQuery = new StringQuery(setQuery, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(setQuery, true);
+ QueryEnhancer queryEnhancer = QueryEnhancer.create(query);
- assertThat(stringQuery.getAlias()).isNullOrEmpty();
- assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN");
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isNullOrEmpty();
+ assertThat(query.getProjection()).isEqualToIgnoringCase("SOME_COLUMN");
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery);
- assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN"))).endsWith("ORDER BY SOME_COLUMN ASC");
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery);
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("SOME_COLUMN"))))
+ .endsWith("ORDER BY SOME_COLUMN ASC");
assertThat(queryEnhancer.detectAlias()).isNullOrEmpty();
assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN");
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -105,16 +108,16 @@ void complexSetOperationListWorks() {
+ "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE \n" //
+ "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE";
- StringQuery stringQuery = new StringQuery(setQuery, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(setQuery, true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
- assertThat(stringQuery.getAlias()).isNullOrEmpty();
- assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN");
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isNullOrEmpty();
+ assertThat(query.getProjection()).isEqualToIgnoringCase("SOME_COLUMN");
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery);
- assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN").ascending())).endsWith("ORDER BY SOME_COLUMN ASC");
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery);
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("SOME_COLUMN").ascending())))
+ .endsWith("ORDER BY SOME_COLUMN ASC");
assertThat(queryEnhancer.detectAlias()).isNullOrEmpty();
assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN");
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -132,16 +135,16 @@ void deeplyNestedcomplexSetOperationListWorks() {
+ "\tselect CustomerID from customers where country = 'Germany'\n"//
+ "\t;";
- StringQuery stringQuery = new StringQuery(setQuery, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(setQuery, true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
- assertThat(stringQuery.getAlias()).isNullOrEmpty();
- assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID");
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isNullOrEmpty();
+ assertThat(query.getProjection()).isEqualToIgnoringCase("CustomerID");
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery);
- assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).endsWith("ORDER BY CustomerID DESC");
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery);
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("CustomerID").descending())))
+ .endsWith("ORDER BY CustomerID DESC");
assertThat(queryEnhancer.detectAlias()).isNullOrEmpty();
assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("CustomerID");
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -152,16 +155,15 @@ void valuesStatementsWorks() {
String setQuery = "VALUES (1, 2, 'test')";
- StringQuery stringQuery = new StringQuery(setQuery, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(setQuery, true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
- assertThat(stringQuery.getAlias()).isNullOrEmpty();
- assertThat(stringQuery.getProjection()).isNullOrEmpty();
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isNullOrEmpty();
+ assertThat(query.getProjection()).isNullOrEmpty();
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery);
- assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).isEqualTo(setQuery);
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(setQuery);
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("CustomerID").descending()))).isEqualTo(setQuery);
assertThat(queryEnhancer.detectAlias()).isNullOrEmpty();
assertThat(queryEnhancer.getProjection()).isNullOrEmpty();
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -173,18 +175,18 @@ void withStatementsWorks() {
String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))) \n"
+ "select day, value from sample_data as a";
- StringQuery stringQuery = new StringQuery(setQuery, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(setQuery, true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
- assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a");
- assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value");
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isEqualToIgnoringCase("a");
+ assertThat(query.getProjection()).isEqualToIgnoringCase("day, value");
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(
+ assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(
"with sample_data (day, value) AS (VALUES ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))) "
+ "SELECT count(1) FROM sample_data AS a");
- assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC");
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("day").descending())))
+ .endsWith("ORDER BY a.day DESC");
assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a");
assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value");
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -196,18 +198,18 @@ void multipleWithStatementsWorks() {
String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))), test2 as (values (1,2,3)) \n"
+ "select day, value from sample_data as a";
- StringQuery stringQuery = new StringQuery(setQuery, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(setQuery, true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
- assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a");
- assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value");
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isEqualToIgnoringCase("a");
+ assertThat(query.getProjection()).isEqualToIgnoringCase("day, value");
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(
+ assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(
"with sample_data (day, value) AS (VALUES ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))), test2 AS (VALUES (1, 2, 3)) "
+ "SELECT count(1) FROM sample_data AS a");
- assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC");
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("day").descending())))
+ .endsWith("ORDER BY a.day DESC");
assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a");
assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value");
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -216,15 +218,15 @@ void multipleWithStatementsWorks() {
@Test // GH-3038
void truncateStatementShouldWork() {
- StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery("TRUNCATE TABLE foo", true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
- assertThat(stringQuery.getAlias()).isNull();
- assertThat(stringQuery.getProjection()).isEmpty();
- assertThat(stringQuery.hasConstructorExpression()).isFalse();
+ assertThat(query.getAlias()).isNull();
+ assertThat(query.getProjection()).isEmpty();
+ assertThat(query.hasConstructorExpression()).isFalse();
- assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).isEqualTo("TRUNCATE TABLE foo");
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
+ assertThat(queryEnhancer.rewrite(getRewriteInformation(Sort.by("day").descending())))
+ .isEqualTo("TRUNCATE TABLE foo");
assertThat(queryEnhancer.detectAlias()).isNull();
assertThat(queryEnhancer.getProjection()).isEmpty();
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -232,15 +234,14 @@ void truncateStatementShouldWork() {
@ParameterizedTest // GH-2641
@MethodSource("mergeStatementWorksSource")
- void mergeStatementWorksWithJSqlParser(String query, String alias) {
+ void mergeStatementWorksWithJSqlParser(String queryString, String alias) {
- StringQuery stringQuery = new StringQuery(query, true);
- QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery);
+ DefaultEntityQuery query = new TestEntityQuery(queryString, true);
+ QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
assertThat(queryEnhancer.detectAlias()).isEqualTo(alias);
- assertThat(QueryUtils.detectAlias(query)).isNull();
+ assertThat(QueryUtils.detectAlias(queryString)).isNull();
- assertThat(queryEnhancer.getJoinAliases()).isEmpty();
assertThat(queryEnhancer.detectAlias()).isEqualTo(alias);
assertThat(queryEnhancer.getProjection()).isEmpty();
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -257,4 +258,9 @@ static Stream mergeStatementWorksSource() {
null));
}
+ private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) {
+ return new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()));
+ }
+
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java
index 861272154b..e68faf4092 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java
@@ -34,7 +34,6 @@
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
-import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -64,7 +63,8 @@
@MockitoSettings(strictness = Strictness.LENIENT)
class JpaQueryLookupStrategyUnitTests {
- private static final ValueExpressionDelegate VALUE_EXPRESSION_DELEGATE = ValueExpressionDelegate.create();
+ private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(),
+ QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT);
@Mock EntityManager em;
@Mock EntityManagerFactory emf;
@@ -72,7 +72,6 @@ class JpaQueryLookupStrategyUnitTests {
@Mock NamedQueries namedQueries;
@Mock Metamodel metamodel;
@Mock ProjectionFactory projectionFactory;
- @Mock BeanFactory beanFactory;
private JpaQueryMethodFactory queryMethodFactory;
@@ -90,7 +89,7 @@ void setUp() {
void invalidAnnotatedQueryCausesException() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
- VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
+ CONFIG);
Method method = UserRepository.class.getMethod("findByFoo", String.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
@@ -102,7 +101,7 @@ void invalidAnnotatedQueryCausesException() throws Exception {
void considersNamedCountQuery() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
- VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
+ CONFIG);
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo");
@@ -124,7 +123,7 @@ void considersNamedCountQuery() throws Exception {
void considersNamedCountOnStringQueryQuery() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
- VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
+ CONFIG);
when(namedQueries.hasQuery("foo.count")).thenReturn(true);
when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo");
@@ -143,7 +142,7 @@ void considersNamedCountOnStringQueryQuery() throws Exception {
void prefersDeclaredQuery() throws Exception {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
- VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
+ CONFIG);
Method method = UserRepository.class.getMethod("annotatedQueryWithQueryAndQueryName");
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
@@ -156,7 +155,7 @@ void prefersDeclaredQuery() throws Exception {
void namedQueryWithSortShouldThrowIllegalStateException() throws NoSuchMethodException {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
- VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
+ CONFIG);
Method method = UserRepository.class.getMethod("customNamedQuery", String.class, Sort.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
@@ -181,7 +180,7 @@ void noQueryShouldNotBeInvoked() {
void customQueryWithQuestionMarksShouldWork() throws NoSuchMethodException {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
- VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT);
+ CONFIG);
Method namedMethod = UserRepository.class.getMethod("customQueryWithQuestionMarksAndNamedParam", String.class);
RepositoryMetadata namedMetadata = new DefaultRepositoryMetadata(UserRepository.class);
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java
index 9738c7843a..fa335ecee6 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java
@@ -15,8 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.entry;
+import static org.assertj.core.api.Assertions.*;
import java.util.HashMap;
import java.util.List;
@@ -25,6 +24,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@@ -40,8 +40,11 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
+import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.util.ReflectionTestUtils;
/**
* Unit tests for repository with {@link Query} and {@link QueryRewrite}.
@@ -54,6 +57,7 @@
class JpaQueryRewriteIntegrationTests {
@Autowired private UserRepositoryWithRewriter repository;
+ @Autowired private JpaRepositoryFactoryBean factoryBean;
// Results
static final String ORIGINAL_QUERY = "original query";
@@ -66,6 +70,14 @@ void setUp() {
results.clear();
}
+ @Test
+ void shouldConfigureQueryEnhancerSelector() {
+
+ JpaRepositoryFactory factory = (JpaRepositoryFactory) ReflectionTestUtils.getField(factoryBean, "factory");
+
+ assertThat(factory).extracting("queryEnhancerSelector").isInstanceOf(MyQueryEnhancerSelector.class);
+ }
+
@Test
void nativeQueryShouldHandleRewrites() {
@@ -222,7 +234,8 @@ private static String replaceAlias(String query, Sort sort) {
@ImportResource("classpath:infrastructure.xml")
@EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = UserRepositoryWithRewriter.class, //
includeFilters = @ComponentScan.Filter(value = { UserRepositoryWithRewriter.class },
- type = FilterType.ASSIGNABLE_TYPE))
+ type = FilterType.ASSIGNABLE_TYPE),
+ queryEnhancerSelector = MyQueryEnhancerSelector.class)
static class JpaRepositoryConfig {
@Bean
@@ -231,4 +244,10 @@ QueryRewriter queryRewriter() {
}
}
+
+ static class MyQueryEnhancerSelector extends QueryEnhancerSelector.DefaultQueryEnhancerSelector {
+ public MyQueryEnhancerSelector() {
+ super(QueryEnhancerFactories.fallback(), DefaultQueryEnhancerSelector.jpql());
+ }
+ }
}
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 8b6385e65d..44256fe4c9 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
@@ -30,9 +30,9 @@ public class JpqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
- assumeThat(query.isNativeQuery()).isFalse();
+ assumeThat(query.isNative()).isFalse();
- return JpaQueryEnhancer.forJpql(query);
+ return JpaQueryEnhancer.forJpql(query.getQueryString());
}
@Override
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 acc6617811..39ed9b6d9d 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
@@ -29,6 +29,8 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.query.ReturnedType;
/**
* Verify that JPQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
@@ -216,13 +218,16 @@ void applySortingAccountsForNewlinesInSubselect() {
Sort sort = Sort.by(Sort.Order.desc("age"));
+
assertThat(newParser("""
select u
from user u
where exists (select u2
from user u2
)
- """).applySorting(sort)).isEqualToIgnoringWhitespace("""
+ """).rewrite(new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()))))
+ .isEqualToIgnoringWhitespace("""
select u
from user u
where exists (select u2
@@ -808,7 +813,8 @@ private void assertCountQuery(String originalQuery, String countQuery) {
}
private String createQueryFor(String query, Sort sort) {
- return newParser(query).applySorting(sort);
+ return newParser(query).rewrite(new DefaultQueryRewriteInformation(sort,
+ ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())));
}
private String createCountQueryFor(String query) {
@@ -832,6 +838,6 @@ private String projection(String query) {
}
private QueryEnhancer newParser(String query) {
- return JpaQueryEnhancer.forJpql(DeclaredQuery.of(query, false));
+ return JpaQueryEnhancer.forJpql(query);
}
}
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java
index e85ff114f1..d438cdf9a6 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java
@@ -89,7 +89,7 @@ void strictErrorHandlingThrowsExceptionForAllVariationsOfParameters() {
softly
.assertThatThrownBy(
- () -> setter.setParameter(BindableQuery.from(query), methodArguments, STRICT)) //
+ () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, STRICT)) //
.describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", //
parameter.getClass(), //
parameter.getName(), //
@@ -118,7 +118,7 @@ void lenientErrorHandlingThrowsNoExceptionForAllVariationsOfParameters() {
softly
.assertThatCode(
- () -> setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT)) //
+ () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT)) //
.describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", //
parameter.getClass(), //
parameter.getName(), //
@@ -149,7 +149,7 @@ void lenientSetsParameterWhenSuccessIsUnsure() {
temporalType //
);
- setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT);
+ setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT);
if (temporalType == null) {
verify(query).setParameter(eq(11), any(Date.class));
@@ -179,7 +179,7 @@ void parameterNotSetWhenSuccessImpossible() {
temporalType //
);
- setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT);
+ setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT);
if (temporalType == null) {
verify(query, never()).setParameter(anyInt(), any(Date.class));
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java
index dadfd1083d..77a8496122 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java
@@ -88,7 +88,7 @@ void rejectsPersistenceProviderIfIncapableOfExtractingQueriesAndPagebleBeingUsed
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, projectionFactory, extractor);
when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenThrow(new IllegalArgumentException());
- assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em));
+ assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR));
}
@Test // DATAJPA-142
@@ -100,7 +100,7 @@ void doesNotRejectPersistenceProviderIfNamedCountQueryIsAvailable() {
TypedQuery countQuery = mock(TypedQuery.class);
when(em.createNamedQuery(eq(queryMethod.getNamedCountQueryName()), eq(Long.class))).thenReturn(countQuery);
- NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em);
+ NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR);
query.doCreateCountQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[1]));
verify(em, times(1)).createNamedQuery(queryMethod.getNamedCountQueryName(), Long.class);
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java
index cf9dab51fb..c17cc49f94 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java
@@ -30,11 +30,9 @@
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
-import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -72,13 +70,14 @@ void shouldApplySorting() {
JpaQueryMethod queryMethod = new JpaQueryMethod(respositoryMethod, repositoryMetadata, projectionFactory,
queryExtractor);
- Query annotation = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class);
+ NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, queryMethod.getRequiredDeclaredQuery(),
+ queryMethod.getDeclaredCountQuery(),
+ new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR,
+ ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT));
+ QueryProvider sql = query.getSortedQuery(Sort.by("foo", "bar"),
+ queryMethod.getResultProcessor().getReturnedType());
- NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(),
- QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create());
- String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType());
-
- assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc");
+ assertThat(sql.getQueryString()).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc");
}
interface TestRepo extends Repository