JpaQueryMethod.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.LockModeType;
import jakarta.persistence.QueryHint;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
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;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Meta;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* JPA specific extension of {@link QueryMethod}.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Nicolas Cirigliano
* @author Mark Paluch
* @author ������������ ��������������
* @author R��da Housni Alaoui
* @author Greg Turnquist
* @author Aleksei Elin
*/
public class JpaQueryMethod extends QueryMethod {
/**
* @see <a href=
* "https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1#persistent-fields-and-properties">Jakarta
* Persistence Specification: Persistent Fields and Properties - Paragraph starting with
* "Collection-valued persistent...".</a>
*/
private static final Set<Class<?>> NATIVE_ARRAY_TYPES = Set.of(byte[].class, Byte[].class, char[].class,
Character[].class);
private static final StoredProcedureAttributeSource storedProcedureAttributeSource = StoredProcedureAttributeSource.INSTANCE;
private final QueryExtractor extractor;
private final Method method;
private final Class<?> returnType;
private @Nullable StoredProcedureAttributes storedProcedureAttributes;
private final Lazy<LockModeType> lockModeType;
private final Lazy<QueryHints> queryHints;
private final Lazy<JpaEntityGraph> jpaEntityGraph;
private final Lazy<Modifying> modifying;
private final Lazy<Boolean> isNativeQuery;
private final Lazy<Boolean> isCollectionQuery;
private final Lazy<Boolean> isProcedureQuery;
private final Lazy<JpaEntityMetadata<?>> entityMetadata;
private final Lazy<Optional<Meta>> metaAnnotation;
/**
* Creates a {@link JpaQueryMethod}.
*
* @param method must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @param factory must not be {@literal null}.
* @param extractor must not be {@literal null}.
*/
public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
QueryExtractor extractor) {
this(method, metadata, factory, extractor, JpaParameters::new);
}
/**
* Creates a {@link JpaQueryMethod}.
*
* @param method must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @param factory must not be {@literal null}.
* @param extractor must not be {@literal null}.
* @param parametersFunction function to obtain {@link JpaParameters}, must not be {@literal null}.
* @since 3.5
*/
public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
QueryExtractor extractor, Function<ParametersSource, JpaParameters> parametersFunction) {
super(method, metadata, factory, parametersFunction);
Assert.notNull(method, "Method must not be null");
Assert.notNull(extractor, "Query extractor must not be null");
this.method = method;
this.returnType = potentiallyUnwrapReturnTypeFor(metadata, method);
this.extractor = extractor;
this.lockModeType = Lazy
.of(() -> (LockModeType) Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Lock.class)) //
.map(AnnotationUtils::getValue) //
.orElse(null));
this.queryHints = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, QueryHints.class));
this.modifying = Lazy.of(() -> AnnotatedElementUtils.findMergedAnnotation(method, Modifying.class));
this.jpaEntityGraph = Lazy.of(() -> {
EntityGraph entityGraph = AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class);
if (entityGraph == null) {
return null;
}
return new JpaEntityGraph(entityGraph, getNamedQueryName());
});
this.isNativeQuery = Lazy.of(() -> getAnnotationValue("nativeQuery", Boolean.class));
this.isCollectionQuery = Lazy.of(() -> super.isCollectionQuery() && !NATIVE_ARRAY_TYPES.contains(this.returnType));
this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null);
this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass()));
this.metaAnnotation = Lazy
.of(() -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Meta.class)));
if (isModifyingQuery() && getParameters().hasSpecialParameter()) {
throw QueryCreationException.create(this,
String.format("Modifying method must not contain %s", Parameters.TYPES));
}
}
private static Class<?> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
TypeInformation<?> returnType = metadata.getReturnType(method);
while (QueryExecutionConverters.supports(returnType.getType())
|| QueryExecutionConverters.supportsUnwrapping(returnType.getType())) {
returnType = returnType.getRequiredComponentType();
}
return returnType.getType();
}
@Override
public JpaEntityMetadata<?> getEntityInformation() {
return this.entityMetadata.get();
}
/**
* Returns whether the finder is a modifying one.
*
* @return
*/
@Override
public boolean isModifyingQuery() {
return modifying.getNullable() != null;
}
/**
* Returns all {@link QueryHint}s annotated at this class. Note, that {@link QueryHints}
*
* @return
*/
List<QueryHint> getHints() {
QueryHints hints = this.queryHints.getNullable();
if (hints != null) {
return Arrays.asList(hints.value());
}
return Collections.emptyList();
}
/**
* Returns the {@link LockModeType} to be used for the query.
*
* @return
*/
@Nullable
LockModeType getLockModeType() {
return lockModeType.getNullable();
}
/**
* Returns the {@link EntityGraph} to be used for the query.
*
* @return
* @since 1.6
*/
@Nullable
JpaEntityGraph getEntityGraph() {
return jpaEntityGraph.getNullable();
}
/**
* Returns whether the potentially configured {@link QueryHint}s shall be applied when triggering the count query for
* pagination.
*
* @return
*/
boolean applyHintsToCountQuery() {
QueryHints hints = this.queryHints.getNullable();
return hints != null ? hints.forCounting() : false;
}
/**
* Returns the {@link QueryExtractor}.
*
* @return
*/
public QueryExtractor getQueryExtractor() {
return extractor;
}
/**
* Returns the {@link Method}.
*
* @return
*/
Method getMethod() {
return method;
}
/**
* Returns the actual return type of the method.
*
* @return
*/
Class<?> getReturnType() {
return returnType;
}
/**
* @return return true if {@link Meta} annotation is available.
* @since 3.0
*/
public boolean hasQueryMetaAttributes() {
return getMetaAnnotation() != null;
}
/**
* Returns the {@link Meta} annotation that is applied to the method or {@code null} if not available.
*
* @return
* @since 3.0
*/
@Nullable
Meta getMetaAnnotation() {
return metaAnnotation.get().orElse(null);
}
/**
* Returns the {@link org.springframework.data.jpa.repository.query.Meta} attributes to be applied.
*
* @return never {@literal null}.
* @since 1.6
*/
public org.springframework.data.jpa.repository.query.Meta getQueryMetaAttributes() {
Meta meta = getMetaAnnotation();
if (meta == null) {
return new org.springframework.data.jpa.repository.query.Meta();
}
org.springframework.data.jpa.repository.query.Meta metaAttributes = new org.springframework.data.jpa.repository.query.Meta();
if (StringUtils.hasText(meta.comment())) {
metaAttributes.setComment(meta.comment());
}
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.
*
* @return
*/
public @Nullable String getAnnotatedQuery() {
String query = getAnnotationValue("value", String.class);
return StringUtils.hasText(query) ? query : null;
}
/**
* @return {@code true} if this method is annotated with {@code @Query(name=���)}.
*/
boolean hasAnnotatedQueryName() {
return StringUtils.hasText(getAnnotationValue("name", String.class));
}
/**
* Returns the required query string declared in 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 2.0
*/
public String getRequiredAnnotatedQuery() throws IllegalStateException {
String query = getAnnotatedQuery();
if (query != null) {
return query;
}
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.
*
* @return
*/
public @Nullable String getCountQuery() {
String countQuery = getAnnotationValue("countQuery", String.class);
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.
*
* @return
* @since 1.6
*/
@Nullable
String getCountQueryProjection() {
String countProjection = getAnnotationValue("countProjection", String.class);
return StringUtils.hasText(countProjection) ? countProjection : null;
}
/**
* Returns whether the backing query is a native one.
*
* @return
*/
boolean isNativeQuery() {
return this.isNativeQuery.get();
}
/**
* Utility method that returns a {@link DeclaredQuery} object for the given {@code queryString}.
*
* @param query the query string to wrap.
* @return a {@link DeclaredQuery} object for the given {@code queryString}.
* @since 4.0
*/
DeclaredQuery getDeclaredQuery(String query) {
return isNativeQuery() ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query);
}
@Override
public String getNamedQueryName() {
String annotatedName = getAnnotationValue("name", String.class);
return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName();
}
/**
* Returns the name of the {@link NamedQuery} that shall be used for count queries.
*
* @return
*/
public String getNamedCountQueryName() {
String annotatedName = getAnnotationValue("countName", String.class);
return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count";
}
/**
* Returns whether we should flush automatically for modifying queries.
*
* @return whether we should flush automatically.
*/
boolean getFlushAutomatically() {
return getMergedOrDefaultAnnotationValue("flushAutomatically", Modifying.class, Boolean.class);
}
/**
* Returns whether we should clear automatically for modifying queries.
*
* @return whether we should clear automatically.
*/
boolean getClearAutomatically() {
return getMergedOrDefaultAnnotationValue("clearAutomatically", Modifying.class, Boolean.class);
}
/**
* Returns the {@link Query} annotation's attribute casted to the given type or default value if no annotation
* available.
*
* @param attribute
* @param type
* @return
*/
private <T> T getAnnotationValue(String attribute, Class<T> type) {
return getMergedOrDefaultAnnotationValue(attribute, Query.class, type);
}
@SuppressWarnings({ "rawtypes", "unchecked", "NullAway" })
private <T> T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class<T> targetType) {
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationType);
if (annotation == null) {
return targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute));
}
return targetType.cast(AnnotationUtils.getValue(annotation, attribute));
}
@Override
public JpaParameters getParameters() {
return (JpaParameters) super.getParameters();
}
@Override
public boolean isCollectionQuery() {
return this.isCollectionQuery.get();
}
/**
* Return {@literal true} if the method contains a {@link Procedure} annotation.
*
* @return
*/
public boolean isProcedureQuery() {
return this.isProcedureQuery.get();
}
/**
* Returns a new {@link StoredProcedureAttributes} representing the stored procedure meta-data for this
* {@link JpaQueryMethod}.
*
* @return
*/
StoredProcedureAttributes getProcedureAttributes() {
if (storedProcedureAttributes == null) {
this.storedProcedureAttributes = storedProcedureAttributeSource.createFrom(method, getEntityInformation());
}
return storedProcedureAttributes;
}
/**
* Returns the {@link QueryRewriter} type.
*
* @return type of the {@link QueryRewriter}
* @since 3.0
*/
public Class<? extends QueryRewriter> getQueryRewriter() {
return getMergedOrDefaultAnnotationValue("queryRewriter", Query.class, Class.class);
}
}