diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 64e61530f6..0a86a5dabd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -18,9 +18,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.QueryRewriter; @@ -65,8 +62,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param queryRewriter must not be {@literal null}. */ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, QueryMethodEvaluationContextProvider evaluationContextProvider, - SpelExpressionParser parser) { + @Nullable String countQueryString, QueryRewriter queryRewriter, + QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { super(method, em); @@ -77,7 +74,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri this.evaluationContextProvider = evaluationContextProvider; this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser, - method.isNativeQuery()); + method.isNativeQuery(), method.getQueryEnhancerChoice()); DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection()); this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser, diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java index 2359260dab..f08081165a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java @@ -27,7 +27,7 @@ * @author Diego Krupitza * @since 2.0.3 */ -interface DeclaredQuery { +public interface DeclaredQuery { /** * Creates a {@literal DeclaredQuery} from a query {@literal String}. @@ -111,4 +111,27 @@ default boolean usesPaging() { default boolean isNativeQuery() { return false; } + + /** + * Gets the {@link QueryEnhancer} used for this Query. + * + * @return the concrete {@link QueryEnhancer} implementation used for this given Query + */ + @Nullable + QueryEnhancer getQueryEnhancer(); + + /** + * Returns the methods {@link QueryEnhancerChoice} annotation + * + * @return + */ + @Nullable + QueryEnhancerChoice getQueryEnhancerChoice(); + + /** + * Returns whether the method has a {@link QueryEnhancerChoice} or not + * + * @return + */ + boolean hasQueryEnhancerChoice(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java index 7dd4f9ce2a..372e663d1f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java @@ -26,41 +26,35 @@ * @author Diego Krupitza * @since 2.7.0 */ -public class DefaultQueryEnhancer implements QueryEnhancer { - - private final DeclaredQuery query; +public class DefaultQueryEnhancer extends QueryEnhancer { public DefaultQueryEnhancer(DeclaredQuery query) { - this.query = query; + super(query); } @Override public String applySorting(Sort sort, @Nullable String alias) { - return QueryUtils.applySorting(this.query.getQueryString(), sort, alias); + return QueryUtils.applySorting(this.getQuery().getQueryString(), sort, alias); } @Override public String detectAlias() { - return QueryUtils.detectAlias(this.query.getQueryString()); + return QueryUtils.detectAlias(this.getQuery().getQueryString()); } @Override public String createCountQueryFor(@Nullable String countProjection) { - return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection); + return QueryUtils.createCountQueryFor(this.getQuery().getQueryString(), countProjection); } @Override public String getProjection() { - return QueryUtils.getProjection(this.query.getQueryString()); + return QueryUtils.getProjection(this.getQuery().getQueryString()); } @Override public Set getJoinAliases() { - return QueryUtils.getOuterJoinAliases(this.query.getQueryString()); + return QueryUtils.getOuterJoinAliases(this.getQuery().getQueryString()); } - @Override - public DeclaredQuery getQuery() { - return this.query; - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java index a2cf998401..a6dd7b7fad 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java @@ -25,6 +25,7 @@ * NULL-Object pattern implementation for {@link DeclaredQuery}. * * @author Jens Schauder + * @author Diego Krupitza * @since 2.0.3 */ class EmptyDeclaredQuery implements DeclaredQuery { @@ -76,4 +77,19 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable Str public boolean usesJdbcStyleParameters() { return false; } + + @Override + public QueryEnhancer getQueryEnhancer() { + return null; + } + + @Override + public QueryEnhancerChoice getQueryEnhancerChoice() { + return null; + } + + @Override + public boolean hasQueryEnhancerChoice() { + return false; + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java index 8bb25aa6c6..1f65be5a7f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java @@ -22,12 +22,13 @@ import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression. *

- * Currently the following template variables are available: + * Currently, the following template variables are available: *

    *
  1. {@code #entityName} - the simple class name of the given entity
  2. *
      @@ -60,7 +61,23 @@ class ExpressionBasedStringQuery extends StringQuery { */ public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, SpelExpressionParser parser, boolean nativeQuery) { - super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query)); + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), + null); + } + + /** + * Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link EntityMetadata}. + * + * @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 queryEnhancerChoice may be {@literal null}. + */ + public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, SpelExpressionParser parser, + boolean nativeQuery, @Nullable QueryEnhancerChoice queryEnhancerChoice) { + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), + queryEnhancerChoice); } /** @@ -74,7 +91,8 @@ public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, S */ static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata metadata, SpelExpressionParser parser, boolean nativeQuery) { - return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery); + return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery, + query.getQueryEnhancerChoice()); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java index 0efa084c1e..a21acf9fc4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -15,9 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlCount; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlLower; -import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression; +import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*; +import static org.springframework.data.jpa.repository.query.QueryUtils.*; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Alias; @@ -29,11 +28,23 @@ import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.merge.Merge; -import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectBody; +import net.sf.jsqlparser.statement.select.SelectExpressionItem; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SetOperationList; +import net.sf.jsqlparser.statement.select.WithItem; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.values.ValuesStatement; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.domain.Sort; @@ -50,17 +61,15 @@ * @author Geoffrey Deremetz * @since 2.7.0 */ -public class JSqlParserQueryEnhancer implements QueryEnhancer { +public class JSqlParserQueryEnhancer extends QueryEnhancer { - private final DeclaredQuery query; private final ParsedType parsedType; /** * @param query the query we want to enhance. Must not be {@literal null}. */ public JSqlParserQueryEnhancer(DeclaredQuery query) { - - this.query = query; + super(query); this.parsedType = detectParsedType(); } @@ -72,7 +81,7 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) { private ParsedType detectParsedType() { try { - Statement statement = CCJSqlParserUtil.parse(this.query.getQueryString()); + Statement statement = CCJSqlParserUtil.parse(this.getQuery().getQueryString()); if (statement instanceof Insert) { return ParsedType.INSERT; @@ -95,7 +104,7 @@ private ParsedType detectParsedType() { @Override public String applySorting(Sort sort, @Nullable String alias) { - String queryString = query.getQueryString(); + String queryString = this.getQuery().getQueryString(); Assert.hasText(queryString, "Query must not be null or empty"); if (this.parsedType != ParsedType.SELECT) { @@ -192,7 +201,7 @@ Set getSelectionAliases() { return new HashSet<>(); } - Select selectStatement = parseSelectStatement(this.query.getQueryString()); + Select selectStatement = parseSelectStatement(this.getQuery().getQueryString()); PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody(); return this.getSelectionAliases(selectBody); } @@ -280,7 +289,7 @@ private OrderByElement getOrderClause(final Set joinAliases, final Set getJoinAliases() { - return this.getJoinAliases(this.query.getQueryString()); + return this.getJoinAliases(this.getQuery().getQueryString()); } /** @@ -492,11 +501,6 @@ private boolean onlyASingleColumnProjection(List projection) { && (((SelectExpressionItem) projection.get(0)).getExpression()) instanceof Column; } - @Override - public DeclaredQuery getQuery() { - return this.query; - } - /** * An enum to represent the top level parsed statement of the provided query. *
        diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index 883e600217..4ae7b5c2f5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -62,6 +62,7 @@ * @author Сергей Цыпанов * @author Réda Housni Alaoui * @author Greg Turnquist + * @author Diego Krupitza */ public class JpaQueryMethod extends QueryMethod { @@ -98,6 +99,7 @@ public class JpaQueryMethod extends QueryMethod { private final Lazy isProcedureQuery; private final Lazy> entityMetadata; private final Map, Optional> annotationCache; + private final Lazy queryEnhancerChoice; /** * Creates a {@link JpaQueryMethod}. @@ -140,6 +142,11 @@ protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionF this.isProcedureQuery = Lazy.of(() -> AnnotationUtils.findAnnotation(method, Procedure.class) != null); this.entityMetadata = Lazy.of(() -> new DefaultJpaEntityMetadata<>(getDomainClass())); this.annotationCache = new ConcurrentReferenceHashMap<>(); + this.queryEnhancerChoice = Lazy.of(() -> // + Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, QueryEnhancerChoice.class)) // + .orElseGet( + () -> AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), QueryEnhancerChoice.class)) // + ); Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()), String.format("Modifying method must not contain %s", Parameters.TYPES)); @@ -387,6 +394,25 @@ boolean isNativeQuery() { return this.isNativeQuery.get(); } + /** + * Returns the methods {@link QueryEnhancerChoice} annotation + * + * @return + */ + @Nullable + QueryEnhancerChoice getQueryEnhancerChoice() { + return this.queryEnhancerChoice.getNullable(); + } + + /** + * Returns whether the method has a {@link QueryEnhancerChoice} or not + * + * @return + */ + boolean hasQueryEnhancerChoice() { + return this.queryEnhancerChoice.getOptional().isPresent(); + } + @Override public String getNamedQueryName() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java index 1c470ae671..f2a67c7271 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java @@ -27,7 +27,13 @@ * @author Greg Turnquist * @since 2.7.0 */ -public interface QueryEnhancer { +public abstract class QueryEnhancer { + + private final DeclaredQuery query; + + protected QueryEnhancer(DeclaredQuery query) { + this.query = query; + } /** * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. @@ -35,7 +41,7 @@ public interface QueryEnhancer { * @param sort the sort specification to apply. * @return the modified query string. */ - default String applySorting(Sort sort) { + public String applySorting(Sort sort) { return applySorting(sort, detectAlias()); } @@ -46,22 +52,21 @@ default String applySorting(Sort sort) { * @param alias the alias to be used in the order by clause. May be {@literal null} or empty. * @return the modified query string. */ - String applySorting(Sort sort, @Nullable String alias); + public abstract String applySorting(Sort sort, @Nullable String alias); /** * Resolves the alias for the entity to be retrieved from the given JPA query. * * @return Might return {@literal null}. */ - @Nullable - String detectAlias(); + @Nullable public abstract String detectAlias(); /** * Creates a count projected query from the given original query. * * @return Guaranteed to be not {@literal null}. */ - default String createCountQueryFor() { + public String createCountQueryFor() { return createCountQueryFor(null); } @@ -71,14 +76,14 @@ default String createCountQueryFor() { * @param countProjection may be {@literal null}. * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. */ - String createCountQueryFor(@Nullable String countProjection); + public abstract String createCountQueryFor(@Nullable String countProjection); /** * Returns whether the given JPQL query contains a constructor expression. * * @return whether the given JPQL query contains a constructor expression. */ - default boolean hasConstructorExpression() { + public boolean hasConstructorExpression() { return QueryUtils.hasConstructorExpression(getQuery().getQueryString()); } @@ -87,14 +92,16 @@ default boolean hasConstructorExpression() { * * @return the projection part of the query. */ - String getProjection(); + public abstract String getProjection(); - Set getJoinAliases(); + public abstract Set getJoinAliases(); /** * Gets the query we want to use for enhancements. * * @return non-null {@link DeclaredQuery} that wraps the query */ - DeclaredQuery getQuery(); + public DeclaredQuery getQuery() { + return this.query; + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerChoice.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerChoice.java new file mode 100644 index 0000000000..f9d670f7c2 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerChoice.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 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.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotations that allows to select which specific {@link QueryEnhancer} should be used. If provided on a method level + * the given {@link QueryEnhancer} will be only used for this single method. If placed above a {@link ElementType#TYPE} + * it will be globally used for all the queries inside this type. + * + * @author Diego Krupitza + */ +@Inherited +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface QueryEnhancerChoice { + + /** + * The selected {@link QueryEnhancer} to use. + */ + Class value() default DefaultQueryEnhancer.class; + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java index 8365122cdd..295a847c9b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactory.java @@ -15,8 +15,14 @@ */ package org.springframework.data.jpa.repository.query; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.data.util.Lazy; /** * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link DeclaredQuery}. @@ -33,6 +39,42 @@ public final class QueryEnhancerFactory { private QueryEnhancerFactory() {} + /** + * Creates a {@link List} containing all the possible {@link QueryEnhancer} implementations for native queries. The + * order of the entries indicates that the first {@link QueryEnhancer} should be used. If this is not possible the + * next until the last one is reached which will be always {@link DefaultQueryEnhancer}. + * + * @param query the query for which the list should be created for + * @return list containing all the suitable implementations for the given query + */ + private static List> nativeQueryEnhancers(DeclaredQuery query) { + ArrayList> suitableImplementations = new ArrayList<>(); + + if (qualifiesForJSqlParserUsage(query)) { + suitableImplementations.add(Lazy.of(() -> new JSqlParserQueryEnhancer(query))); + } + + // DefaultQueryEnhancer has to be the last since this is our fallback + suitableImplementations.add(Lazy.of(() -> new DefaultQueryEnhancer(query))); + return suitableImplementations; + } + + /** + * Creates a {@link List} containing all the possible {@link QueryEnhancer} implementations for non-native queries. + * The order of the entries indicates that the first {@link QueryEnhancer} should be used. If this is not possible the + * next until the last one is reached which will be always {@link DefaultQueryEnhancer}. + * + * @param query the query for which the list should be created for + * @return list containing all the suitable implementations for the given query + */ + private static List> nonNativeQueryEnhancers(DeclaredQuery query) { + ArrayList> suitableImplementations = new ArrayList<>(); + + // DefaultQueryEnhancer has to be the last since this is our fallback + suitableImplementations.add(Lazy.of(() -> new DefaultQueryEnhancer(query))); + return suitableImplementations; + } + /** * Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}. * @@ -40,12 +82,57 @@ private QueryEnhancerFactory() {} * @return an implementation of {@link QueryEnhancer} that suits the query the most */ public static QueryEnhancer forQuery(DeclaredQuery query) { + if (query.getQueryEnhancer() != null) { + return query.getQueryEnhancer(); + } - if (qualifiesForJSqlParserUsage(query)) { - return new JSqlParserQueryEnhancer(query); - } else { - return new DefaultQueryEnhancer(query); + if (query.hasQueryEnhancerChoice()) { + LOG.debug("Using QueryEnhancerChoice for the query [%s]".formatted(query.getQueryString())); + return getQueryEnhancerByChoice(query); + } + + return findBestQueryEnhancerFit(query); + } + + /** + * Gets the {@link QueryEnhancer} that was selected by using the {@link QueryEnhancerChoice}. + * + * @param query the query for which we want to extract the {@link QueryEnhancer} + * @return an implementation of {@link QueryEnhancer} that was provided as {@link QueryEnhancerChoice} + */ + private static QueryEnhancer getQueryEnhancerByChoice(DeclaredQuery query) { + try { + return Objects.requireNonNull(query.getQueryEnhancerChoice()) // + .value() // + .getConstructor(DeclaredQuery.class) // + .newInstance(query); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException("Could not create QueryEnhancer of type %s for query [%s]!".formatted( + Objects.requireNonNull(query.getQueryEnhancerChoice()).value().getName(), query.getQueryString()), e); + } + } + + /** + * Tries to find the best {@link QueryEnhancer} implementation for the given query. + * + * @param query the query for which we want to find the implementation + * @return the best fit {@link QueryEnhancer} + */ + private static QueryEnhancer findBestQueryEnhancerFit(DeclaredQuery query) { + List> suitableQueryEnhancers = query.isNativeQuery() ? nativeQueryEnhancers(query) + : nonNativeQueryEnhancers(query); + + for (Lazy suitableQueryEnhancer : suitableQueryEnhancers) { + try { + return suitableQueryEnhancer.get(); + } catch (Exception e) { + LOG.debug("Falling back to next QueryEnhancer implementation, due to exception.", e); + } } + + throw new IllegalStateException( + "No QueryEnhancer found for the query [%s]! This should not happen since the default implementation (DefaultQueryEnhancer) should have been called!" + .formatted(query.getQueryString())); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index cdd49c6241..10ed3a5622 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -350,7 +350,7 @@ private static String getOrderClause(Set joinAliases, Set select * @param query a query string to extract the aliases of joins from. Must not be {@literal null}. * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}. */ - static Set getOuterJoinAliases(String query) { + public static Set getOuterJoinAliases(String query) { Set result = new HashSet<>(); Matcher matcher = JOIN_PATTERN.matcher(query); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index 56e2d46185..ebcbdcdae6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -60,17 +60,32 @@ class StringQuery implements DeclaredQuery { private final boolean usesJdbcStyleParameters; private final boolean isNative; private final QueryEnhancer queryEnhancer; + private final QueryEnhancerChoice queryEnhancerChoice; /** * Creates a new {@link StringQuery} from the given JPQL query. * * @param query must not be {@literal null} or empty. + * @param isNative is the query native or not */ @SuppressWarnings("deprecation") StringQuery(String query, boolean isNative) { + this(query, isNative, null); + } + + /** + * Creates a new {@link StringQuery} from the given JPQL query. + * + * @param query must not be {@literal null} or empty. + * @param isNative is the query native or not + * @param queryEnhancerChoice the QueryEnhancer choice made by the dev. May be {@literal null}. + */ + @SuppressWarnings("deprecation") + StringQuery(String query, boolean isNative, @Nullable QueryEnhancerChoice queryEnhancerChoice) { Assert.hasText(query, "Query must not be null or empty"); + this.queryEnhancerChoice = queryEnhancerChoice; this.isNative = isNative; this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); @@ -86,6 +101,21 @@ class StringQuery implements DeclaredQuery { this.hasConstructorExpression = this.queryEnhancer.hasConstructorExpression(); } + @Override + public QueryEnhancer getQueryEnhancer() { + return this.queryEnhancer; + } + + @Override + public QueryEnhancerChoice getQueryEnhancerChoice() { + return this.queryEnhancerChoice; + } + + @Override + public boolean hasQueryEnhancerChoice() { + return this.queryEnhancerChoice != null; + } + /** * Returns whether we have found some like bindings. */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 4d32e8b5c0..70b8f1019d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -15,14 +15,13 @@ */ package org.springframework.data.jpa.repository; -import static java.util.Arrays.asList; +import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.domain.Example.of; +import static org.springframework.data.domain.Example.*; import static org.springframework.data.domain.ExampleMatcher.*; -import static org.springframework.data.domain.Sort.Direction.ASC; -import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.springframework.data.domain.Sort.Direction.*; +import static org.springframework.data.jpa.domain.Specification.*; import static org.springframework.data.jpa.domain.Specification.not; -import static org.springframework.data.jpa.domain.Specification.where; import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; import jakarta.persistence.EntityManager; @@ -34,7 +33,14 @@ import jakarta.persistence.criteria.Root; import lombok.Data; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; @@ -48,7 +54,14 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.domain.*; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.Specification; @@ -2974,6 +2987,25 @@ void containsWithCollection() { assertThat(result).containsOnly(firstUser, secondUser); } + @Test + void nativeQueryWithSpELStatementTest() { + + String exampleFirstName = "Peter"; + User firstPeter = new User(exampleFirstName, "2", "email1@test.com"); + repository.save(firstPeter); + + User secondPeter = new User(exampleFirstName, "2", "email2@test.com"); + repository.save(secondPeter); + + User firstDiego = new User("Diego", "2", "email3@test.com"); + repository.save(firstDiego); + + User exampleUser = new User(exampleFirstName, "IGNORE", "IGNORE@IGNORE.com"); + List foundData = repository.nativeQueryWithSpELStatement(exampleUser); + + assertThat(foundData).hasSize(2); + } + @Test // GH-2593 void insertStatementModifyingQueryWorks() { flushTestUsers(); @@ -3019,6 +3051,18 @@ void mergeWithNativeStatement() { assertThat(repository.findById(firstUser.getId())) // .isPresent() // .map(User::getAge).contains(30); + + } + + @Test + void customQueryEnhacerIsUsedWhenUsingChoice() { + flushTestUsers(); + assertThat(repository.findAll()).hasSize(4); + + // this should return there is only one element because our MyCustomQueryEnhancer has for "createCountQueryFor" a + // simple "Select 1" + Page pageWithCustomQueryEnhancer = repository.findPageWithCustomQueryEnhancer(Pageable.ofSize(1)); + assertThat(pageWithCustomQueryEnhancer.getTotalElements()).isEqualTo(1); } private Page executeSpecWithSort(Sort sort) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java index 10579e2971..d462219f90 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java @@ -27,10 +27,14 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -48,6 +52,7 @@ 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.sample.MyCustomQueryEnhancer; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -66,6 +71,7 @@ * @author Christoph Strobl * @author Jens Schauder * @author Mark Paluch + * @author Diego Krupitza */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -479,6 +485,49 @@ void usesAliasedValueForEntityGraph() throws Exception { assertThat(method.getEntityGraph().getType()).isEqualTo(EntityGraphType.LOAD); } + @MethodSource("shouldDetectQueryEnhancerChoiceCorrectlySource") + @ParameterizedTest + void shouldDetectQueryEnhancerChoiceCorrectly(String methodName, Class expected) + throws Exception { + + JpaQueryMethod queryMethod = getQueryMethod(ValidRepository.class, methodName); + + if (expected == null) { + assertThat(queryMethod.getQueryEnhancerChoice()).isNull(); + } else { + assertThat(queryMethod.getQueryEnhancerChoice().value()).isEqualTo(expected); + } + } + + @MethodSource("shouldDetectQueryEnhancerChoiceCorrectlyOnInterfaceLevelSource") + @ParameterizedTest + void shouldDetectQueryEnhancerChoiceCorrectlyOnInterfaceLevel(String methodName, + Class expected) throws Exception { + + JpaQueryMethod queryMethod = getQueryMethod(QueryEnhancerRepositoryMode.class, methodName); + + assertThat(queryMethod.getQueryEnhancerChoice()).isNotNull(); + assertThat(queryMethod.getQueryEnhancerChoice().value()).isEqualTo(expected); + } + + public static Stream shouldDetectQueryEnhancerChoiceCorrectlySource() { + return Stream.of( // + Arguments.of("shouldUseDefaultEnhancer", DefaultQueryEnhancer.class), // + Arguments.of("shouldUseJSQLParserEnhancer", JSqlParserQueryEnhancer.class), // + Arguments.of("shouldUseTotalCustomEnhancer", MyCustomQueryEnhancer.class), // + Arguments.of("findsProjection", null) // + ); + } + + public static Stream shouldDetectQueryEnhancerChoiceCorrectlyOnInterfaceLevelSource() { + return Stream.of( // + Arguments.of("shouldUseDefaultEnhancer", DefaultQueryEnhancer.class), // + Arguments.of("shouldUseJSQLParserEnhancer", JSqlParserQueryEnhancer.class), // + Arguments.of("shouldUseTotalCustomEnhancer", MyCustomQueryEnhancer.class), // + Arguments.of("findAll", DefaultQueryEnhancer.class) // + ); + } + /** * Interface to define invalid repository methods for testing. * @@ -543,6 +592,36 @@ interface ValidRepository extends Repository { @CustomComposedAnnotationWithAliasFor void withMetaAnnotationUsingAliasFor(); + + @QueryEnhancerChoice(JSqlParserQueryEnhancer.class) + @org.springframework.data.jpa.repository.Query(value = "Select * from User u", nativeQuery = true) + List shouldUseJSQLParserEnhancer(); + + @QueryEnhancerChoice + @org.springframework.data.jpa.repository.Query(value = "Select * from User u", nativeQuery = true) + List shouldUseDefaultEnhancer(); + + @QueryEnhancerChoice(MyCustomQueryEnhancer.class) + @org.springframework.data.jpa.repository.Query(value = "Select * from User u", nativeQuery = true) + List shouldUseTotalCustomEnhancer(); + } + + @QueryEnhancerChoice + interface QueryEnhancerRepositoryMode extends Repository { + + List findAll(); + + @QueryEnhancerChoice(JSqlParserQueryEnhancer.class) + @org.springframework.data.jpa.repository.Query(value = "Select * from User u", nativeQuery = true) + List shouldUseJSQLParserEnhancer(); + + @QueryEnhancerChoice + @org.springframework.data.jpa.repository.Query(value = "Select * from User u", nativeQuery = true) + List shouldUseDefaultEnhancer(); + + @QueryEnhancerChoice(MyCustomQueryEnhancer.class) + @org.springframework.data.jpa.repository.Query(value = "Select * from User u", nativeQuery = true) + List shouldUseTotalCustomEnhancer(); } interface JpaRepositoryOverride extends JpaRepository { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java index a6248e5a6c..2798ae9eb6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java @@ -17,7 +17,14 @@ import static org.assertj.core.api.Assertions.*; +import java.lang.annotation.Annotation; +import java.util.stream.Stream; + 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.data.jpa.repository.sample.MyCustomQueryEnhancer; /** * Unit tests for {@link QueryEnhancerFactory}. @@ -47,4 +54,55 @@ void createsJSqlImplementationForNativeQuery() { assertThat(queryEnhancer) // .isInstanceOf(JSqlParserQueryEnhancer.class); } + + @Test + void fallsBackToOtherQueryEnhancerWhenUsingHibernatePlaceHolder() { + StringQuery query = new StringQuery("SELECT c.* FROM {h-schema}countries c", true); + + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query); + + assertThat(queryEnhancer) // + .isNotInstanceOf(JSqlParserQueryEnhancer.class); + } + + @ParameterizedTest + @MethodSource("generatesCorrectQueryEnhancerUsingChoiceSource") + void generatesCorrectQueryEnhancerUsingChoice(String stringQuery, boolean isNative, + Class choice, Class expectedEnhancer) { + QueryEnhancerChoice queryEnhancerChoice = getQueryEnhancerChoice(choice); + + StringQuery query = new StringQuery(stringQuery, isNative, queryEnhancerChoice); + assertThat(query.getQueryEnhancer()) // + .isNotNull() // + .isInstanceOf(expectedEnhancer); + } + + static Stream generatesCorrectQueryEnhancerUsingChoiceSource() { + return Stream.of( // + Arguments.of("SELECT u FROM User u", true, DefaultQueryEnhancer.class, DefaultQueryEnhancer.class), // + Arguments.of("SELECT u FROM User u", true, JSqlParserQueryEnhancer.class, JSqlParserQueryEnhancer.class), // + Arguments.of("SELECT u FROM User u", true, MyCustomQueryEnhancer.class, MyCustomQueryEnhancer.class), // + + Arguments.of("SELECT u FROM com.diegok.User u", false, DefaultQueryEnhancer.class, DefaultQueryEnhancer.class), // + Arguments.of("SELECT u FROM com.diegok.User u", false, JSqlParserQueryEnhancer.class, + JSqlParserQueryEnhancer.class), // + Arguments.of("SELECT u FROM com.diegok.User u", false, MyCustomQueryEnhancer.class, MyCustomQueryEnhancer.class) // + ); + } + + private static QueryEnhancerChoice getQueryEnhancerChoice(Class choice) { + return new QueryEnhancerChoice() { + + @Override + public Class annotationType() { + return QueryEnhancerChoice.class; + } + + @Override + public Class value() { + return choice; + } + }; + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java index c082840c2a..209101ab85 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java @@ -419,14 +419,6 @@ void doesNotPrefixAliasedFunctionCallNameWithDots() { assertThat(getEnhancer(query).applySorting(sort, "m")).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)) // - .isInstanceOf(IllegalArgumentException.class); - } - @Test // DATAJPA-965, DATAJPA-970 void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpaces() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java index 4746f27106..e8a88ddb29 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java @@ -17,15 +17,21 @@ import static org.assertj.core.api.Assertions.*; +import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; 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.data.jpa.repository.query.StringQuery.InParameterBinding; import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding; import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; +import org.springframework.data.jpa.repository.sample.MyCustomQueryEnhancer; import org.springframework.data.repository.query.parser.Part.Type; /** @@ -566,6 +572,49 @@ void usingGreaterThanWithNamedParameter() { .containsExactly("age"); } + @Test + void usesNoneQueryEnhancerChoiceIfNotPresent() { + String queryString = "SELECT u FROM User u WHERE :age>u.age"; + + StringQuery query = new StringQuery(queryString, true, null); + assertThat(query.getQueryEnhancer()).isNotNull(); + assertThat(query.hasQueryEnhancerChoice()).isFalse(); + assertThat(query.getQueryEnhancerChoice()).isNull(); + } + + @ParameterizedTest + @MethodSource("usesCorrectQueryEnhancerChoiceSource") + void usesCorrectQueryEnhancerChoice(Class choice) { + String queryString = "SELECT u FROM User u WHERE :age>u.age"; + + QueryEnhancerChoice queryEnhancerChoice = getQueryEnhancerChoice(choice); + + StringQuery query = new StringQuery(queryString, true, queryEnhancerChoice); + assertThat(query.getQueryEnhancer()).isNotNull(); + assertThat(query.hasQueryEnhancerChoice()).isTrue(); + assertThat(query.getQueryEnhancerChoice()).isNotNull().extracting(QueryEnhancerChoice::value).isEqualTo(choice); + } + + static Stream usesCorrectQueryEnhancerChoiceSource() { + return Stream.of(Arguments.of(DefaultQueryEnhancer.class), Arguments.of(JSqlParserQueryEnhancer.class), + Arguments.of(MyCustomQueryEnhancer.class)); + } + + private static QueryEnhancerChoice getQueryEnhancerChoice(Class choice) { + return new QueryEnhancerChoice() { + + @Override + public Class annotationType() { + return QueryEnhancerChoice.class; + } + + @Override + public Class value() { + return choice; + } + }; + } + void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) { DeclaredQuery declaredQuery = DeclaredQuery.of(query, nativeQuery); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/MyCustomQueryEnhancer.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/MyCustomQueryEnhancer.java new file mode 100644 index 0000000000..f22a6e92d6 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/MyCustomQueryEnhancer.java @@ -0,0 +1,41 @@ +package org.springframework.data.jpa.repository.sample; + +import java.util.Set; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.QueryEnhancer; +import org.springframework.data.jpa.repository.query.QueryUtils; + +public class MyCustomQueryEnhancer extends QueryEnhancer { + + public MyCustomQueryEnhancer(DeclaredQuery query) { + super(query); + } + + @Override + public String applySorting(Sort sort, String alias) { + return QueryUtils.applySorting(getQuery().getQueryString(), sort, alias); + } + + @Override + public String detectAlias() { + return QueryUtils.detectAlias(getQuery().getQueryString()); + } + + @Override + public String createCountQueryFor(String countProjection) { + // we return this because we use this to test if the correct enhancer is used + return "Select distinct(1) From User u"; + } + + @Override + public String getProjection() { + return QueryUtils.getProjection(getQuery().getQueryString()); + } + + @Override + public Set getJoinAliases() { + return QueryUtils.getOuterJoinAliases(this.getQuery().getQueryString()); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 7bf350f9f2..3a32086617 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -27,6 +27,7 @@ import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.*; import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.data.jpa.repository.query.QueryEnhancerChoice; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; @@ -668,6 +669,9 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter // GH-2607 List findByAttributesContains(String attribute); + @Query(value = "SELECT c.* FROM SD_User c WHERE c.firstname = :#{#user.firstname}", nativeQuery = true) + List nativeQueryWithSpELStatement(@Param("user") User example); + // GH-2593 @Modifying @Query( @@ -690,6 +694,10 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter nativeQuery = true) int mergeNativeStatement(); + @Query(value = "Select u from User u") + @QueryEnhancerChoice(MyCustomQueryEnhancer.class) + Page findPageWithCustomQueryEnhancer(Pageable pageable); + interface RolesAndFirstname { String getFirstname();