Polishing.

Introduce refined names: EntityQuery, TemplatedQuery, ParametrizedQuery, QueryProvider. Return QueryProvider where possible. Introduce rewrite as concept on DeclaredQuery to retain its nature and track the origin of the query rewriting.

Move methods solely used in tests to TestDefaultEntityQuery.

Remove unused methods, fix naming, group DeclaredQuery implementations in DeclaredQueries. Add documentation.

See #3622
Original pull request: #3527
This commit is contained in:
Mark Paluch
2025-03-17 15:19:59 +01:00
parent 5da86f1bf0
commit e2446caf38
55 changed files with 1490 additions and 1198 deletions

View File

@@ -27,6 +27,8 @@ import org.openjdk.jmh.annotations.Timeout;
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 class HqlParserBenchmarks {
DeclaredQuery query;
Sort sort = Sort.by("foo");
QueryEnhancer enhancer;
QueryEnhancer.QueryRewriteInformation rewriteInformation;
@Setup(Level.Iteration)
public void doSetup() {
@@ -57,12 +60,14 @@ public class HqlParserBenchmarks {
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);
}
}

View File

@@ -29,6 +29,8 @@ import org.openjdk.jmh.annotations.Timeout;
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 class JSqlParserQueryEnhancerBenchmarks {
JSqlParserQueryEnhancer enhancer;
Sort sort = Sort.by("foo");
private byte[] serialized;
private QueryEnhancer.QueryRewriteInformation rewriteInformation;
@Setup(Level.Iteration)
public void doSetup() throws IOException {
@@ -57,12 +60,14 @@ public class JSqlParserQueryEnhancerBenchmarks {
union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE""";
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);
}
}

View File

@@ -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 @@ public interface JpaSpecificationExecutor<T> {
* Returns a {@link Page} of entities matching the given {@link Specification}.
* <p>
* Supports counting the total number of entities matching the {@link Specification}.
* <p>
*
* @param spec can be {@literal null}, if no {@link Specification} is given all entities matching {@code <T>} will be
* selected.

View File

@@ -94,4 +94,5 @@ public @interface NativeQuery {
* Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query.
*/
String sqlResultSetMapping() default "";
}

View File

@@ -90,4 +90,5 @@ public @interface Query {
* @since 3.0
*/
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
}

View File

@@ -178,4 +178,5 @@ public @interface EnableJpaRepositories {
* @since 4.0
*/
Class<? extends QueryEnhancerSelector> queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class;
}

View File

@@ -23,9 +23,9 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
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;
@@ -54,9 +54,9 @@ import org.springframework.util.StringUtils;
*/
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
private final StringQuery query;
private final EntityQuery query;
private final Map<Class<?>, Boolean> knownProjections = new ConcurrentHashMap<>();
private final Lazy<IntrospectedQuery> countQuery;
private final Lazy<ParametrizedQuery> countQuery;
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryRewriter queryRewriter;
private final QuerySortRewriter querySortRewriter;
@@ -70,25 +70,42 @@ 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 countQuery can be {@literal null} if not defined.
* @param queryConfiguration must not be {@literal null}.
*/
public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
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(query, "Query must not be null");
Assert.notNull(queryConfiguration, "JpaQueryConfiguration must not be null");
this.valueExpressionDelegate = queryConfiguration.getValueExpressionDelegate();
this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters());
this.query = ExpressionBasedStringQuery.create(queryString, method, queryConfiguration);
this.query = TemplatedQuery.create(query, method.getEntityInformation(), queryConfiguration);
this.countQuery = Lazy.of(() -> {
if (StringUtils.hasText(countQueryString)) {
return ExpressionBasedStringQuery.create(countQueryString, method, queryConfiguration);
if (countQuery != null) {
return TemplatedQuery.create(countQuery, method.getEntityInformation(), queryConfiguration);
}
return this.query.deriveCountQuery(method.getCountQueryProjection());
@@ -114,14 +131,18 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
"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 = getReturnedType(processor);
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.
@@ -212,7 +233,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
return createBinder(query);
}
protected ParameterBinder createBinder(IntrospectedQuery query) {
protected ParameterBinder createBinder(ParametrizedQuery query) {
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query,
valueExpressionDelegate, valueExpressionContextProvider);
}
@@ -245,7 +266,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
/**
* @return the countQuery
*/
public IntrospectedQuery getCountQuery() {
public ParametrizedQuery getCountQuery() {
return countQuery.get();
}
@@ -253,11 +274,11 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* 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();
String queryToUse = potentiallyRewriteQuery(queryString, sort, pageable);
String queryToUse = potentiallyRewriteQuery(query.getQueryString(), sort, pageable);
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
return em.createQuery(queryToUse);
@@ -286,8 +307,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
: queryRewriter.rewrite(originalQuery, sort);
}
String applySorting(CachableQuery cachableQuery) {
return cachableQuery.getDeclaredQuery().getQueryEnhancer()
QueryProvider applySorting(CachableQuery cachableQuery) {
return cachableQuery.getDeclaredQuery()
.rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType()));
}
@@ -295,7 +316,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
* Query Sort Rewriter interface.
*/
interface QuerySortRewriter {
String getSorted(StringQuery query, Sort sort, ReturnedType returnedType);
QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType);
}
/**
@@ -305,28 +326,28 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
INSTANCE;
public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) {
return query.getQueryEnhancer().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(StringQuery 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 = query.getQueryEnhancer()
QueryProvider cachedQuery = this.cachedQuery;
if (cachedQuery == null) {
this.cachedQuery = cachedQuery = query
.rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}
return cachedQueryString;
return cachedQuery;
}
}
@@ -335,22 +356,22 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
*/
class CachingQuerySortRewriter implements QuerySortRewriter {
private final ConcurrentLruCache<CachableQuery, String> queryCache = new ConcurrentLruCache<>(16,
private final ConcurrentLruCache<CachableQuery, QueryProvider> queryCache = new ConcurrentLruCache<>(16,
AbstractStringBasedJpaQuery.this::applySorting);
private volatile @Nullable String cachedQueryString;
private volatile @Nullable QueryProvider cachedQuery;
@Override
public String getSorted(StringQuery 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));
@@ -366,12 +387,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
*/
static class CachableQuery {
private final StringQuery query;
private final EntityQuery query;
private final String queryString;
private final Sort sort;
private final ReturnedType returnedType;
CachableQuery(StringQuery query, Sort sort, ReturnedType returnedType) {
CachableQuery(EntityQuery query, Sort sort, ReturnedType returnedType) {
this.query = query;
this.queryString = query.getQueryString();
@@ -379,7 +400,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
this.returnedType = returnedType;
}
StringQuery getDeclaredQuery() {
EntityQuery getDeclaredQuery() {
return query;
}

View File

@@ -1,67 +0,0 @@
/*
* 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.Collections;
import java.util.List;
/**
* @author Christoph Strobl
*/
final class BindableQuery implements DeclaredQuery {
private final DeclaredQuery source;
private final String bindableQueryString;
private final List<ParameterBinding> bindings;
private final boolean usesJdbcStyleParameters;
public BindableQuery(DeclaredQuery source, String bindableQueryString, List<ParameterBinding> bindings, boolean usesJdbcStyleParameters) {
this.source = source;
this.bindableQueryString = bindableQueryString;
this.bindings = bindings;
this.usesJdbcStyleParameters = usesJdbcStyleParameters;
}
@Override
public boolean isNativeQuery() {
return source.isNativeQuery();
}
boolean hasBindings() {
return !bindings.isEmpty();
}
boolean usesJdbcStyleParameters() {
return usesJdbcStyleParameters;
}
@Override
public String getQueryString() {
return bindableQueryString;
}
public BindableQuery unifyBindings(BindableQuery comparisonQuery) {
if (comparisonQuery.hasBindings() && !comparisonQuery.bindings.equals(this.bindings)) {
return new BindableQuery(source, bindableQueryString, comparisonQuery.bindings, usesJdbcStyleParameters);
}
return this;
}
public List<ParameterBinding> getBindings() {
return Collections.unmodifiableList(bindings);
}
}

View File

@@ -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 + "]";
}
}
}

View File

@@ -17,13 +17,17 @@ package org.springframework.data.jpa.repository.query;
/**
* Interface defining the contract to represent a declared query.
* <p>
* 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
*/
public interface DeclaredQuery extends StructuredQuery {
public interface DeclaredQuery extends QueryProvider {
/**
* Creates a DeclaredQuery for a JPQL query.
@@ -32,7 +36,7 @@ public interface DeclaredQuery extends StructuredQuery {
* @return new instance of {@link DeclaredQuery}.
*/
static DeclaredQuery jpqlQuery(String jpql) {
return new JpqlQuery(jpql);
return new DeclaredQueries.JpqlQuery(jpql);
}
/**
@@ -42,13 +46,40 @@ public interface DeclaredQuery extends StructuredQuery {
* @return new instance of {@link DeclaredQuery}.
*/
static DeclaredQuery nativeQuery(String sql) {
return new NativeQuery(sql);
return new DeclaredQueries.NativeQuery(sql);
}
/**
* Return whether the query is a native query of not.
*
* @return <code>true</code> if native query otherwise <code>false</code>
* @return {@literal true} if native query; {@literal false} if it is a JPQL query.
*/
boolean isNativeQuery();
boolean isNative();
/**
* Return whether the query is a JPQL query of not.
*
* @return {@literal true} if JPQL query; {@literal false} if it is a native query.
* @since 4.0
*/
default boolean isJpql() {
return !isNative();
}
/**
* Rewrite a query string using a new query string retaining its source and {@link #isNative() native} flag.
*
* @param newQueryString the new query string.
* @return the rewritten {@link DeclaredQuery}.
* @since 4.0
*/
default DeclaredQuery rewrite(String newQueryString) {
if (getQueryString().equals(newQueryString)) {
return this;
}
return new DeclaredQueries.RewrittenQuery(this, newQueryString);
}
}

View File

@@ -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.
* <p>
* 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<ParameterBinding> 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<ParameterBinding> getParameterBindings() {
return query.getBindings();
}
}
}

View File

@@ -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 @@ import org.jspecify.annotations.Nullable;
* @author Diego Krupitza
* @since 2.7.0
*/
public class DefaultQueryEnhancer implements QueryEnhancer {
class DefaultQueryEnhancer implements QueryEnhancer {
private final StructuredQuery query;
private final QueryProvider query;
private final boolean hasConstructorExpression;
private final @Nullable String alias;
private final String projection;
private final Set<String> joinAliases;
public DefaultQueryEnhancer(StructuredQuery 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
@@ -61,7 +45,7 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
@Override
public String createCountQueryFor(@Nullable String countProjection) {
boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNativeQuery() : true;
boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNative() : true;
return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, nativeQuery);
}
@@ -81,12 +65,8 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
}
@Override
public Set<String> getJoinAliases() {
return this.joinAliases;
}
@Override
public StructuredQuery getQuery() {
public QueryProvider getQuery() {
return this.query;
}
}

View File

@@ -18,21 +18,30 @@ package org.springframework.data.jpa.repository.query;
import java.util.Collections;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.jspecify.annotations.Nullable;
/**
* NULL-Object pattern implementation for {@link IntrospectedQuery}.
* NULL-Object pattern implementation for {@link ParametrizedQuery}.
*
* @author Jens Schauder
* @author Mark Paluch
* @since 2.0.3
*/
class EmptyIntrospectedQuery implements EntityQuery {
enum EmptyIntrospectedQuery implements EntityQuery {
/**
* An implementation implementing the NULL-Object pattern for situations where there is no query.
*/
static final EntityQuery EMPTY_QUERY = new EmptyIntrospectedQuery();
INSTANCE;
EmptyIntrospectedQuery() {}
@Override
public boolean hasParameterBindings() {
return false;
}
@Override
public boolean usesJdbcStyleParameters() {
return false;
}
@Override
public boolean hasNamedParameter() {
@@ -40,19 +49,14 @@ class EmptyIntrospectedQuery implements EntityQuery {
}
@Override
public String getQueryString() {
return "";
public List<ParameterBinding> getParameterBindings() {
return Collections.emptyList();
}
public @Nullable String getAlias() {
return null;
}
@Override
public boolean isNativeQuery() {
return false;
}
@Override
public boolean hasConstructorExpression() {
return false;
@@ -69,27 +73,18 @@ class EmptyIntrospectedQuery implements EntityQuery {
}
@Override
public List<ParameterBinding> getParameterBindings() {
return Collections.emptyList();
public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) {
return INSTANCE;
}
@Override
public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) {
return EMPTY_QUERY;
public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) {
return this;
}
@Override
public String applySorting(Sort sort) {
return "";
public String toString() {
return "<EMPTY>";
}
@Override
public boolean usesJdbcStyleParameters() {
return false;
}
@Override
public DeclaredQuery getDeclaredQuery() {
return DeclaredQuery.nativeQuery("");
}
}

View File

@@ -15,61 +15,34 @@
*/
package org.springframework.data.jpa.repository.query;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.jspecify.annotations.Nullable;
/**
* A wrapper for a String representation of a query offering information about the query.
* 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.
* <p>
* 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 2.0.3
* @since 4.0
*/
interface EntityQuery extends IntrospectedQuery {
interface EntityQuery extends ParametrizedQuery {
/**
* Creates a DeclaredQuery for a JPQL query.
* Create a new {@link EntityQuery} given {@link DeclaredQuery} and {@link QueryEnhancerSelector}.
*
* @param query the JPQL query string.
* @return
* @param query must not be {@literal null}.
* @param selector must not be {@literal null}.
* @return a new {@link EntityQuery}.
*/
static EntityQuery introspectJpql(String query, QueryEnhancerFactory queryEnhancer) {
return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY
: new StringQuery(query, false, queryEnhancer, parameterBindings -> {});
}
static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) {
/**
* Creates a DeclaredQuery for a JPQL query.
*
* @param query the JPQL query string.
* @return
*/
static EntityQuery introspectJpql(String query, QueryEnhancerSelector selector) {
return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY
: new StringQuery(query, false, selector, parameterBindings -> {});
}
PreprocessedQuery preparsed = PreprocessedQuery.parse(query);
QueryEnhancerFactory enhancerFactory = selector.select(preparsed);
/**
* Creates a DeclaredQuery for a native query.
*
* @param query the native query string.
* @return
*/
static EntityQuery introspectNativeQuery(String query, QueryEnhancerFactory queryEnhancer) {
return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY
: new StringQuery(query, true, queryEnhancer, parameterBindings -> {});
}
/**
* Creates a DeclaredQuery for a native query.
*
* @param query the native query string.
* @return
*/
static EntityQuery introspectNativeQuery(String query, QueryEnhancerSelector selector) {
return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY
: new StringQuery(query, true, selector, parameterBindings -> {});
return new DefaultEntityQuery(preparsed, enhancerFactory);
}
/**
@@ -84,18 +57,6 @@ interface EntityQuery extends IntrospectedQuery {
*/
boolean isDefaultProjection();
/**
* 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.
*/
IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection);
String applySorting(Sort sort);
/**
* @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
* @since 2.0.6
@@ -104,4 +65,24 @@ interface EntityQuery extends IntrospectedQuery {
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);
}

View File

@@ -74,7 +74,7 @@ import org.springframework.util.StringUtils;
*/
public class JSqlParserQueryEnhancer implements QueryEnhancer {
private final StructuredQuery query;
private final QueryProvider query;
private final Statement statement;
private final ParsedType parsedType;
private final boolean hasConstructorExpression;
@@ -87,7 +87,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
/**
* @param query the query we want to enhance. Must not be {@literal null}.
*/
public JSqlParserQueryEnhancer(StructuredQuery query) {
public JSqlParserQueryEnhancer(QueryProvider query) {
this.query = query;
this.statement = parseStatement(query.getQueryString(), Statement.class);
@@ -329,35 +329,20 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
return this.projection;
}
@Override
public Set<String> getJoinAliases() {
return joinAliases;
}
public Set<String> getSelectionAliases() {
return selectAliases;
}
@Override
public StructuredQuery 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");
@@ -410,8 +395,8 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer {
this.query::getQueryString);
}
private static String createCountQueryFor(StructuredQuery query, PlainSelect selectBody,
@Nullable String countProjection, @Nullable String primaryAlias) {
private static String createCountQueryFor(PlainSelect selectBody, @Nullable String countProjection,
@Nullable String primaryAlias) {
// remove order by
selectBody.setOrderByElements(null);

View File

@@ -54,4 +54,5 @@ public class JpaQueryConfiguration {
public ValueExpressionDelegate getValueExpressionDelegate() {
return valueExpressionDelegate;
}
}

View File

@@ -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.jspecify.annotations.Nullable;
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<Q extends QueryInformation> implements QueryEnhancer {
private final Q queryInformation;
private final String projection;
private final SortedQueryRewriteFunction<Q> sortFunction;
private final BiFunction<String, Q, ParseTreeVisitor<QueryTokenStream>> countQueryFunction;
private final BiFunction<@Nullable String, Q, ParseTreeVisitor<QueryTokenStream>> countQueryFunction;
JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector<Q> introspector,
SortedQueryRewriteFunction<Q> sortFunction,
BiFunction<String, Q, ParseTreeVisitor<QueryTokenStream>> countQueryFunction) {
BiFunction<@Nullable String, Q, ParseTreeVisitor<QueryTokenStream>> countQueryFunction) {
this.context = context;
this.sortFunction = sortFunction;
@@ -142,7 +140,7 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
}
/**
* Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} 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.
@@ -152,7 +150,7 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
}
/**
* Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} 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.
@@ -162,7 +160,7 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
}
/**
* Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} 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.
@@ -197,8 +195,7 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
}
/**
* 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() {
@@ -206,24 +203,13 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
}
/**
* 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<String> getJoinAliases() {
return Set.of();
}
/**
* Look up the {@link DeclaredQuery} from the query parser.
*/
@@ -232,17 +218,6 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
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(
@@ -250,28 +225,6 @@ class JpaQueryEnhancer<Q extends QueryInformation> implements QueryEnhancer {
.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.
*

View File

@@ -32,7 +32,6 @@ 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;
@@ -151,20 +150,22 @@ public final class JpaQueryLookupStrategy {
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 createStringQuery(method, em, method.getRequiredAnnotatedQuery(),
return createStringQuery(method, em, method.getRequiredDeclaredQuery(),
getCountQuery(method, namedQueries, em), configuration);
}
String name = method.getNamedQueryName();
if (namedQueries.hasQuery(name)) {
return createStringQuery(method, em, namedQueries.getQuery(name), getCountQuery(method, namedQueries, em),
return createStringQuery(method, em, method.getDeclaredQuery(namedQueries.getQuery(name)),
getCountQuery(method, namedQueries, em),
configuration);
}
@@ -173,7 +174,15 @@ public final class JpaQueryLookupStrategy {
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();
@@ -203,20 +212,20 @@ public final class JpaQueryLookupStrategy {
*
* @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 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, String queryString,
@Nullable String countQueryString, JpaQueryConfiguration configuration) {
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, queryString, countQueryString, configuration)
: new SimpleJpaQuery(method, em, queryString, countQueryString, configuration);
return method.isNativeQuery() ? new NativeJpaQuery(method, em, query, countQuery, configuration)
: new SimpleJpaQuery(method, em, query, countQuery, configuration);
}
/**

View File

@@ -27,9 +27,9 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
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;
@@ -295,6 +295,13 @@ public class JpaQueryMethod extends QueryMethod {
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.
@@ -333,6 +340,25 @@ public class JpaQueryMethod extends QueryMethod {
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.
@@ -345,6 +371,19 @@ public class JpaQueryMethod extends QueryMethod {
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.
@@ -368,6 +407,17 @@ public class JpaQueryMethod extends QueryMethod {
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() {

View File

@@ -80,7 +80,7 @@ final class NamedQuery extends AbstractJpaQuery {
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();
@@ -94,14 +94,17 @@ final class NamedQuery extends AbstractJpaQuery {
method, method.isNativeQuery() ? "NativeQuery" : "Query"));
}
String queryString = extractor.extractQueryString(query);
String queryString = extractor.extractQueryString(namedQuery);
// TODO: What is queryString is null?
if (method.isNativeQuery() || (query != null && query.toString().contains("NativeQuery"))) {
this.entityQuery = Lazy.of(() -> EntityQuery.introspectNativeQuery(queryString, selector));
DeclaredQuery declaredQuery;
if (method.isNativeQuery() || (namedQuery != null && namedQuery.toString().contains("NativeQuery"))) {
declaredQuery = DeclaredQuery.nativeQuery(queryString);
} else {
this.entityQuery = Lazy.of(() -> EntityQuery.introspectJpql(queryString, selector));
declaredQuery = DeclaredQuery.jpqlQuery(queryString);
}
this.entityQuery = Lazy.of(() -> EntityQuery.create(declaredQuery, selector));
}
/**

View File

@@ -19,16 +19,15 @@ import jakarta.persistence.EntityManager;
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.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.ObjectUtils;
/**
@@ -57,23 +56,45 @@ class NativeJpaQuery extends AbstractStringBasedJpaQuery {
* @param countQueryString must not be {@literal null} or empty.
* @param queryConfiguration must not be {@literal null}.
*/
public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString,
JpaQueryConfiguration queryConfiguration) {
super(method, em, queryString, countQueryString, queryConfiguration);
MergedAnnotations annotations = MergedAnnotations.from(method.getMethod());
MergedAnnotation<NativeQuery> annotation = annotations.get(NativeQuery.class);
this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null;
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<NativeQuery> 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);

View File

@@ -78,13 +78,13 @@ class ParameterBinderFactory {
* query in order to ensure that all query parameters are bound.
*
* @param parameters method parameters that are available for binding, must not be {@literal null}.
* @param query the {@link StringQuery} the binders shall be created for, must not be {@literal null}.
* @param query the {@link DefaultEntityQuery} the binders shall be created for, must not be {@literal null}.
* @param parser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
* @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a
* {@link jakarta.persistence.Query} while processing SpEL expressions where applicable.
*/
static ParameterBinder createQueryAwareBinder(JpaParameters parameters, IntrospectedQuery query,
static ParameterBinder createQueryAwareBinder(JpaParameters parameters, ParametrizedQuery query,
ValueExpressionParser parser, ValueEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(parameters, "JpaParameters must not be null");
@@ -126,11 +126,11 @@ class ParameterBinderFactory {
private static Iterable<QueryParameterSetter> createSetters(List<ParameterBinding> parameterBindings,
QueryParameterSetterFactory... factories) {
return createSetters(parameterBindings, EmptyIntrospectedQuery.EMPTY_QUERY, factories);
return createSetters(parameterBindings, EmptyIntrospectedQuery.INSTANCE, factories);
}
private static Iterable<QueryParameterSetter> createSetters(List<ParameterBinding> parameterBindings,
IntrospectedQuery query, QueryParameterSetterFactory... strategies) {
ParametrizedQuery query, QueryParameterSetterFactory... strategies) {
List<QueryParameterSetter> setters = new ArrayList<>(parameterBindings.size());
for (ParameterBinding parameterBinding : parameterBindings) {
@@ -141,7 +141,7 @@ class ParameterBinderFactory {
}
private static QueryParameterSetter createQueryParameterSetter(ParameterBinding binding,
QueryParameterSetterFactory[] strategies, IntrospectedQuery query) {
QueryParameterSetterFactory[] strategies, ParametrizedQuery query) {
for (QueryParameterSetterFactory strategy : strategies) {

View File

@@ -18,34 +18,24 @@ package org.springframework.data.jpa.repository.query;
import java.util.List;
/**
* A wrapper for a String representation of a query offering information about the query.
* A parsed and structured representation of a query providing introspection details about parameter bindings.
* <p>
* 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 2.0.3
* @since 4.0
* @see EntityQuery
* @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector)
* @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration)
*/
interface IntrospectedQuery extends StructuredQuery {
DeclaredQuery getDeclaredQuery();
default String getQueryString() {
return getDeclaredQuery().getQueryString();
}
interface ParametrizedQuery extends QueryProvider {
/**
* @return whether the underlying query has at least one named parameter.
* @return whether the underlying query has at least one parameter.
*/
boolean hasNamedParameter();
/**
* 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.
*/
List<ParameterBinding> getParameterBindings();
boolean hasParameterBindings();
/**
* Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
@@ -56,4 +46,14 @@ interface IntrospectedQuery extends StructuredQuery {
*/
boolean usesJdbcStyleParameters();
/**
* @return whether the underlying query has at least one named parameter.
*/
boolean hasNamedParameter();
/**
* @return the registered {@link ParameterBinding}s.
*/
List<ParameterBinding> getParameterBindings();
}

View File

@@ -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 static java.util.regex.Pattern.*;
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,18 +30,12 @@ import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.domain.Sort;
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;
@@ -48,190 +43,44 @@ import org.springframework.util.ObjectUtils;
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.
* <p>
* 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).
* <p>
* 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 EntityQuery {
final class PreprocessedQuery implements DeclaredQuery {
private final BindableQuery bindableQuery;
private final DeclaredQuery source;
private final List<ParameterBinding> bindings;
private final boolean usesJdbcStyleParameters;
private final boolean containsPageableInSpel;
private final QueryEnhancerFactory queryEnhancerFactory;
private final QueryEnhancer queryEnhancer;
private final boolean hasNamedParameters;
private final boolean hasNamedBindings;
/**
* Creates a new {@link StringQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
StringQuery(String query, boolean isNative) {
this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR, it -> {});
}
/**
* Creates a new {@link StringQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
StringQuery(String query, boolean isNative, QueryEnhancerFactory factory,Consumer<List<ParameterBinding>> parameterPostProcessor) {
Assert.hasText(query, "Query must not be null or empty");
this.containsPageableInSpel = query.contains("#pageable");
this.queryEnhancerFactory = factory;
DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source);
parameterPostProcessor.accept(this.bindableQuery.getBindings());
this.queryEnhancer = factory.create(this.bindableQuery);
this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings());
}
/**
* Creates a new {@link StringQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
StringQuery(String query, boolean isNative, QueryEnhancerSelector selector, Consumer<List<ParameterBinding>> parameterPostProcessor) {
Assert.hasText(query, "Query must not be null or empty");
this.containsPageableInSpel = query.contains("#pageable");
DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source);
this.queryEnhancerFactory = selector.select(source);
this.queryEnhancer = queryEnhancerFactory.create(this.bindableQuery);
parameterPostProcessor.accept(this.bindableQuery.getBindings());
this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings());
}
/**
* internal copy constructor
*
* @param bindableQuery
* @param factory
* @param enhancer
* @param hasNamedParameters
* @param containsPageableInSpel
*/
private StringQuery(BindableQuery bindableQuery, QueryEnhancerFactory factory, QueryEnhancer enhancer, boolean hasNamedParameters, boolean containsPageableInSpel) {
this.bindableQuery = bindableQuery;
this.queryEnhancerFactory = factory;
this.queryEnhancer = enhancer;
this.hasNamedParameters = hasNamedParameters;
private PreprocessedQuery(DeclaredQuery query, List<ParameterBinding> bindings, boolean usesJdbcStyleParameters,
boolean containsPageableInSpel) {
this.source = query;
this.bindings = bindings;
this.usesJdbcStyleParameters = usesJdbcStyleParameters;
this.containsPageableInSpel = containsPageableInSpel;
}
QueryEnhancer getQueryEnhancer() {
return queryEnhancer;
}
/**
* Returns whether we have found some like bindings.
*/
boolean hasParameterBindings() {
return this.bindableQuery.hasBindings();
}
String getProjection() {
return this.queryEnhancer.getProjection();
}
@Override
public String getQueryString() {
return bindableQuery.getQueryString();
}
@Override
public List<ParameterBinding> getParameterBindings() {
return this.bindableQuery.getBindings();
}
@Override
public IntrospectedQuery 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.bindableQuery.isNativeQuery(), queryEnhancerFactory, 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 : getParameterBindings()) {
Predicate<ParameterBinding> identifier = binding::bindsTo;
Predicate<ParameterBinding> notCompatible = Predicate.not(binding::isCompatibleWith);
// replace incompatible bindings
if ( derivedBindings.removeIf(
it -> identifier.test(it) && notCompatible.test(it))) {
derivedBindings.add(binding);
}
}
}
});
}
@Override
public String applySorting(Sort sort) {
return queryEnhancer.applySorting(sort);
}
@Override
public boolean usesJdbcStyleParameters() {
return bindableQuery.usesJdbcStyleParameters();
}
public @Nullable String getAlias() {
return queryEnhancer.detectAlias();
}
@Override
public boolean hasConstructorExpression() {
return queryEnhancer.hasConstructorExpression();
}
@Override
public boolean isDefaultProjection() {
return getProjection().equalsIgnoreCase(getAlias());
}
@Override
public boolean hasNamedParameter() {
return hasNamedParameters;
}
@Override
public boolean usesPaging() {
return containsPageableInSpel;
}
@Override
public DeclaredQuery getDeclaredQuery() {
return bindableQuery;
this.hasNamedBindings = containsNamedParameter(bindings);
}
private static boolean containsNamedParameter(List<ParameterBinding> bindings) {
for (ParameterBinding parameterBinding : bindings) {
if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) {
if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin()
.isMethodArgument()) {
return true;
}
}
@@ -239,77 +88,81 @@ class StringQuery implements EntityQuery {
}
/**
* Value object to track and allocate used parameter index labels in a query.
* 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}.
*/
static class IndexedParameterLabels {
public static PreprocessedQuery parse(DeclaredQuery declaredQuery) {
return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite,
parameterBindings -> {
});
}
private final TreeSet<Integer> usedLabels;
private final boolean sequential;
@Override
public String getQueryString() {
return source.getQueryString();
}
public IndexedParameterLabels(Set<Integer> usedLabels) {
@Override
public boolean isNative() {
return source.isNative();
}
this.usedLabels = usedLabels instanceof TreeSet<Integer> ts ? ts : new TreeSet<Integer>(usedLabels);
this.sequential = isSequential(usedLabels);
}
boolean hasBindings() {
return !bindings.isEmpty();
}
private static boolean isSequential(Set<Integer> usedLabels) {
boolean hasNamedBindings() {
return this.hasNamedBindings;
}
for (int i = 0; i < usedLabels.size(); i++) {
boolean containsPageableInSpel() {
return containsPageableInSpel;
}
if (usedLabels.contains(i + 1)) {
continue;
boolean usesJdbcStyleParameters() {
return usesJdbcStyleParameters;
}
List<ParameterBinding> getBindings() {
return Collections.unmodifiableList(bindings);
}
/**
* 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
*/
@Override
public PreprocessedQuery rewrite(String newQueryString) {
return ParameterBindingParser.INSTANCE.parse(newQueryString, source::rewrite, 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.hasBindings() && !this.bindings.equals(derivedBindings)) {
for (ParameterBinding binding : bindings) {
Predicate<ParameterBinding> identifier = binding::bindsTo;
Predicate<ParameterBinding> notCompatible = Predicate.not(binding::isCompatibleWith);
// replace incompatible bindings
if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) {
derivedBindings.add(binding);
}
}
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();
}
@Override
public String toString() {
return "ParametrizedQuery[" + source + ", " + bindings + ']';
}
/**
@@ -333,7 +186,7 @@ class StringQuery implements EntityQuery {
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;
@@ -371,19 +224,24 @@ class StringQuery implements EntityQuery {
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
* the cleaned up query.
*/
BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(DeclaredQuery query) {
PreprocessedQuery parse(String query, Function<String, DeclaredQuery> declaredQueryFactory,
Consumer<List<ParameterBinding>> parameterBindingPostProcessor) {
IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query));
boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels();
List<ParameterBinding> bindings = new ArrayList<>();
boolean jdbcStyle = false;
boolean containsPageableInSpel = query.contains("#pageable");
/*
* Prefer indexed access over named parameters if only SpEL Expression parameters are present.
*/
if (!parametersShouldBeAccessedByIndex && query.getQueryString().contains("?#{")) {
if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) {
parametersShouldBeAccessedByIndex = true;
}
ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query.getQueryString(),
ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query,
parametersShouldBeAccessedByIndex, parameterLabels);
String resultingQuery = parsedQuery.getQueryString();
@@ -409,7 +267,8 @@ class StringQuery implements EntityQuery {
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;
}
@@ -429,56 +288,64 @@ class StringQuery implements EntityQuery {
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);
ParameterBinding.ParameterOrigin origin = ObjectUtils.isEmpty(expression)
? ParameterBinding.ParameterOrigin.ofParameter(parameterName, parameterIndex)
: ParameterBinding.ParameterOrigin.ofExpression(expression);
BindingIdentifier targetBinding = queryParameter;
Function<BindingIdentifier, ParameterBinding> bindingFactory = switch (ParameterBindingType.of(typeSource)) {
ParameterBinding.BindingIdentifier targetBinding = queryParameter;
Function<ParameterBinding.BindingIdentifier, ParameterBinding> bindingFactory = switch (ParameterBindingType
.of(typeSource)) {
case LIKE -> {
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
yield (identifier) -> new LikeParameterBinding(identifier, origin, likeType);
Part.Type likeType = ParameterBinding.LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
yield (identifier) -> new ParameterBinding.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.
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 && jdbcStyle) ? "?"
: "?" + 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 new BindableQuery(query, resultingQuery, bindings, jdbcStyle);
parameterBindingPostProcessor.accept(bindings);
return new PreprocessedQuery(declaredQueryFactory.apply(resultingQuery), bindings, jdbcStyle,
containsPageableInSpel);
}
private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel,
@@ -586,18 +453,17 @@ class StringQuery implements EntityQuery {
}
}
/**
* 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<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
private final MultiValueMap<ParameterBinding.BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
private final Consumer<ParameterBinding> registration;
@@ -611,21 +477,22 @@ class StringQuery implements EntityQuery {
}
/**
* Return whether the identifier is already bound.
*
* @param identifier
* @return
* @return whether the identifier is already bound.
*/
public boolean isBound(BindingIdentifier identifier) {
public boolean isBound(ParameterBinding.BindingIdentifier identifier) {
return !getBindings(identifier).isEmpty();
}
BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
Function<BindingIdentifier, ParameterBinding> bindingFactory, IndexedParameterLabels parameterLabels) {
ParameterBinding.BindingIdentifier register(ParameterBinding.BindingIdentifier identifier,
ParameterBinding.ParameterOrigin origin,
Function<ParameterBinding.BindingIdentifier, ParameterBinding> 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<ParameterBinding> bindingsForOrigin = getBindings(methodArgument);
if (!isBound(identifier)) {
@@ -645,7 +512,7 @@ class StringQuery implements EntityQuery {
}
}
BindingIdentifier syntheticIdentifier;
ParameterBinding.BindingIdentifier syntheticIdentifier;
if (identifier.hasName() && methodArgument.hasName()) {
int index = 0;
@@ -654,9 +521,10 @@ class StringQuery implements EntityQuery {
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);
@@ -666,11 +534,12 @@ class StringQuery implements EntityQuery {
}
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<ParameterBinding> getBindings(BindingIdentifier identifier) {
private List<ParameterBinding> getBindings(ParameterBinding.BindingIdentifier identifier) {
return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>());
}
@@ -678,4 +547,79 @@ class StringQuery implements EntityQuery {
registration.accept(parameterBinding);
}
}
/**
* Value object to track and allocate used parameter index labels in a query.
*/
static class IndexedParameterLabels {
private final TreeSet<Integer> usedLabels;
private final boolean sequential;
public IndexedParameterLabels(Set<Integer> usedLabels) {
this.usedLabels = usedLabels instanceof TreeSet<Integer> ts ? ts : new TreeSet<Integer>(usedLabels);
this.sequential = isSequential(usedLabels);
}
private static boolean isSequential(Set<Integer> 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();
}
}
}

View File

@@ -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 @@ import org.springframework.data.repository.query.ReturnedType;
*
* @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,60 +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<String> getJoinAliases();
/**
* Gets the query we want to use for enhancements.
*
* @return non-null {@link DeclaredQuery} that wraps the query.
*/
StructuredQuery 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 <code>countProjection</code>.
* 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}.
@@ -116,7 +91,7 @@ public interface QueryEnhancer {
/**
* Interface to describe the information needed to rewrite a query.
*
* @since 3.5
* @since 4.0
*/
interface QueryRewriteInformation {
@@ -129,6 +104,7 @@ public interface QueryEnhancer {
* @return type expected to be returned by the query.
*/
ReturnedType getReturnedType();
}
}

View File

@@ -25,10 +25,11 @@ import org.springframework.util.ClassUtils;
* Pre-defined QueryEnhancerFactories to be used for query enhancement.
*
* @author Mark Paluch
* @since 4.0
*/
public class QueryEnhancerFactories {
private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class);
private static final Log LOG = LogFactory.getLog(QueryEnhancerFactories.class);
static final boolean jSqlParserPresent = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser",
QueryEnhancerFactory.class.getClassLoader());
@@ -57,7 +58,7 @@ public class QueryEnhancerFactories {
}
@Override
public QueryEnhancer create(StructuredQuery query) {
public QueryEnhancer create(QueryProvider query) {
return new DefaultQueryEnhancer(query);
}
},
@@ -65,11 +66,12 @@ public class QueryEnhancerFactories {
JSQLPARSER {
@Override
public boolean supports(DeclaredQuery query) {
return query.isNativeQuery();
return query.isNative();
}
@Override
public QueryEnhancer create(StructuredQuery query) {
public QueryEnhancer create(QueryProvider query) {
if (jSqlParserPresent) {
return new JSqlParserQueryEnhancer(query);
}
@@ -81,33 +83,33 @@ public class QueryEnhancerFactories {
HQL {
@Override
public boolean supports(DeclaredQuery query) {
return !query.isNativeQuery();
return query.isJpql();
}
@Override
public QueryEnhancer create(StructuredQuery query) {
public QueryEnhancer create(QueryProvider query) {
return JpaQueryEnhancer.forHql(query.getQueryString());
}
},
EQL {
@Override
public boolean supports(DeclaredQuery query) {
return !query.isNativeQuery();
return query.isJpql();
}
@Override
public QueryEnhancer create(StructuredQuery query) {
public QueryEnhancer create(QueryProvider query) {
return JpaQueryEnhancer.forEql(query.getQueryString());
}
},
JPQL {
@Override
public boolean supports(DeclaredQuery query) {
return !query.isNativeQuery();
return query.isJpql();
}
@Override
public QueryEnhancer create(StructuredQuery query) {
public QueryEnhancer create(QueryProvider query) {
return JpaQueryEnhancer.forJpql(query.getQueryString());
}
}
@@ -165,4 +167,5 @@ public class QueryEnhancerFactories {
public static QueryEnhancerFactory jpql() {
return BuiltinQueryEnhancerFactories.JPQL;
}
}

View File

@@ -16,13 +16,13 @@
package org.springframework.data.jpa.repository.query;
/**
* Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link IntrospectedQuery}.
* 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
* @since 4.0
*/
public interface QueryEnhancerFactory {
@@ -38,9 +38,9 @@ public interface QueryEnhancerFactory {
* Creates a new {@link QueryEnhancer} for the given query.
*
* @param query the query to be enhanced and introspected.
* @return
* @return the query enhancer to be used.
*/
QueryEnhancer create(StructuredQuery query);
QueryEnhancer create(QueryProvider query);
/**
* Creates a new {@link QueryEnhancerFactory} for the given {@link DeclaredQuery}.

View File

@@ -21,9 +21,10 @@ import org.springframework.data.jpa.provider.PersistenceProvider;
* Interface declaring a strategy to select a {@link QueryEnhancer} for a given {@link DeclaredQuery query}.
* <p>
* 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 transformations.
* so that query methods can derive count queries, apply sorting and perform other rewrite transformations.
*
* @author Mark Paluch
* @since 4.0
*/
public interface QueryEnhancerSelector {
@@ -90,4 +91,5 @@ public interface QueryEnhancerSelector {
}
}
}

View File

@@ -20,9 +20,9 @@ import jakarta.persistence.TemporalType;
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, IntrospectedQuery introspectedQuery);
abstract @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery);
/**
* Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}.
@@ -180,7 +180,7 @@ abstract class QueryParameterSetterFactory {
}
@Override
public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) {
public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery parametrizedQuery) {
if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) {
return null;
@@ -212,7 +212,7 @@ abstract class QueryParameterSetterFactory {
private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory {
@Override
public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) {
public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) {
if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) {
return null;
@@ -248,7 +248,7 @@ abstract class QueryParameterSetterFactory {
}
@Override
public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) {
public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) {
Assert.notNull(binding, "Binding must not be null");
@@ -294,7 +294,7 @@ abstract class QueryParameterSetterFactory {
}
@Override
public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) {
public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) {
if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) {

View File

@@ -16,23 +16,22 @@
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)
*/
final class NativeQuery implements DeclaredQuery {
public interface QueryProvider {
private final String sql;
/**
* Return the query string.
*
* @return the query string.
*/
String getQueryString();
NativeQuery(String sql) {
this.sql = sql;
}
@Override
public boolean isNativeQuery() {
return true;
}
@Override
public String getQueryString() {
return sql;
}
}

View File

@@ -445,10 +445,8 @@ public abstract class QueryUtils {
*
* @param query must not be {@literal null}.
* @return Might return {@literal null}.
* @deprecated use {@link IntrospectedQuery#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 abstract class QueryUtils {
*
* @param originalQuery must not be {@literal null} or empty.
* @return Guaranteed to be not {@literal null}.
* @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead.
*/
@Deprecated
public static String createCountQueryFor(String originalQuery) {
static String createCountQueryFor(String originalQuery) {
return createCountQueryFor(originalQuery, null);
}
@@ -568,10 +564,8 @@ public abstract class QueryUtils {
* @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 IntrospectedQuery#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);
}

View File

@@ -18,11 +18,9 @@ package org.springframework.data.jpa.repository.query;
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}
@@ -41,14 +39,14 @@ class SimpleJpaQuery extends AbstractStringBasedJpaQuery {
*
* @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 can be {@literal null} if not defined.
* @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,
JpaQueryConfiguration queryConfiguration) {
public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery query,
@Nullable DeclaredQuery countQuery, JpaQueryConfiguration queryConfiguration) {
super(method, em, queryString, countQueryString, queryConfiguration);
super(method, em, query, countQuery, queryConfiguration);
validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", method);

View File

@@ -1,24 +0,0 @@
/*
* 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;
/**
* @author Christoph Strobl
*/
public interface StructuredQuery {
String getQueryString();
}

View File

@@ -23,12 +23,11 @@ import org.springframework.core.env.StandardEnvironment;
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.
* <p>
* Currently, the following template variables are available:
* <ol>
@@ -42,7 +41,7 @@ import org.springframework.util.Assert;
* @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,18 +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 selector must not be {@literal null}.
* @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}.
*/
ExpressionBasedStringQuery(String query, JpaEntityMetadata<?> metadata, ValueExpressionParser parser,
boolean nativeQuery, QueryEnhancerSelector selector) {
super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query),
selector, parameterBindings -> {});
public static EntityQuery create(String queryString, JpaQueryMethod queryMethod, JpaQueryConfiguration queryContext) {
return create(queryMethod.getDeclaredQuery(queryString), queryMethod.getEntityInformation(), queryContext);
}
/**
* Create a {@link DefaultEntityQuery} given {@link DeclaredQuery query}, {@link JpaEntityMetadata} and
* {@link JpaQueryConfiguration}.
*
* @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}.
*/
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());
}
/**
@@ -80,7 +96,7 @@ class ExpressionBasedStringQuery extends StringQuery {
* @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");
@@ -91,15 +107,14 @@ class ExpressionBasedStringQuery extends StringQuery {
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;
@@ -120,10 +135,4 @@ class ExpressionBasedStringQuery extends StringQuery {
return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
}
public static StringQuery create(String query, JpaQueryMethod method, JpaQueryConfiguration queryContext) {
return new ExpressionBasedStringQuery(query, method.getEntityInformation(),
queryContext.getValueExpressionDelegate().getValueExpressionParser(),
method.isNativeQuery(), queryContext.getSelector());
}
}

View File

@@ -68,9 +68,10 @@ class AbstractStringBasedJpaQueryIntegrationTests {
when(mock.getMetamodel()).thenReturn(em.getMetamodel());
JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class);
AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, method.getAnnotatedQuery(), null, CONFIG);
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());

View File

@@ -150,7 +150,7 @@ class AbstractStringBasedJpaQueryUnitTests {
}
@Override
protected String applySorting(CachableQuery query) {
protected QueryProvider applySorting(CachableQuery query) {
captureInvocation("applySorting", query);
@@ -158,12 +158,13 @@ class AbstractStringBasedJpaQueryUnitTests {
}
@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;
}

View File

@@ -28,11 +28,10 @@ import org.springframework.data.jpa.repository.query.ParameterBinding.InParamete
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.jpa.repository.query.StringQuery.ParameterBindingParser;
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
@@ -44,13 +43,13 @@ import org.springframework.data.repository.query.parser.Part.Type;
* @author Aleksei Elin
* @author Gunha Hwang
*/
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);
@@ -67,8 +66,8 @@ class StringQueryUnitTests {
@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())
@@ -91,7 +90,7 @@ class StringQueryUnitTests {
@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();
@@ -117,7 +116,8 @@ class StringQueryUnitTests {
@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");
@@ -134,7 +134,7 @@ class StringQueryUnitTests {
@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);
@@ -165,7 +165,7 @@ class StringQueryUnitTests {
@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);
@@ -198,7 +198,7 @@ class StringQueryUnitTests {
@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);
@@ -225,7 +225,7 @@ class StringQueryUnitTests {
@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();
@@ -239,7 +239,7 @@ class StringQueryUnitTests {
@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);
@@ -247,7 +247,8 @@ class StringQueryUnitTests {
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())
@@ -257,7 +258,7 @@ class StringQueryUnitTests {
@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);
@@ -265,7 +266,7 @@ class StringQueryUnitTests {
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");
@@ -274,7 +275,7 @@ class StringQueryUnitTests {
@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);
@@ -296,8 +297,8 @@ class StringQueryUnitTests {
@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)
@@ -310,8 +311,8 @@ class StringQueryUnitTests {
@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)
@@ -323,7 +324,7 @@ class StringQueryUnitTests {
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);
@@ -338,7 +339,7 @@ class StringQueryUnitTests {
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);
@@ -355,7 +356,7 @@ class StringQueryUnitTests {
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)");
@@ -376,7 +377,7 @@ class StringQueryUnitTests {
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)");
@@ -397,7 +398,7 @@ class StringQueryUnitTests {
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);
@@ -411,7 +412,7 @@ class StringQueryUnitTests {
@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();
@@ -424,7 +425,7 @@ class StringQueryUnitTests {
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);
@@ -443,7 +444,7 @@ class StringQueryUnitTests {
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");
}
@@ -452,7 +453,7 @@ class StringQueryUnitTests {
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");
}
@@ -460,17 +461,17 @@ class StringQueryUnitTests {
@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);
@@ -485,17 +486,17 @@ class StringQueryUnitTests {
@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);
@@ -511,7 +512,7 @@ class StringQueryUnitTests {
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);
@@ -527,13 +528,13 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
assertThat(bindings).hasSize(1);
@@ -544,8 +545,10 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
@@ -560,7 +563,8 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
@@ -571,7 +575,7 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
@@ -582,7 +586,7 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
@@ -593,7 +597,7 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
@@ -604,7 +608,7 @@ class StringQueryUnitTests {
@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<ParameterBinding> bindings = query.getParameterBindings();
@@ -615,7 +619,7 @@ class StringQueryUnitTests {
@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();
@@ -626,7 +630,7 @@ class StringQueryUnitTests {
@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();
@@ -637,8 +641,8 @@ class StringQueryUnitTests {
@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");
@@ -654,12 +658,11 @@ class StringQueryUnitTests {
@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();
}
/**
@@ -670,14 +673,16 @@ class StringQueryUnitTests {
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<ParameterBinding> bindings = query.getParameterBindings();
assertThat(bindings).isNotEmpty();
@@ -704,7 +709,7 @@ class StringQueryUnitTests {
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);
}
@@ -728,7 +733,7 @@ class StringQueryUnitTests {
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);
}
@@ -781,7 +786,7 @@ class StringQueryUnitTests {
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();
@@ -801,16 +806,18 @@ class StringQueryUnitTests {
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<String> testQueries = Arrays.asList( //
@@ -821,7 +828,7 @@ class StringQueryUnitTests {
for (String testQuery : testQueries) {
assertThat(new StringQuery(testQuery, false) //
assertThat(new TestEntityQuery(testQuery, false) //
.usesJdbcStyleParameters()) //
.describedAs(testQuery) //
.describedAs(testQuery) //
@@ -833,7 +840,7 @@ class StringQueryUnitTests {
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();
@@ -853,7 +860,7 @@ class StringQueryUnitTests {
"select a, b from C");
for (String queryString : queriesWithoutDefaultProjection) {
assertThat(new StringQuery(queryString, true).isDefaultProjection()) //
assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) //
.describedAs(queryString) //
.isFalse();
}
@@ -870,7 +877,7 @@ class StringQueryUnitTests {
);
for (String queryString : queriesWithDefaultProjection) {
assertThat(new StringQuery(queryString, true).isDefaultProjection()) //
assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) //
.describedAs(queryString) //
.isTrue();
}
@@ -880,7 +887,7 @@ class StringQueryUnitTests {
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();
@@ -892,7 +899,7 @@ class StringQueryUnitTests {
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) //
@@ -903,7 +910,7 @@ class StringQueryUnitTests {
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) //
@@ -912,9 +919,8 @@ class StringQueryUnitTests {
void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) {
EntityQuery introspectedQuery = nativeQuery
? EntityQuery.introspectNativeQuery(query, QueryEnhancerSelector.DEFAULT_SELECTOR)
: EntityQuery.introspectJpql(query, QueryEnhancerSelector.DEFAULT_SELECTOR);
DeclaredQuery declaredQuery = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
EntityQuery introspectedQuery = EntityQuery.create(declaredQuery, QueryEnhancerSelector.DEFAULT_SELECTOR);
assertThat(introspectedQuery.hasNamedParameter()) //
.describedAs("hasNamed Parameter " + label) //
@@ -927,7 +933,8 @@ class StringQueryUnitTests {
private void checkHasNamedParameter(String query, boolean expected, String label) {
DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
BindableQuery bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source);
PreprocessedQuery bindableQuery = PreprocessedQuery.ParameterBindingParser.INSTANCE.parse(source.getQueryString(),
source::rewrite, it -> {});
assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) //
.describedAs(String.format("<%s> (%s)", query, label)) //

View File

@@ -21,6 +21,8 @@ import org.junit.jupiter.api.Disabled;
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}.
@@ -45,7 +47,8 @@ class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests {
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 @@ class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@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");
}

View File

@@ -30,7 +30,7 @@ public class EqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
assumeThat(query.isNativeQuery()).isFalse();
assumeThat(query.isNative()).isFalse();
return JpaQueryEnhancer.forEql(query.getQueryString());
}

View File

@@ -29,6 +29,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
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 @@ class EqlQueryTransformerTests {
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 @@ class EqlQueryTransformerTests {
}
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) {

View File

@@ -30,7 +30,7 @@ public class HqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
assumeThat(query.isNativeQuery()).isFalse();
assumeThat(query.isNative()).isFalse();
return JpaQueryEnhancer.forHql(query.getQueryString());
}

View File

@@ -33,6 +33,8 @@ import org.springframework.data.domain.PageRequest;
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 @@ class HqlQueryTransformerTests {
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 @@ class HqlQueryTransformerTests {
}
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) {

View File

@@ -48,7 +48,8 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
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");
}
@@ -77,7 +78,7 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
ORDER BY b.b1, a.a1, a.a2
"""));
String sql = enhancer.createCountQueryFor();
String sql = enhancer.createCountQueryFor(null);
assertThat(sql).startsWith("SELECT count(DISTINCT a.*) FROM TableA a");
}
@@ -97,16 +98,16 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
+ "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.getDeclaredQuery()).create(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();
@@ -120,16 +121,16 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
+ "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.getDeclaredQuery()).create(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();
@@ -147,16 +148,16 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
+ "\tselect CustomerID from customers where country = 'Germany'\n"//
+ "\t;";
StringQuery stringQuery = new StringQuery(setQuery, true);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(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();
@@ -167,16 +168,15 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
String setQuery = "VALUES (1, 2, 'test')";
StringQuery stringQuery = new StringQuery(setQuery, true);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(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();
@@ -188,18 +188,18 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
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.getDeclaredQuery()).create(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().toLowerCase()).isEqualToIgnoringWhitespace(
assertThat(queryEnhancer.createCountQueryFor(null).toLowerCase()).isEqualToIgnoringWhitespace(
"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();
@@ -211,18 +211,18 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
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.getDeclaredQuery()).create(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().toLowerCase()).isEqualToIgnoringWhitespace(
assertThat(queryEnhancer.createCountQueryFor(null).toLowerCase()).isEqualToIgnoringWhitespace(
"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();
@@ -231,15 +231,15 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Test // GH-3038
void truncateStatementShouldWork() {
StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(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();
@@ -247,15 +247,14 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@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.getDeclaredQuery()).create(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();
@@ -285,4 +284,9 @@ class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
assertThat(queryEnhancer.getProjection()).isEqualTo("is_contained_in(:innerId, :outerId)");
}
private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) {
return new DefaultQueryRewriteInformation(sort,
ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()));
}
}

View File

@@ -30,7 +30,7 @@ public class JpqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery query) {
assumeThat(query.isNativeQuery()).isFalse();
assumeThat(query.isNative()).isFalse();
return JpaQueryEnhancer.forJpql(query.getQueryString());
}

View File

@@ -29,6 +29,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
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 @@ class JpqlQueryTransformerTests {
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 @@ class JpqlQueryTransformerTests {
}
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) {

View File

@@ -30,7 +30,6 @@ import org.mockito.Mock;
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;
@@ -71,14 +70,14 @@ class NativeJpaQueryUnitTests {
JpaQueryMethod queryMethod = new JpaQueryMethod(respositoryMethod, repositoryMetadata, projectionFactory,
queryExtractor);
Query annotation = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class);
NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(),
NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, queryMethod.getRequiredDeclaredQuery(),
queryMethod.getDeclaredCountQuery(),
new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR,
ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT));
String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType());
QueryProvider sql = query.getSortedQuery(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<Object, Object> {

View File

@@ -18,7 +18,6 @@ package org.springframework.data.jpa.repository.query;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindingParser;
/**
* Unit tests for the {@link ParameterBindingParser}.
@@ -68,7 +67,7 @@ class ParameterBindingParserUnitTests {
private void checkHasParameter(SoftAssertions softly, String query, boolean containsParameter, String label) {
StringQuery stringQuery = new StringQuery(query, false);
DefaultEntityQuery stringQuery = new TestEntityQuery(query, false);
softly.assertThat(stringQuery.getParameterBindings().size()) //
.describedAs(String.format("<%s> (%s)", query, label)) //

View File

@@ -32,9 +32,10 @@ class QueryEnhancerFactoryUnitTests {
@Test
void createsParsingImplementationForNonNativeQuery() {
StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false);
DefaultEntityQuery query = new TestEntityQuery("select new com.example.User(u.firstname) from User u",
false);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query);
QueryEnhancer queryEnhancer = QueryEnhancer.create(query);
assertThat(queryEnhancer) //
.isInstanceOf(JpaQueryEnhancer.class);
@@ -47,9 +48,9 @@ class QueryEnhancerFactoryUnitTests {
@Test
void createsJSqlImplementationForNativeQuery() {
StringQuery query = new StringQuery("select * from User", true);
DefaultEntityQuery query = new TestEntityQuery("select * from User", true);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query);
assertThat(queryEnhancer) //
.isInstanceOf(JSqlParserQueryEnhancer.class);

View File

@@ -36,7 +36,7 @@ abstract class QueryEnhancerTckTests {
void shouldDeriveNativeCountQuery(String query, String expected) {
QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query));
String countQueryFor = enhancer.createCountQueryFor();
String countQueryFor = enhancer.createCountQueryFor(null);
// lenient cleanup to allow for rendering variance
String sanitized = countQueryFor.replaceAll("\r", " ").replaceAll("\n", " ").replaceAll(" {2}", " ")
@@ -179,7 +179,7 @@ abstract class QueryEnhancerTckTests {
void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {
QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query));
String countQueryFor = enhancer.createCountQueryFor();
String countQueryFor = enhancer.createCountQueryFor(null);
assertThat(countQueryFor).isEqualToIgnoringCase(expected);
}
@@ -203,9 +203,9 @@ abstract class QueryEnhancerTckTests {
// DATAJPA-1696
void findProjectionClauseWithIncludedFrom() {
StringQuery query = new StringQuery("select x, frommage, y from t", true);
DefaultEntityQuery query = new TestEntityQuery("select x, frommage, y from t", true);
assertThat(createQueryEnhancer(query.getDeclaredQuery()).getProjection()).isEqualTo("x, frommage, y");
assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y");
}
abstract QueryEnhancer createQueryEnhancer(DeclaredQuery query);

View File

@@ -20,7 +20,6 @@ import static org.assertj.core.api.Assumptions.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@@ -30,9 +29,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.query.ReturnedType;
/**
* Unit tests for {@link QueryEnhancer}.
@@ -40,6 +42,7 @@ import org.springframework.data.jpa.domain.JpaSort;
* @author Diego Krupitza
* @author Geoffrey Deremetz
* @author Krzysztof Krason
* @author Mark Paluch
*/
class QueryEnhancerUnitTests {
@@ -78,7 +81,7 @@ class QueryEnhancerUnitTests {
@ParameterizedTest
@MethodSource("detectsAliasWithUCorrectlySource")
void detectsAliasWithUCorrectly(IntrospectedQuery query, String alias) {
void detectsAliasWithUCorrectly(DefaultEntityQuery query, String alias) {
assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax")
.doesNotStartWithIgnoringCase("from");
@@ -89,21 +92,21 @@ class QueryEnhancerUnitTests {
public static Stream<Arguments> detectsAliasWithUCorrectlySource() {
return Stream.of( //
Arguments.of(new StringQuery(QUERY, true), "u"), //
Arguments.of(new StringQuery(SIMPLE_QUERY, false), "u"), //
Arguments.of(new StringQuery(COUNT_QUERY, true), "u"), //
Arguments.of(new StringQuery(QUERY_WITH_AS, true), "u"), //
Arguments.of(new StringQuery("SELECT u FROM USER U", false), "U"), //
Arguments.of(new StringQuery("select u from User u", true), "u"), //
Arguments.of(new StringQuery("select u from com.acme.User u", true), "u"), //
Arguments.of(new StringQuery("select u from T05User u", true), "u") //
Arguments.of(new TestEntityQuery(QUERY, true), "u"), //
Arguments.of(new TestEntityQuery(SIMPLE_QUERY, false), "u"), //
Arguments.of(new TestEntityQuery(COUNT_QUERY, true), "u"), //
Arguments.of(new TestEntityQuery(QUERY_WITH_AS, true), "u"), //
Arguments.of(new TestEntityQuery("SELECT u FROM USER U", false), "U"), //
Arguments.of(new TestEntityQuery("select u from User u", true), "u"), //
Arguments.of(new TestEntityQuery("select u from com.acme.User u", true), "u"), //
Arguments.of(new TestEntityQuery("select u from T05User u", true), "u") //
);
}
@Test
void allowsFullyQualifiedEntityNamesInQuery() {
StringQuery query = new StringQuery(FQ_QUERY, true);
DefaultEntityQuery query = new TestEntityQuery(FQ_QUERY, true);
assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
assertCountQuery(FQ_QUERY, "select count(u) from org.acme.domain.User$Foo_Bar u", true);
@@ -112,20 +115,18 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-252
void doesNotPrefixOrderReferenceIfOuterJoinAliasDetected() {
StringQuery query = new StringQuery("select p from Person p left join p.address address", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p left join p.address address", true);
assertThat(getEnhancer(query).applySorting(Sort.by("address.city")))
assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("address.city"))))
.endsWithIgnoringCase("order by address.city asc");
assertThat(getEnhancer(query).applySorting(Sort.by("address.city", "lastname"), "p"))
.endsWithIgnoringCase("order by address.city asc, p.lastname asc");
}
@Test // DATAJPA-252
void extendsExistingOrderByClausesCorrectly() {
StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p order by p.lastname asc", true);
assertThat(getEnhancer(query).applySorting(Sort.by("firstname"), "p"))
assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("firstname"))))
.endsWithIgnoringCase("order by p.lastname asc, p.firstname asc");
}
@@ -134,9 +135,10 @@ class QueryEnhancerUnitTests {
Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase());
StringQuery query = new StringQuery("select p from Person p", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true);
assertThat(getEnhancer(query).applySorting(sort, "p")).endsWithIgnoringCase("order by lower(p.firstname) asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by lower(p.firstname) asc");
}
@Test // DATAJPA-296
@@ -144,9 +146,9 @@ class QueryEnhancerUnitTests {
Sort sort = Sort.by(Sort.Order.by("firstname").ignoreCase());
StringQuery query = new StringQuery("select p from Person p order by p.lastname asc", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p order by p.lastname asc", true);
assertThat(getEnhancer(query).applySorting(sort, "p"))
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by p.lastname asc, lower(p.firstname) asc");
}
@@ -160,12 +162,12 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-148
void doesNotPrefixSortsIfFunction() {
StringQuery query = new StringQuery("select p from Person p", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true);
Sort sort = Sort.by("sum(foo)");
QueryEnhancer enhancer = getEnhancer(query);
assertThatThrownBy(() -> enhancer.applySorting(sort, "p")) //
assertThatThrownBy(() -> enhancer.rewrite(getRewriteInformation(sort))) //
.isInstanceOf(InvalidDataAccessApiUsageException.class);
}
@@ -173,8 +175,8 @@ class QueryEnhancerUnitTests {
void findsExistingOrderByIndependentOfCase() {
Sort sort = Sort.by("lastname");
StringQuery originalQuery = new StringQuery("select p from Person p ORDER BY p.firstname", true);
String query = getEnhancer(originalQuery).applySorting(sort, "p");
DefaultEntityQuery originalQuery = new TestEntityQuery("select p from Person p ORDER BY p.firstname", true);
String query = getEnhancer(originalQuery).rewrite(getRewriteInformation(sort));
assertThat(query).endsWithIgnoringCase("ORDER BY p.firstname, p.lastname asc");
}
@@ -182,17 +184,17 @@ class QueryEnhancerUnitTests {
@Test // GH-3263
void preserveSourceQueryWhenAddingSort() {
StringQuery query = new StringQuery("WITH all_projects AS (SELECT * FROM projects) SELECT * FROM all_projects p",
true);
DefaultEntityQuery query = new TestEntityQuery(
"WITH all_projects AS (SELECT * FROM projects) SELECT * FROM all_projects p", true);
assertThat(getEnhancer(query).applySorting(Sort.by("name"), "p")) //
assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("name")))) //
.startsWithIgnoringCase(query.getQueryString()).endsWithIgnoringCase("ORDER BY p.name ASC");
}
@Test // GH-2812
void createCountQueryFromDeleteQuery() {
StringQuery query = new StringQuery("delete from some_table where id in :ids", true);
DefaultEntityQuery query = new TestEntityQuery("delete from some_table where id in :ids", true);
assertThat(getEnhancer(query).createCountQueryFor("p.lastname"))
.isEqualToIgnoringCase("delete from some_table where id in :ids");
@@ -201,7 +203,7 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-456
void createCountQueryFromTheGivenCountProjection() {
StringQuery query = new StringQuery("select p.lastname,p.firstname from Person p", true);
DefaultEntityQuery query = new TestEntityQuery("select p.lastname,p.firstname from Person p", true);
assertThat(getEnhancer(query).createCountQueryFor("p.lastname"))
.isEqualToIgnoringCase("select count(p.lastname) from Person p");
@@ -210,24 +212,26 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-726
void detectsAliasesInPlainJoins() {
StringQuery query = new StringQuery("select p from Customer c join c.productOrder p where p.delay = true", true);
DefaultEntityQuery query = new TestEntityQuery(
"select p from Customer c join c.productOrder p where p.delay = true", true);
Sort sort = Sort.by("p.lineItems");
assertThat(getEnhancer(query).applySorting(sort, "c")).endsWithIgnoringCase("order by p.lineItems asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by p.lineItems asc");
}
@Test // DATAJPA-736
void supportsNonAsciiCharactersInEntityNames() {
StringQuery query = new StringQuery("select u from Usèr u", true);
DefaultEntityQuery query = new TestEntityQuery("select u from Usèr u", true);
assertThat(getEnhancer(query).createCountQueryFor()).isEqualToIgnoringCase("select count(u) from Usèr u");
assertThat(getEnhancer(query).createCountQueryFor(null)).isEqualToIgnoringCase("select count(u) from Usèr u");
}
@Test // DATAJPA-798
void detectsAliasInQueryContainingLineBreaks() {
StringQuery query = new StringQuery("select \n u \n from \n User \nu", true);
DefaultEntityQuery query = new TestEntityQuery("select \n u \n from \n User \nu", true);
assertThat(getEnhancer(query).detectAlias()).isEqualTo("u");
}
@@ -236,26 +240,28 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-815
void doesPrefixPropertyWithNonNative() {
StringQuery query = new StringQuery("from Cat c join Dog d", false);
DefaultEntityQuery query = new TestEntityQuery("from Cat c join Dog d", false);
Sort sort = Sort.by("dPropertyStartingWithJoinAlias");
assertThat(getEnhancer(query).applySorting(sort, "c")).endsWith("order by c.dPropertyStartingWithJoinAlias asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWith("order by c.dPropertyStartingWithJoinAlias asc");
}
@Test // DATAJPA-815
void doesPrefixPropertyWithNative() {
StringQuery query = new StringQuery("Select * from Cat c join Dog d", true);
DefaultEntityQuery query = new TestEntityQuery("Select * from Cat c join Dog d", true);
Sort sort = Sort.by("dPropertyStartingWithJoinAlias");
assertThat(getEnhancer(query).applySorting(sort, "c"))
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by c.dPropertyStartingWithJoinAlias asc");
}
@Test // DATAJPA-938
void detectsConstructorExpressionInDistinctQuery() {
StringQuery query = new StringQuery("select distinct new com.example.Foo(b.name) from Bar b", false);
DefaultEntityQuery query = new TestEntityQuery("select distinct new com.example.Foo(b.name) from Bar b",
false);
assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
}
@@ -263,7 +269,7 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-938
void detectsComplexConstructorExpression() {
StringQuery query = new StringQuery("select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) " //
DefaultEntityQuery query = new TestEntityQuery("select new foo.bar.Foo(ip.id, ip.name, sum(lp.amount)) " //
+ "from Bar lp join lp.investmentProduct ip " //
+ "where (lp.toDate is null and lp.fromDate <= :now and lp.fromDate is not null) and lp.accountId = :accountId "
//
@@ -276,7 +282,7 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-938
void detectsConstructorExpressionWithLineBreaks() {
StringQuery query = new StringQuery("select new foo.bar.FooBar(\na.id) from DtoA a ", false);
DefaultEntityQuery query = new TestEntityQuery("select new foo.bar.FooBar(\na.id) from DtoA a ", false);
assertThat(getEnhancer(query).hasConstructorExpression()).isTrue();
}
@@ -285,140 +291,138 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-960
void doesNotQualifySortIfNoAliasDetectedNonNative() {
StringQuery query = new StringQuery("from mytable where ?1 is null", false);
DefaultEntityQuery query = new TestEntityQuery("from mytable where ?1 is null", false);
assertThat(getEnhancer(query).applySorting(Sort.by("firstname"))).endsWith("order by firstname asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("firstname"))))
.endsWith("order by firstname asc");
}
@Test // DATAJPA-960
void doesNotQualifySortIfNoAliasDetectedNative() {
StringQuery query = new StringQuery("Select * from mytable where ?1 is null", true);
DefaultEntityQuery query = new TestEntityQuery("Select * from mytable where ?1 is null", true);
assertThat(getEnhancer(query).applySorting(Sort.by("firstname"))).endsWithIgnoringCase("order by firstname asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(Sort.by("firstname"))))
.endsWithIgnoringCase("order by firstname asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotAllowWhitespaceInSort() {
StringQuery query = new StringQuery("select p from Person p", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true);
Sort sort = Sort.by("case when foo then bar");
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> getEnhancer(query).applySorting(sort, "p"));
.isThrownBy(() -> getEnhancer(query).rewrite(getRewriteInformation(sort)));
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixUnsafeJpaSortFunctionCalls() {
JpaSort sort = JpaSort.unsafe("sum(foo)");
StringQuery query = new StringQuery("select p from Person p", true);
DefaultEntityQuery query = new TestEntityQuery("select p from Person p", true);
assertThat(getEnhancer(query).applySorting(sort, "p")).endsWithIgnoringCase("order by sum(foo) asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by sum(foo) asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixMultipleAliasedFunctionCalls() {
StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m",
true);
DefaultEntityQuery query = new TestEntityQuery(
"SELECT AVG(m.price) AS avgPrice, SUM(m.stocks) AS sumStocks FROM Magazine m", true);
Sort sort = Sort.by("avgPrice", "sumStocks");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avgPrice asc, sumStocks asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by avgPrice asc, sumStocks asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixSingleAliasedFunctionCalls() {
StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true);
Sort sort = Sort.by("avgPrice");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avgPrice asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by avgPrice asc");
}
@Test // DATAJPA-965, DATAJPA-970
void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() {
StringQuery query = new StringQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS avgPrice FROM Magazine m", true);
Sort sort = Sort.by("someOtherProperty");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by m.someOtherProperty asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by m.someOtherProperty asc");
}
@Test // DATAJPA-965, DATAJPA-970
void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() {
StringQuery query = new StringQuery("SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery("SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m",
true);
Sort sort = Sort.by("name", "avgPrice");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by m.name asc, avgPrice asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by m.name asc, avgPrice asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixAliasedFunctionCallNameWithMultipleNumericParameters() {
StringQuery query = new StringQuery("SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery(
"SELECT SUBSTRING(m.name, 2, 5) AS trimmedName FROM Magazine m", true);
Sort sort = Sort.by("trimmedName");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by trimmedName asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by trimmedName asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixAliasedFunctionCallNameWithMultipleStringParameters() {
StringQuery query = new StringQuery("SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery(
"SELECT CONCAT(m.name, 'foo') AS extendedName FROM Magazine m", true);
Sort sort = Sort.by("extendedName");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by extendedName asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort)))
.endsWithIgnoringCase("order by extendedName asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixAliasedFunctionCallNameWithUnderscores() {
StringQuery query = new StringQuery("SELECT AVG(m.price) AS avg_price FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS avg_price FROM Magazine m", true);
Sort sort = Sort.by("avg_price");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avg_price asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by avg_price asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixAliasedFunctionCallNameWithDots() {
StringQuery query = new StringQuery("SELECT AVG(m.price) AS average FROM Magazine m", false);
DefaultEntityQuery query = new TestEntityQuery("SELECT AVG(m.price) AS average FROM Magazine m", false);
Sort sort = Sort.by("avg");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWith("order by m.avg asc");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWith("order by m.avg asc");
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixAliasedFunctionCallNameWithDotsNativeQuery() {
// this is invalid since the '.' character is not allowed. Not in sql nor in JPQL.
assertThatThrownBy(() -> new StringQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", true)) //
assertThatThrownBy(() -> new TestEntityQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m", true)) //
.isInstanceOf(IllegalArgumentException.class);
}
@Test // DATAJPA-965, DATAJPA-970
void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() {
StringQuery query = new StringQuery("SELECT AVG( m.price ) AS avgPrice FROM Magazine m", true);
DefaultEntityQuery query = new TestEntityQuery(
"SELECT AVG( m.price ) AS avgPrice FROM Magazine m", true);
Sort sort = Sort.by("avgPrice");
assertThat(getEnhancer(query).applySorting(sort, "m")).endsWithIgnoringCase("order by avgPrice asc");
}
@Test // DATAJPA-1000
void discoversCorrectAliasForJoinFetch() {
String queryString = "SELECT DISTINCT user FROM User user LEFT JOIN user.authorities AS authority";
Set<String> aliases = QueryUtils.getOuterJoinAliases(queryString);
StringQuery nativeQuery = new StringQuery(queryString, true);
Set<String> joinAliases = new JSqlParserQueryEnhancer(nativeQuery).getJoinAliases();
assertThat(aliases).containsExactly("authority");
assertThat(joinAliases).containsExactly("authority");
assertThat(getEnhancer(query).rewrite(getRewriteInformation(sort))).endsWithIgnoringCase("order by avgPrice asc");
}
@Test // DATAJPA-1171
@@ -438,11 +442,11 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-1506
void detectsAliasWithGroupAndOrderBy() {
StringQuery queryWithGroupNoAlias = new StringQuery("select * from User group by name", true);
StringQuery queryWithGroupAlias = new StringQuery("select * from User u group by name", true);
DefaultEntityQuery queryWithGroupNoAlias = new TestEntityQuery("select * from User group by name", true);
DefaultEntityQuery queryWithGroupAlias = new TestEntityQuery("select * from User u group by name", true);
StringQuery queryWithOrderNoAlias = new StringQuery("select * from User order by name", true);
StringQuery queryWithOrderAlias = new StringQuery("select * from User u order by name", true);
DefaultEntityQuery queryWithOrderNoAlias = new TestEntityQuery("select * from User order by name", true);
DefaultEntityQuery queryWithOrderAlias = new TestEntityQuery("select * from User u order by name", true);
assertThat(getEnhancer(queryWithGroupNoAlias).detectAlias()).isNull();
assertThat(getEnhancer(queryWithOrderNoAlias).detectAlias()).isNull();
@@ -453,12 +457,12 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-1061
void appliesSortCorrectlyForFieldAliases() {
StringQuery query = new StringQuery(
DefaultEntityQuery query = new TestEntityQuery(
"SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a",
true);
Sort sort = Sort.by("authorName");
String fullQuery = getEnhancer(query).applySorting(sort);
String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort));
assertThat(fullQuery).endsWithIgnoringCase("order by authorName asc");
}
@@ -466,11 +470,11 @@ class QueryEnhancerUnitTests {
@Test // GH-2280
void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() {
StringQuery query = new StringQuery("SELECT customer.id as id, customer.name as name FROM CustomerEntity customer",
true);
DefaultEntityQuery query = new TestEntityQuery(
"SELECT customer.id as id, customer.name as name FROM CustomerEntity customer", true);
Sort sort = Sort.by(Sort.Order.by("name").ignoreCase());
String fullQuery = getEnhancer(query).applySorting(sort);
String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort));
assertThat(fullQuery).isEqualToIgnoringCase(
"SELECT customer.id as id, customer.name as name FROM CustomerEntity customer order by lower(name) asc");
@@ -479,12 +483,12 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-1061
void appliesSortCorrectlyForFunctionAliases() {
StringQuery query = new StringQuery(
DefaultEntityQuery query = new TestEntityQuery(
"SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a",
true);
Sort sort = Sort.by("title");
String fullQuery = getEnhancer(query).applySorting(sort);
String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort));
assertThat(fullQuery).endsWithIgnoringCase("order by title asc");
}
@@ -492,12 +496,12 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-1061
void appliesSortCorrectlyForSimpleField() {
StringQuery query = new StringQuery(
DefaultEntityQuery query = new TestEntityQuery(
"SELECT m.price, lower(m.title) AS title, a.name as authorName FROM Magazine m INNER JOIN m.author a",
true);
Sort sort = Sort.by("price");
String fullQuery = getEnhancer(query).applySorting(sort);
String fullQuery = getEnhancer(query).rewrite(getRewriteInformation(sort));
assertThat(fullQuery).endsWithIgnoringCase("order by m.price asc");
}
@@ -505,30 +509,34 @@ class QueryEnhancerUnitTests {
@Test
void createCountQuerySupportsLineBreakRightAfterDistinct() {
StringQuery query1 = new StringQuery("select\ndistinct\nuser.age,\n" + //
DefaultEntityQuery query1 = new TestEntityQuery("select\ndistinct\nuser.age,\n" + //
"user.name\n" + //
"from\nUser\nuser", true);
StringQuery query2 = new StringQuery("select\ndistinct user.age,\n" + //
DefaultEntityQuery query2 = new TestEntityQuery("select\ndistinct user.age,\n" + //
"user.name\n" + //
"from\nUser\nuser", true);
assertThat(getEnhancer(query1).createCountQueryFor()).isEqualTo(getEnhancer(query2).createCountQueryFor());
assertThat(getEnhancer(query1).createCountQueryFor(null)).isEqualTo(getEnhancer(query2).createCountQueryFor(null));
}
@Test
void detectsAliasWithGroupAndOrderByWithLineBreaks() {
StringQuery queryWithGroupAndLineBreak = new StringQuery("select * from User group\nby name", true);
StringQuery queryWithGroupAndLineBreakAndAlias = new StringQuery("select * from User u group\nby name", true);
DefaultEntityQuery queryWithGroupAndLineBreak = new TestEntityQuery("select * from User group\nby name",
true);
DefaultEntityQuery queryWithGroupAndLineBreakAndAlias = new TestEntityQuery(
"select * from User u group\nby name", true);
assertThat(getEnhancer(queryWithGroupAndLineBreak).detectAlias()).isNull();
assertThat(getEnhancer(queryWithGroupAndLineBreakAndAlias).detectAlias()).isEqualTo("u");
StringQuery queryWithOrderAndLineBreak = new StringQuery("select * from User order\nby name", true);
StringQuery queryWithOrderAndLineBreakAndAlias = new StringQuery("select * from User u order\nby name", true);
StringQuery queryWithOrderAndMultipleLineBreakAndAlias = new StringQuery("select * from User\nu\norder \n by name",
DefaultEntityQuery queryWithOrderAndLineBreak = new TestEntityQuery("select * from User order\nby name",
true);
DefaultEntityQuery queryWithOrderAndLineBreakAndAlias = new TestEntityQuery(
"select * from User u order\nby name", true);
DefaultEntityQuery queryWithOrderAndMultipleLineBreakAndAlias = new TestEntityQuery(
"select * from User\nu\norder \n by name", true);
assertThat(getEnhancer(queryWithOrderAndLineBreak).detectAlias()).isNull();
assertThat(getEnhancer(queryWithOrderAndLineBreakAndAlias).detectAlias()).isEqualTo("u");
@@ -537,7 +545,7 @@ class QueryEnhancerUnitTests {
@ParameterizedTest // DATAJPA-1679
@MethodSource("findProjectionClauseWithDistinctSource")
void findProjectionClauseWithDistinct(IntrospectedQuery query, String expected) {
void findProjectionClauseWithDistinct(DefaultEntityQuery query, String expected) {
SoftAssertions.assertSoftly(sofly -> sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected));
}
@@ -545,10 +553,10 @@ class QueryEnhancerUnitTests {
public static Stream<Arguments> findProjectionClauseWithDistinctSource() {
return Stream.of( //
Arguments.of(new StringQuery("select * from x", true), "*"), //
Arguments.of(new StringQuery("select a, b, c from x", true), "a, b, c"), //
Arguments.of(new StringQuery("select distinct a, b, c from x", true), "a, b, c"), //
Arguments.of(new StringQuery("select DISTINCT a, b, c from x", true), "a, b, c") //
Arguments.of(new TestEntityQuery("select * from x", true), "*"), //
Arguments.of(new TestEntityQuery("select a, b, c from x", true), "a, b, c"), //
Arguments.of(new TestEntityQuery("select distinct a, b, c from x", true), "a, b, c"), //
Arguments.of(new TestEntityQuery("select DISTINCT a, b, c from x", true), "a, b, c") //
);
}
@@ -566,33 +574,17 @@ class QueryEnhancerUnitTests {
// This is a required behavior the testcase in #findProjectionClauseWithSubselect tells why
String queryString = "select * from (select x from y)";
StringQuery query = new StringQuery(queryString, true);
DefaultEntityQuery query = new TestEntityQuery(queryString, true);
assertThat(getEnhancer(query).getProjection()).isEqualTo("*");
}
@Disabled
@ParameterizedTest // DATAJPA-252
@MethodSource("detectsJoinAliasesCorrectlySource")
void detectsJoinAliasesCorrectly(String queryString, List<String> aliases) {
StringQuery nativeQuery = new StringQuery(queryString, true);
StringQuery nonNativeQuery = new StringQuery(queryString, false);
Set<String> nativeJoinAliases = getEnhancer(nativeQuery).getJoinAliases();
Set<String> nonNativeJoinAliases = getEnhancer(nonNativeQuery).getJoinAliases();
assertThat(nonNativeJoinAliases).containsAll(nativeJoinAliases);
assertThat(nativeJoinAliases).hasSameSizeAs(aliases) //
.containsAll(aliases);
}
@Test // GH-2441
void correctFunctionAliasWithComplexNestedFunctions() {
String queryString = "\nSELECT \nCAST(('{' || string_agg(distinct array_to_string(c.institutes_ids, ','), ',') || '}') AS bigint[]) as institutesIds\nFROM\ncity c";
StringQuery nativeQuery = new StringQuery(queryString, true);
DefaultEntityQuery nativeQuery = new TestEntityQuery(queryString, true);
JSqlParserQueryEnhancer queryEnhancer = (JSqlParserQueryEnhancer) getEnhancer(nativeQuery);
assertThat(queryEnhancer.getSelectionAliases()).contains("institutesIds");
@@ -608,9 +600,10 @@ class QueryEnhancerUnitTests {
+ " city c\n" //
+ " ) dd";
StringQuery nativeQuery = new StringQuery(queryString, true);
DefaultEntityQuery nativeQuery = new TestEntityQuery(queryString, true);
QueryEnhancer queryEnhancer = getEnhancer(nativeQuery);
String result = queryEnhancer.applySorting(Sort.by(new Sort.Order(Sort.Direction.ASC, "institutesIds")));
String result = queryEnhancer
.rewrite(getRewriteInformation(Sort.by(new Sort.Order(Sort.Direction.ASC, "institutesIds"))));
assertThat(result).containsIgnoringCase("order by dd.institutesIds");
}
@@ -625,23 +618,22 @@ class QueryEnhancerUnitTests {
boolean constructorExpressionNotConsideringQueryType = QueryUtils.hasConstructorExpression(modifyingQuery);
String countQueryForNotConsiderQueryType = QueryUtils.createCountQueryFor(modifyingQuery);
StringQuery modiQuery = new StringQuery(modifyingQuery, true);
DefaultEntityQuery modiQuery = new TestEntityQuery(modifyingQuery, true);
assertThat(modiQuery.getAlias()).isEqualToIgnoringCase(aliasNotConsideringQueryType);
assertThat(modiQuery.getProjection()).isEqualToIgnoringCase(projectionNotConsideringQueryType);
assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType);
assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery);
assertThat(QueryEnhancerFactory.forQuery(modiQuery.getDeclaredQuery()).create(modiQuery.getDeclaredQuery()).createCountQueryFor())
.isEqualToIgnoringCase(modifyingQuery);
assertThat(QueryEnhancer.create(modiQuery).createCountQueryFor(null)).isEqualToIgnoringCase(modifyingQuery);
}
@ParameterizedTest // GH-2593
@MethodSource("insertStatementIsProcessedSameAsDefaultSource")
void insertStatementIsProcessedSameAsDefault(String insertQuery) {
StringQuery stringQuery = new StringQuery(insertQuery, true);
QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery.getDeclaredQuery());
DefaultEntityQuery stringQuery = new TestEntityQuery(insertQuery, true);
QueryEnhancer queryEnhancer = QueryEnhancer.create(stringQuery);
Sort sorting = Sort.by("day").descending();
@@ -657,11 +649,11 @@ class QueryEnhancerUnitTests {
assertThat(stringQuery.hasConstructorExpression()).isFalse();
// access over enhancer
assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(queryUtilsCountQuery);
assertThat(queryEnhancer.applySorting(sorting)).isEqualTo(insertQuery); // cant check with queryutils result since
// query utils appens order by which is not
// supported by sql standard.
assertThat(queryEnhancer.getJoinAliases()).isEqualTo(queryUtilsOuterJoinAlias);
assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase(queryUtilsCountQuery);
assertThat(queryEnhancer.rewrite(getRewriteInformation(sorting))).isEqualTo(insertQuery); // cant check with
// queryutils result since
// query utils appens order by which is not
// supported by sql standard.
assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase(queryUtilsDetectAlias);
assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase(queryUtilsProjection);
assertThat(queryEnhancer.hasConstructorExpression()).isFalse();
@@ -689,15 +681,20 @@ class QueryEnhancerUnitTests {
}
private static void assertCountQuery(String originalQuery, String countQuery, boolean nativeQuery) {
assertCountQuery(new StringQuery(originalQuery, nativeQuery), countQuery);
assertCountQuery(new TestEntityQuery(originalQuery, nativeQuery), countQuery);
}
private static void assertCountQuery(StringQuery originalQuery, String countQuery) {
assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery);
private static void assertCountQuery(DefaultEntityQuery originalQuery, String countQuery) {
assertThat(getEnhancer(originalQuery).createCountQueryFor(null)).isEqualToIgnoringCase(countQuery);
}
private static QueryEnhancer getEnhancer(IntrospectedQuery query) {
return QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query.getDeclaredQuery());
private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) {
return new DefaultQueryRewriteInformation(sort,
ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()));
}
private static QueryEnhancer getEnhancer(DeclaredQuery query) {
return QueryEnhancer.create(query);
}
}

View File

@@ -53,7 +53,7 @@ class QueryParameterSetterFactoryUnitTests {
@Test // DATAJPA-1058
void noExceptionWhenQueryDoesNotContainNamedParameters() {
setterFactory.create(binding,
EntityQuery.introspectJpql("from Employee e", QueryEnhancerSelector.DEFAULT_SELECTOR));
EntityQuery.create(DeclaredQuery.jpqlQuery("from Employee e"), QueryEnhancerSelector.DEFAULT_SELECTOR));
}
@Test // DATAJPA-1058
@@ -63,7 +63,7 @@ class QueryParameterSetterFactoryUnitTests {
assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(() -> setterFactory.create(binding,
EntityQuery.introspectJpql("from Employee e where e.name = :NamedParameter",
EntityQuery.create(DeclaredQuery.jpqlQuery("from Employee e where e.name = :NamedParameter"),
QueryEnhancerSelector.DEFAULT_SELECTOR))) //
.withMessageContaining("Java 8") //
.withMessageContaining("@Param") //
@@ -81,10 +81,9 @@ class QueryParameterSetterFactoryUnitTests {
when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1));
assertThatExceptionOfType(IllegalArgumentException.class) //
.isThrownBy(
() -> setterFactory.create(binding,
EntityQuery.introspectJpql("from Employee e where e.name = ?1",
QueryEnhancerSelector.DEFAULT_SELECTOR))) //
.isThrownBy(() -> setterFactory.create(binding,
EntityQuery.create(DeclaredQuery.jpqlQuery("from Employee e where e.name = ?1"),
QueryEnhancerSelector.DEFAULT_SELECTOR))) //
.withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query");
}
}

View File

@@ -121,7 +121,8 @@ class SimpleJpaQueryUnitTests {
extractor);
when(em.createQuery("foo", Long.class)).thenReturn(typedQuery);
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, CONFIG);
SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, method.getDeclaredQuery("select u from User u"), null,
CONFIG);
assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {})))
.isEqualTo(typedQuery);
@@ -135,7 +136,8 @@ class SimpleJpaQueryUnitTests {
Method method = UserRepository.class.getMethod("findAllPaged", Pageable.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null, CONFIG);
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em,
queryMethod.getDeclaredQuery("select u from User u"), null, CONFIG);
jpaQuery.createCountQuery(
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -150,7 +152,7 @@ class SimpleJpaQueryUnitTests {
Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em,
queryMethod.getAnnotatedQuery(), null, CONFIG);
queryMethod.getRequiredDeclaredQuery(), null, CONFIG);
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
@@ -169,7 +171,7 @@ class SimpleJpaQueryUnitTests {
Method method = SampleRepository.class.getMethod("findByLastnameNativeAnnotation", String.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em,
queryMethod.getAnnotatedQuery(), null, CONFIG);
queryMethod.getRequiredDeclaredQuery(), null, CONFIG);
assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class);
@@ -281,8 +283,9 @@ class SimpleJpaQueryUnitTests {
Method method = SampleRepository.class.getMethod("findAllWithExpressionInCountQuery", Pageable.class);
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u",
"select count(u.id) from #{#entityName} u", CONFIG);
AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em,
queryMethod.getDeclaredQuery("select u from User u"),
queryMethod.getDeclaredQuery("select count(u.id) from #{#entityName} u"), CONFIG);
jpaQuery.createCountQuery(
new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) }));
@@ -294,18 +297,18 @@ class SimpleJpaQueryUnitTests {
return createJpaQuery(method, null);
}
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString,
@Nullable String countQueryString) {
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable DeclaredQuery query,
@Nullable DeclaredQuery countQzery) {
return JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, queryString,
countQueryString, CONFIG);
return JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, query, countQzery,
CONFIG);
}
private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional<String> countQueryString) {
private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional<DeclaredQuery> countQueryString) {
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(),
countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery()));
return createJpaQuery(queryMethod, queryMethod.getRequiredDeclaredQuery(),
countQueryString == null ? null : countQueryString.orElse(queryMethod.getDeclaredCountQuery()));
}
interface SampleRepository {

View File

@@ -33,7 +33,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.Part.Type;
/**
* Unit tests for {@link ExpressionBasedStringQuery}.
* Unit tests for {@link TemplatedQuery}.
*
* @author Thomas Darimont
* @author Oliver Gierke
@@ -45,7 +45,7 @@ import org.springframework.data.repository.query.parser.Part.Type;
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class ExpressionBasedStringQueryUnitTests {
class TemplatedQueryUnitTests {
private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(),
QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT);
@@ -61,16 +61,14 @@ class ExpressionBasedStringQueryUnitTests {
void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() {
String source = "select u from #{#entityName} u where u.firstname like :firstname";
StringQuery query = new ExpressionBasedStringQuery(source, metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
EntityQuery query = jpqlEntityQuery(source);
assertThat(query.getQueryString()).isEqualTo("select u from User u where u.firstname like :firstname");
}
@Test // DATAJPA-424
void renderAliasInExpressionQueryCorrectly() {
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector());
DefaultEntityQuery query = jpqlEntityQuery("select u from #{#entityName} u");
assertThat(query.getAlias()).isEqualTo("u");
assertThat(query.getQueryString()).isEqualTo("select u from User u");
}
@@ -78,12 +76,11 @@ class ExpressionBasedStringQueryUnitTests {
@Test // DATAJPA-1695
void shouldDetectBindParameterCountCorrectly() {
StringQuery query = new ExpressionBasedStringQuery(
EntityQuery query = jpqlEntityQuery(
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(:#{#networkRequest.name})) OR :#{#networkRequest.name} IS NULL "
+ "AND (LOWER(n.server) LIKE LOWER(:#{#networkRequest.server})) OR :#{#networkRequest.server} IS NULL "
+ "AND (n.createdAt >= :#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=:#{#networkRequest.createdTime.endDateTime}) "
+ "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})",
metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
+ "AND (n.updatedAt >= :#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=:#{#networkRequest.updatedTime.endDateTime})");
assertThat(query.getParameterBindings()).hasSize(8);
}
@@ -91,12 +88,11 @@ class ExpressionBasedStringQueryUnitTests {
@Test // GH-2228
void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() {
StringQuery query = new ExpressionBasedStringQuery(
EntityQuery query = jpqlEntityQuery(
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )"
+ "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)"
+ "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})"
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})");
assertThat(query.getParameterBindings()).hasSize(8);
}
@@ -104,40 +100,28 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void shouldDetectComplexNativeQueriesWithSpelAsNonNative() {
StringQuery query = new ExpressionBasedStringQuery(
DefaultEntityQuery query = jpqlEntityQuery(
"select n from #{#entityName} n where (LOWER(n.name) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.name},'%')), '')) OR ?#{#networkRequest.name} IS NULL )"
+ "AND (LOWER(n.server) LIKE LOWER(NULLIF(text(concat('%',?#{#networkRequest.server},'%')), '')) OR ?#{#networkRequest.server} IS NULL)"
+ "AND (n.createdAt >= ?#{#networkRequest.createdTime.startDateTime}) AND (n.createdAt <=?#{#networkRequest.createdTime.endDateTime})"
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})",
metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
+ "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})");
assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse();
}
@Test
void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() {
StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector());
assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse();
assertThat(query.isNative()).isFalse();
}
@Test
void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() {
StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector());
DefaultEntityQuery query = nativeEntityQuery("select u from User u");
assertThat(query.getDeclaredQuery().isNativeQuery()).isTrue();
assertThat(query.isNative()).isTrue();
}
@Test // GH-3041
void namedExpressionsShouldCreateLikeBindings() {
StringQuery query = new ExpressionBasedStringQuery(
"select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
EntityQuery query = jpqlEntityQuery(
"select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%");
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString()).isEqualTo(
@@ -160,9 +144,8 @@ class ExpressionBasedStringQueryUnitTests {
@Test // GH-3041
void indexedExpressionsShouldCreateLikeBindings() {
StringQuery query = new ExpressionBasedStringQuery(
"select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
EntityQuery query = jpqlEntityQuery(
"select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%");
assertThat(query.hasParameterBindings()).isTrue();
assertThat(query.getQueryString())
@@ -185,8 +168,7 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void doesTemplatingWhenEntityNameSpelIsPresent() {
StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u",
metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
EntityQuery query = jpqlEntityQuery("select #{#entityName + 'Hallo'} from #{#entityName} u");
assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u");
}
@@ -194,8 +176,7 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void doesNoTemplatingWhenEntityNameSpelIsNotPresent() {
StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata,
CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
EntityQuery query = jpqlEntityQuery("select #{#entityName + 'Hallo'} from User u");
assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u");
}
@@ -203,9 +184,16 @@ class ExpressionBasedStringQueryUnitTests {
@Test
void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() {
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}",
metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector());
EntityQuery query = jpqlEntityQuery("select u from #{#entityName} u where name = :#{#something}");
assertThat(query.getQueryString()).isEqualTo("select u from User u where name = :__$synthetic$__1");
}
private DefaultEntityQuery nativeEntityQuery(String source) {
return (DefaultEntityQuery) TemplatedQuery.create(DeclaredQuery.nativeQuery(source), metadata, CONFIG);
}
private DefaultEntityQuery jpqlEntityQuery(String source) {
return (DefaultEntityQuery) TemplatedQuery.create(DeclaredQuery.jpqlQuery(source), metadata, CONFIG);
}
}

View File

@@ -16,23 +16,21 @@
package org.springframework.data.jpa.repository.query;
/**
* @author Christoph Strobl
* Test-variant of {@link DefaultEntityQuery} with a simpler constructor.
*
* @author Mark Paluch
*/
final class JpqlQuery implements DeclaredQuery {
class TestEntityQuery extends DefaultEntityQuery {
private final String jpql;
/**
* Creates a new {@link DefaultEntityQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
TestEntityQuery(String query, boolean isNative) {
JpqlQuery(String jpql) {
this.jpql = jpql;
}
@Override
public boolean isNativeQuery() {
return false;
}
@Override
public String getQueryString() {
return jpql;
}
super(PreprocessedQuery.parse(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query)),
QueryEnhancerSelector.DEFAULT_SELECTOR
.select(isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query)));
}
}

View File

@@ -308,17 +308,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
----
====
[TIP]
====
It is possible to disable usage of `JSqlParser` for parsing native queries although it is available on the classpath by setting `spring.data.jpa.query.native.parser=regex` via the `spring.properties` file or a system property.
Valid values are (case-insensitive):
* `auto` (default, automatic selection)
* `regex` (Use the builtin regex-based Query Enhancer)
* `jsqlparser` (Use JSqlParser)
====
A similar approach also works with named native queries, by adding the `.count` suffix to a copy of your query. You probably need to register a result set mapping for your count query, though.
Next to obtaining mapped results, native queries allow you to read the raw `Tuple` from the database by choosing a `Map` container as the method's return type.
@@ -344,8 +333,120 @@ interface UserRepository extends JpaRepository<User, Long> {
NOTE: String-based Tuple Queries are only supported by Hibernate.
Eclipselink supports only Criteria-based Tuple Queries.
[[jpa.query-methods.at-query.projections]]
[[jpa.query-methods.query-introspection-rewriting]]
=== Query Introspection and Rewriting
Spring Data JPA provides a wide range of functionality that can be used to run various flavors of queries.
Specifically, given a declared query, Spring Data JPA can:
* Introspect a query for its projection and run a tuple query for interface projections
* Use DTO projections if the query uses constructor expressions and rewrite the projection when the query declares the entity alias or just a multi-select of expressions
* Apply dynamic sorting
* Derive a `COUNT` query
For this purpose, we ship with Query Parsers specific to HQL (Hibernate) and EQL (EclipseLink) dialects as these dialects are well-defined.
SQL on the other hand allows for quite some variance across dialects.
Because of this, there is no way Spring Data will ever be able to support all levels of query complexity.
We are not general purpose SQL parser library but one to increase developer productivity through making query execution simpler.
Our built-in SQL query enhancer supports only simple queries for introspection `COUNT` query derivation.
A more complex query will require either the usage of link:https://github.com/JSQLParser/JSqlParser[JSqlParser] or that you provide a `COUNT` query through `@Query(countQuery=…)`.
If JSqlParser is on the class path, Spring Data JPA will use it for native queries.
For a fine-grained control over selection, you can configure javadoc:org.springframework.data.jpa.repository.query.QueryEnhancerSelector[] using `@EnableJpaRepositories`:
.Spring Data JPA repositories using JavaConfig
====
[source,java]
----
@Configuration
@EnableJpaRepositories(queryEnhancerSelector = MyQueryEnhancerSelector.class)
class ApplicationConfig {
// …
}
----
====
`QueryEnhancerSelector` is a strategy interface intended to select a javadoc:org.springframework.data.jpa.repository.query.QueryEnhancer[] based on a specific query.
You can also provide your own `QueryEnhancer` implementation if you want.
[[jpa.query-methods.query-rewriter]]
=== Applying a QueryRewriter
Sometimes, no matter how many features you try to apply, it seems impossible to get Spring Data JPA to apply every thing you'd like to a query before it is sent to the `EntityManager`.
You have the ability to get your hands on the query, right before it's sent to the `EntityManager` and "rewrite" it.
That is, you can make any alterations at the last moment.
.Declare a QueryRewriter using `@Query`
====
[source,java]
----
public interface MyRepository extends JpaRepository<User, Long> {
@NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias",
queryRewriter = MyQueryRewriter.class)
List<User> findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias",
queryRewriter = MyQueryRewriter.class)
List<User> findByNonNativeQuery(String param);
}
----
====
This example shows both a native (pure SQL) rewriter as well as a JPQL query, both leveraging the same `QueryRewriter`.
In this scenario, Spring Data JPA will look for a bean registered in the application context of the corresponding type.
You can write a query rewriter like this:
.Example `QueryRewriter`
====
[source,java]
----
public class MyQueryRewriter implements QueryRewriter {
@Override
public String rewrite(String query, Sort sort) {
return query.replaceAll("original_user_alias", "rewritten_user_alias");
}
}
----
====
You have to ensure your `QueryRewriter` is registered in the application context, whether it's by applying one of Spring Framework's
`@Component`-based annotations, or having it as part of a `@Bean` method inside an `@Configuration` class.
Another option is to have the repository itself implement the interface.
.Repository that provides the `QueryRewriter`
====
[source,java]
----
public interface MyRepository extends JpaRepository<User, Long>, QueryRewriter {
@Query(value = "select original_user_alias.* from SD_USER original_user_alias",
nativeQuery = true,
queryRewriter = MyRepository.class)
List<User> findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias",
queryRewriter = MyRepository.class)
List<User> findByNonNativeQuery(String param);
@Override
default String rewrite(String query, Sort sort) {
return query.replaceAll("original_user_alias", "rewritten_user_alias");
}
}
----
====
Depending on what you're doing with your `QueryRewriter`, it may be advisable to have more than one, each registered with the application context.
NOTE: In a CDI-based environment, Spring Data JPA will search the `BeanManager` for instances of your implementation of
`QueryRewriter`.
[[jpa.query-methods.at-query.projections]]
[[jpa.query-methods.sorting]]
== Using Sort
@@ -440,14 +541,14 @@ NOTE: The method parameters are switched according to their order in the defined
NOTE: As of version 4, Spring fully supports Java 8s parameter name discovery based on the `-parameters` compiler flag. By using this flag in your build as an alternative to debug information, you can omit the `@Param` annotation for named parameters.
[[jpa.query.spel-expressions]]
== Using Expressions
== Templated Queries and Expressions
We support the usage of restricted expressions in manually defined queries that are defined with `@Query`.
Upon the query being run, these expressions are evaluated against a predefined set of variables.
NOTE: If you are not familiar with Value Expressions, please refer to xref:jpa/value-expressions.adoc[] to learn about SpEL Expressions and Property Placeholders.
Spring Data JPA supports a variable called `entityName`.
Spring Data JPA supports a template variable called `entityName`.
Its usage is `select x from #{#entityName} x`.
It inserts the `entityName` of the domain type associated with the given repository.
The `entityName` is resolved as follows: