From 4a3842ba9e43f7b919ca0e52f3bec8e11ddbd2ec Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Mon, 7 Aug 2023 17:01:25 -0500 Subject: [PATCH] Introduce flag to skip JSqlParser for a given custom query. See #2989 --- .../springframework/data/jpa/repository/Query.java | 8 ++++++++ .../query/AbstractStringBasedJpaQuery.java | 2 +- .../data/jpa/repository/query/DeclaredQuery.java | 4 ++++ .../query/ExpressionBasedStringQuery.java | 10 ++++++++-- .../data/jpa/repository/query/JpaQueryMethod.java | 6 ++++++ .../jpa/repository/query/QueryEnhancerFactory.java | 2 +- .../data/jpa/repository/query/StringQuery.java | 13 ++++++++++++- .../data/jpa/repository/UserRepositoryTests.java | 13 +++++++++++++ .../repository/query/QueryEnhancerUnitTests.java | 7 +++++++ .../data/jpa/repository/sample/UserRepository.java | 5 +++++ 10 files changed, 65 insertions(+), 5 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java index d626507f24..b381675b7d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java @@ -86,4 +86,12 @@ * @since 3.0 */ Class queryRewriter() default QueryRewriter.IdentityQueryRewriter.class; + + /** + * For native queries, indicate whether or not to skip the JSqlParser. + * + * @return + * @since 3.2 + */ + boolean skipJSql() default false; } 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 cf22cc9f40..8a0e9c6148 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 @@ -75,7 +75,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri this.evaluationContextProvider = evaluationContextProvider; this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser, - method.isNativeQuery()); + method.isNativeQuery(), method.skipJSql()); this.countQuery = Lazy.of(() -> { DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection()); 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 670e030fb2..54dd6f4a05 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 @@ -111,4 +111,8 @@ default boolean usesPaging() { default boolean isNativeQuery() { return false; } + + default boolean skipJSql() { + 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 3bee9c2d48..77e895f2b7 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 @@ -59,9 +59,15 @@ class ExpressionBasedStringQuery extends StringQuery { * @param parser must not be {@literal null}. * @param nativeQuery is a given query is native or not */ + public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, SpelExpressionParser parser, + boolean nativeQuery, boolean skipJSql) { + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), + skipJSql); + } + public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, SpelExpressionParser parser, boolean nativeQuery) { - super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query)); + this(query, metadata, parser, nativeQuery, false); } /** @@ -75,7 +81,7 @@ 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.skipJSql()); } /** 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 ed566ba52c..baf252bb5f 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 @@ -94,6 +94,7 @@ public class JpaQueryMethod extends QueryMethod { private final Lazy jpaEntityGraph; private final Lazy modifying; private final Lazy isNativeQuery; + private final Lazy skipJSql; private final Lazy isCollectionQuery; private final Lazy isProcedureQuery; private final Lazy> entityMetadata; @@ -136,6 +137,7 @@ protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionF return new JpaEntityGraph(entityGraph, getNamedQueryName()); }); this.isNativeQuery = Lazy.of(() -> getAnnotationValue("nativeQuery", Boolean.class)); + this.skipJSql = Lazy.of(() -> getAnnotationValue("skipJSql", 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())); @@ -387,6 +389,10 @@ boolean isNativeQuery() { return this.isNativeQuery.get(); } + boolean skipJSql() { + return this.skipJSql.get(); + } + @Override public String getNamedQueryName() { 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 74aa77e611..ea68845cbb 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 @@ -60,7 +60,7 @@ public static QueryEnhancer forQuery(DeclaredQuery query) { if (query.isNativeQuery()) { - if (jSqlParserPresent) { + if (jSqlParserPresent && !query.skipJSql()) { /* * If JSqlParser fails, throw some alert signaling that people should write a custom Impl. */ 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 afa68ff51c..2da0d324c1 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 @@ -65,6 +65,7 @@ class StringQuery implements DeclaredQuery { private final boolean containsPageableInSpel; private final boolean usesJdbcStyleParameters; private final boolean isNative; + private final boolean skipJSql; private final QueryEnhancer queryEnhancer; /** @@ -73,11 +74,12 @@ class StringQuery implements DeclaredQuery { * @param query must not be {@literal null} or empty. */ @SuppressWarnings("deprecation") - StringQuery(String query, boolean isNative) { + StringQuery(String query, boolean isNative, boolean skipJSql) { Assert.hasText(query, "Query must not be null or empty"); this.isNative = isNative; + this.skipJSql = skipJSql; this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); @@ -92,6 +94,10 @@ class StringQuery implements DeclaredQuery { this.hasConstructorExpression = this.queryEnhancer.hasConstructorExpression(); } + StringQuery(String query, boolean isNative) { + this(query, isNative, false); + } + /** * Returns whether we have found some like bindings. */ @@ -157,6 +163,11 @@ public boolean isNativeQuery() { return isNative; } + @Override + public boolean skipJSql() { + return skipJSql; + } + /** * A parser that extracts the parameter bindings from a given query string. * 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 9264185849..5800f65f8e 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 @@ -2914,6 +2914,19 @@ void supportsProjectionsWithNativeQueries() { assertThat(result.getLastname()).isEqualTo(user.getLastname()); } + @Test // GH-2989 + void supportsProjectionsWithNativeQueriesSkippingJSql() { + + flushTestUsers(); + + User user = repository.findAll().get(0); + + NameOnly result = repository.findByNativeQueryWithNoJSql(user.getId()); + + assertThat(result.getFirstname()).isEqualTo(user.getFirstname()); + assertThat(result.getLastname()).isEqualTo(user.getLastname()); + } + @Test // DATAJPA-1248 void supportsProjectionsWithNativeQueriesAndCamelCaseProperty() { 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 6140b313bf..c5b1e65ea3 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 @@ -625,6 +625,13 @@ void modifyingQueriesAreDetectedCorrectly() { assertThat(QueryEnhancerFactory.forQuery(modiQuery).createCountQueryFor()).isEqualToIgnoringCase(modifyingQuery); } + @Test // GH-2989 + void skippingJSqlShouldRevertToDefaultQueryEnhancer() { + + assertThat(getEnhancer(new StringQuery(QUERY, true, false))).isInstanceOf(JSqlParserQueryEnhancer.class); + assertThat(getEnhancer(new StringQuery(QUERY, true, true))).isInstanceOf(DefaultQueryEnhancer.class); + } + @ParameterizedTest // GH-2593 @MethodSource("insertStatementIsProcessedSameAsDefaultSource") void insertStatementIsProcessedSameAsDefault(String insertQuery) { 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 c9a342538f..5b07d050cb 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 @@ -585,6 +585,11 @@ List findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity @Query(value = "SELECT firstname, lastname FROM SD_User WHERE id = ?1", nativeQuery = true) NameOnly findByNativeQuery(Integer id); + // GH-2989 + @Query(value = "SELECT firstname, lastname FROM SD_User WHERE id = ?1", nativeQuery = true, skipJSql = true) + NameOnly findByNativeQueryWithNoJSql(Integer id); + + // DATAJPA-1248 @Query(value = "SELECT emailaddress FROM SD_User WHERE id = ?1", nativeQuery = true) EmailOnly findEmailOnlyByNativeQuery(Integer id);