AbstractStringBasedJpaQuery.java
/*
* Copyright 2008-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
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;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Lazy;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentLruCache;
/**
* Base class for {@link String} based JPA queries.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Jens Schauder
* @author Tom Hombergs
* @author David Madden
* @author Mark Paluch
* @author Diego Krupitza
* @author Greg Turnquist
* @author Christoph Strobl
*/
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
private final EntityQuery query;
private final Map<Class<?>, Boolean> knownProjections = new ConcurrentHashMap<>();
private final Lazy<ParametrizedQuery> countQuery;
private final ValueExpressionDelegate valueExpressionDelegate;
private final QueryRewriter queryRewriter;
private final QuerySortRewriter querySortRewriter;
private final Lazy<ParameterBinder> countParameterBinder;
private final ValueEvaluationContextProvider valueExpressionContextProvider;
private final boolean hasDeclaredCountQuery;
/**
* 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 queryString must not be {@literal null}.
* @param queryConfiguration must not be {@literal null}.
*/
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.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 = TemplatedQuery.create(query, method.getEntityInformation(), queryConfiguration);
this.hasDeclaredCountQuery = countQuery != null;
this.countQuery = Lazy.of(() -> {
if (countQuery != null) {
return TemplatedQuery.create(countQuery, method.getEntityInformation(), queryConfiguration);
}
return this.query.deriveCountQuery(method.getCountQueryProjection());
});
this.countParameterBinder = Lazy.of(() -> this.createBinder(this.countQuery.get()));
this.queryRewriter = queryConfiguration.getQueryRewriter(method);
JpaParameters parameters = method.getParameters();
if (parameters.hasDynamicProjection()) {
this.querySortRewriter = SimpleQuerySortRewriter.INSTANCE;
} else {
if (parameters.hasPageableParameter() || parameters.hasSortParameter()) {
this.querySortRewriter = new CachingQuerySortRewriter();
} else {
this.querySortRewriter = new UnsortedCachingQuerySortRewriter();
}
}
if (!method.isNativeQuery() && this.query.usesJdbcStyleParameters()) {
throw QueryCreationException.create(method, "JDBC-style parameters (?) are not supported for JPA queries");
}
}
@Override
public boolean hasDeclaredCountQuery() {
return hasDeclaredCountQuery;
}
@Override
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
Sort sort = accessor.getSort();
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
ReturnedType returnedType = getReturnedType(processor);
QueryProvider sortedQuery = getSortedQuery(sort, returnedType);
Query query = createJpaQuery(sortedQuery, sort, accessor.getPageable(), returnedType);
// it is ok to reuse the binding contained in the ParameterBinder, although we create a new query String because the
// parameters in the query do not change.
return parameterBinder.get().bindAndPrepare(query, accessor);
}
/**
* Post-process {@link ReturnedType} to determine if the query is projecting by checking the projection and property
* assignability.
*
* @param processor
* @return
*/
ReturnedType getReturnedType(ResultProcessor processor) {
ReturnedType returnedType = processor.getReturnedType();
Class<?> returnedJavaType = returnedType.getReturnedType();
if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNative()) {
return returnedType;
}
Boolean known = knownProjections.get(returnedJavaType);
if (known != null && known) {
return returnedType;
}
if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)
|| !returnedType.needsCustomConstruction()) {
if (known == null) {
knownProjections.put(returnedJavaType, false);
}
return new NonProjectingReturnedType(returnedType);
}
knownProjections.put(returnedJavaType, true);
return returnedType;
}
QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) {
return querySortRewriter.getSorted(query, sort, returnedType);
}
@Override
protected ParameterBinder createBinder() {
return createBinder(query);
}
protected ParameterBinder createBinder(ParametrizedQuery query) {
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query,
valueExpressionDelegate, valueExpressionContextProvider);
}
@Override
protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) {
String queryString = countQuery.get().getQueryString();
EntityManager em = getEntityManager();
String queryStringToUse = potentiallyRewriteQuery(queryString, accessor.getSort(), accessor.getPageable());
Query query = getQueryMethod().isNativeQuery() //
? em.createNativeQuery(queryStringToUse) //
: em.createQuery(queryStringToUse, Long.class);
countParameterBinder.get().bind(new QueryParameterSetter.BindableQuery(query), accessor,
QueryParameterSetter.ErrorHandling.LENIENT);
return query;
}
/**
* @return the query
*/
public EntityQuery getQuery() {
return query;
}
/**
* @return the countQuery
*/
public ParametrizedQuery getCountQuery() {
return countQuery.get();
}
/**
* Creates an appropriate JPA query from an {@link EntityManager} according to the current {@link AbstractJpaQuery}
* type.
*/
protected Query createJpaQuery(QueryProvider query, Sort sort, @Nullable Pageable pageable,
ReturnedType returnedType) {
EntityManager em = getEntityManager();
String queryToUse = potentiallyRewriteQuery(query.getQueryString(), sort, pageable);
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
return em.createQuery(queryToUse);
}
Class<?> typeToRead = getTypeToRead(returnedType);
return typeToRead == null //
? em.createQuery(queryToUse) //
: em.createQuery(queryToUse, typeToRead);
}
/**
* Use the {@link QueryRewriter}, potentially rewrite the query, using relevant {@link Sort} and {@link Pageable}
* information.
*
* @param originalQuery
* @param sort
* @param pageable
* @return
*/
protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nullable Pageable pageable) {
return pageable != null && pageable.isPaged() //
? queryRewriter.rewrite(originalQuery, pageable) //
: queryRewriter.rewrite(originalQuery, sort);
}
QueryProvider applySorting(CachableQuery cachableQuery) {
return cachableQuery.getDeclaredQuery()
.rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType()));
}
/**
* Query Sort Rewriter interface.
*/
interface QuerySortRewriter {
QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType);
}
/**
* No-op query rewriter.
*/
enum SimpleQuerySortRewriter implements QuerySortRewriter {
INSTANCE;
public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) {
return query.rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}
}
static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter {
private volatile @Nullable QueryProvider cachedQuery;
public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) {
if (sort.isSorted()) {
throw new UnsupportedOperationException("NoOpQueryCache does not support sorting");
}
QueryProvider cachedQuery = this.cachedQuery;
if (cachedQuery == null) {
this.cachedQuery = cachedQuery = query
.rewrite(new DefaultQueryRewriteInformation(sort, returnedType));
}
return cachedQuery;
}
}
/**
* Caching variant of {@link QuerySortRewriter}.
*/
class CachingQuerySortRewriter implements QuerySortRewriter {
private final ConcurrentLruCache<CachableQuery, QueryProvider> queryCache = new ConcurrentLruCache<>(16,
AbstractStringBasedJpaQuery.this::applySorting);
private volatile @Nullable QueryProvider cachedQuery;
@Override
public QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType) {
if (sort.isUnsorted()) {
QueryProvider cachedQuery = this.cachedQuery;
if (cachedQuery == null) {
this.cachedQuery = cachedQuery = queryCache.get(new CachableQuery(query, sort, returnedType));
}
return cachedQuery;
}
return queryCache.get(new CachableQuery(query, sort, returnedType));
}
}
/**
* Value object with optimized {@link Object#equals(Object)} to cache a query based on its query string and
* {@link Sort sorting}.
*
* @since 3.2.3
* @author Christoph Strobl
*/
static class CachableQuery {
private final EntityQuery query;
private final String queryString;
private final Sort sort;
private final ReturnedType returnedType;
CachableQuery(EntityQuery query, Sort sort, ReturnedType returnedType) {
this.query = query;
this.queryString = query.getQueryString();
this.sort = sort;
this.returnedType = returnedType;
}
EntityQuery getDeclaredQuery() {
return query;
}
Sort getSort() {
return sort;
}
public ReturnedType getReturnedType() {
return returnedType;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CachableQuery that = (CachableQuery) o;
if (!Objects.equals(queryString, that.queryString)) {
return false;
}
return Objects.equals(sort, that.sort);
}
@Override
public int hashCode() {
int result = queryString != null ? queryString.hashCode() : 0;
result = 31 * result + (sort != null ? sort.hashCode() : 0);
return result;
}
}
/**
* Non-projecting {@link ReturnedType} wrapper that delegates to the original {@link ReturnedType} but always returns
* {@code false} for {@link #isProjecting()}. This type is to indicate that this query is not projecting, even if the
* original {@link ReturnedType} was because we e.g. select a nested property and do not want DTO constructor
* expression rewriting to kick in.
*/
private static class NonProjectingReturnedType extends ReturnedType {
private final ReturnedType delegate;
NonProjectingReturnedType(ReturnedType delegate) {
super(delegate.getDomainType());
this.delegate = delegate;
}
@Override
public boolean isProjecting() {
return false;
}
@Override
public Class<?> getReturnedType() {
return delegate.getReturnedType();
}
@Override
public boolean needsCustomConstruction() {
return false;
}
@Override
@Nullable
public Class<?> getTypeToRead() {
return delegate.getTypeToRead();
}
@Override
public List<String> getInputProperties() {
return delegate.getInputProperties();
}
}
}