From d28e2ed177d7cfa0d48afb0415001a904abccdd1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 10:10:07 +0100 Subject: [PATCH 01/94] Prepare next development iteration. See #3680 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 8af40fe698..fbed7ea5a5 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 5dcbbb69fd..0bdf2c8e7e 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 38a234cb71..af5244a230 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 475a520c21..bb7829dcaf 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT ../pom.xml From 5a2cdb35a4081105f66ebebbec6d5d08c1b222ae Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 10:10:10 +0100 Subject: [PATCH 02/94] After release cleanups. See #3680 --- Jenkinsfile | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 915e46ddb7..f87caaa0aa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { triggers { pollSCM 'H/10 * * * *' - upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS) + upstream(upstreamProjects: "spring-data-commons/4.0.x", threshold: hudson.model.Result.SUCCESS) } options { diff --git a/pom.xml b/pom.xml index fbed7ea5a5..887a96a4db 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT @@ -41,7 +41,7 @@ 5.0 9.1.0 42.7.4 - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT 0.10.3 org.hibernate From 0fccd8d82df80b433b53dae2cfbcab47c3cc5976 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 11:06:32 +0100 Subject: [PATCH 03/94] Adopt to deprecation removals in Commons. Closes #3683 --- .../EnversRevisionRepositoryFactoryBean.java | 7 +++--- .../jpa/repository/query/JpaParameters.java | 19 -------------- .../query/JpaQueryLookupStrategy.java | 25 ------------------- .../JavaConfigUserRepositoryTests.java | 12 ++++----- .../JpaQueryLookupStrategyUnitTests.java | 17 +++++++------ ...QuerydslJpaPredicateExecutorUnitTests.java | 6 ++--- .../test/resources/application-context.xml | 6 +---- 7 files changed, 23 insertions(+), 69 deletions(-) diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java index 9f40559ca4..825a1d1a4e 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java @@ -15,11 +15,12 @@ */ package org.springframework.data.envers.repository.support; -import java.util.Optional; - import jakarta.persistence.EntityManager; +import java.util.Optional; + import org.hibernate.envers.DefaultRevisionEntity; + import org.springframework.beans.factory.FactoryBean; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; @@ -94,7 +95,7 @@ public RevisionRepositoryFactory(EntityManager entityManager, Class revisionE @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - Object fragmentImplementation = getTargetRepositoryViaReflection( // + Object fragmentImplementation = instantiateClass( // EnversRevisionRepositoryImpl.class, // getEntityInformation(metadata.getDomainType()), // revisionEntityInformation, // diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java index b3fc5526f5..6d5244e95a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java @@ -90,25 +90,6 @@ public static class JpaParameter extends Parameter { private final @Nullable Temporal annotation; private @Nullable TemporalType temporalType; - /** - * Creates a new {@link JpaParameter}. - * - * @param parameter must not be {@literal null}. - * @deprecated since 3.2.1 - */ - @Deprecated(since = "3.2.1", forRemoval = true) - protected JpaParameter(MethodParameter parameter) { - - super(parameter); - - this.annotation = parameter.getParameterAnnotation(Temporal.class); - this.temporalType = null; - if (!isDateParameter() && hasTemporalParamAnnotation()) { - throw new IllegalArgumentException( - Temporal.class.getSimpleName() + " annotation is only allowed on Date parameter"); - } - } - /** * Creates a new {@link JpaParameter}. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 6d25af839a..e6ca9f256b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -22,7 +22,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.ProjectionFactory; @@ -31,8 +30,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.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; @@ -261,28 +258,6 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer } } - /** - * Creates a {@link QueryLookupStrategy} for the given {@link EntityManager} and {@link Key}. - * - * @param em must not be {@literal null}. - * @param queryMethodFactory must not be {@literal null}. - * @param key may be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @param escape must not be {@literal null}. - * @deprecated since 3.4, use - * {@link #create(EntityManager, JpaQueryMethodFactory, Key, ValueExpressionDelegate, QueryRewriterProvider, EscapeCharacter)} - * instead. - */ - @Deprecated(since = "3.4") - public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider, - QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) { - return create(em, queryMethodFactory, key, - new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), - evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionDelegate.create()), - queryRewriterProvider, escape); - } - /** * Creates a {@link QueryLookupStrategy} for the given {@link EntityManager} and {@link Key}. * diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java index d87b9e152c..f40877701e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JavaConfigUserRepositoryTests.java @@ -15,14 +15,15 @@ */ package org.springframework.data.jpa.repository; -import java.io.IOException; -import java.util.Collections; - import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.io.IOException; +import java.util.Collections; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; @@ -42,8 +43,7 @@ import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.support.AnnotationConfigContextLoader; @@ -72,7 +72,7 @@ public EvaluationContextExtension sampleEvaluationContextExtension() { @Bean public UserRepository userRepository() throws Exception { - QueryMethodEvaluationContextProvider evaluationContextProvider = new ExtensionAwareQueryMethodEvaluationContextProvider( + ExtensionAwareEvaluationContextProvider evaluationContextProvider = new ExtensionAwareEvaluationContextProvider( applicationContext); JpaRepositoryFactoryBean factory = new JpaRepositoryFactoryBean<>( diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index a8205cea35..861272154b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -33,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.beans.factory.BeanFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -47,8 +48,8 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Unit tests for {@link JpaQueryLookupStrategy}. @@ -63,7 +64,7 @@ @MockitoSettings(strictness = Strictness.LENIENT) class JpaQueryLookupStrategyUnitTests { - private static final QueryMethodEvaluationContextProvider EVALUATION_CONTEXT_PROVIDER = QueryMethodEvaluationContextProvider.DEFAULT; + private static final ValueExpressionDelegate VALUE_EXPRESSION_DELEGATE = ValueExpressionDelegate.create(); @Mock EntityManager em; @Mock EntityManagerFactory emf; @@ -89,7 +90,7 @@ void setUp() { void invalidAnnotatedQueryCausesException() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("findByFoo", String.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -101,7 +102,7 @@ void invalidAnnotatedQueryCausesException() throws Exception { void considersNamedCountQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo"); @@ -123,7 +124,7 @@ void considersNamedCountQuery() throws Exception { void considersNamedCountOnStringQueryQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo"); @@ -142,7 +143,7 @@ void considersNamedCountOnStringQueryQuery() throws Exception { void prefersDeclaredQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("annotatedQueryWithQueryAndQueryName"); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -155,7 +156,7 @@ void prefersDeclaredQuery() throws Exception { void namedQueryWithSortShouldThrowIllegalStateException() throws NoSuchMethodException { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("customNamedQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -180,7 +181,7 @@ void noQueryShouldNotBeInvoked() { void customQueryWithQuestionMarksShouldWork() throws NoSuchMethodException { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - EVALUATION_CONTEXT_PROVIDER, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); Method namedMethod = UserRepository.class.getMethod("customQueryWithQuestionMarksAndNamedParam", String.class); RepositoryMetadata namedMetadata = new DefaultRepositoryMetadata(UserRepository.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java index 0eecd481ae..d988dc72d5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java @@ -213,7 +213,7 @@ void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequestAndQSort QUser user = QUser.user; Page page = predicateExecutor.findAll(user.firstname.isNotNull(), - new QPageRequest(0, 10, new QSort(user.firstname.asc()))); + QPageRequest.of(0, 10, new QSort(user.firstname.asc()))); assertThat(page.getContent()).containsExactly(carter, dave, oliver); } @@ -224,7 +224,7 @@ void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequest() { QUser user = QUser.user; Page page = predicateExecutor.findAll(user.firstname.isNotNull(), - new QPageRequest(0, 10, user.firstname.asc())); + QPageRequest.of(0, 10, user.firstname.asc())); assertThat(page.getContent()).containsExactly(carter, dave, oliver); } @@ -238,7 +238,7 @@ void findBySpecificationWithSortByQueryDslOrderSpecifierForAssociationShouldGene QUser user = QUser.user; Page page = predicateExecutor.findAll(user.firstname.isNotNull(), - new QPageRequest(0, 10, user.manager.firstname.asc())); + QPageRequest.of(0, 10, user.manager.firstname.asc())); assertThat(page.getContent()).containsExactly(carter, dave, oliver); } diff --git a/spring-data-jpa/src/test/resources/application-context.xml b/spring-data-jpa/src/test/resources/application-context.xml index 1bd58b22cd..3f10133b5d 100644 --- a/spring-data-jpa/src/test/resources/application-context.xml +++ b/spring-data-jpa/src/test/resources/application-context.xml @@ -25,12 +25,10 @@ - - + - @@ -39,8 +37,6 @@ - - From e6b3b53ac18aec1adebd8e0e670ba9845eb438ef Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 23 Aug 2024 10:23:22 +0200 Subject: [PATCH 04/94] Replace derived `CriteriaQuery` with String-based queries. Introduce new DSL to construct JPQL queries. Refactor ParameterMetadata to PartTreeParameterBinding. Disable Keyset pagination with projections for Eclipselink as Eclipselink doesn't consider type hints for JPQL queries. Closes #3588 Original pull request: #3653 --- .../repository/query/AbstractJpaQuery.java | 4 +- .../query/AbstractStringBasedJpaQuery.java | 10 +- ...bernateJpaParametersParameterAccessor.java | 2 +- .../query/JpaCountQueryCreator.java | 44 +- .../query/JpaKeysetScrollQueryCreator.java | 67 +- .../query/JpaParametersParameterAccessor.java | 9 +- .../jpa/repository/query/JpaQueryCreator.java | 388 +++--- .../repository/query/JpqlQueryBuilder.java | 1219 +++++++++++++++++ .../repository/query/JpqlQueryCreator.java | 34 + .../data/jpa/repository/query/JpqlUtils.java | 82 ++ .../query/KeysetScrollDelegate.java | 23 + .../query/KeysetScrollSpecification.java | 88 +- .../data/jpa/repository/query/NamedQuery.java | 11 +- .../jpa/repository/query/ParameterBinder.java | 16 +- .../query/ParameterBinderFactory.java | 34 +- .../repository/query/ParameterBinding.java | 180 ++- .../query/ParameterMetadataProvider.java | 141 +- .../repository/query/PartTreeJpaQuery.java | 230 ++-- .../query/QueryParameterSetter.java | 227 ++- .../query/QueryParameterSetterFactory.java | 137 +- .../data/jpa/repository/query/QueryUtils.java | 4 +- .../query/StoredProcedureJpaQuery.java | 5 +- .../support/JpqlQueryTemplates.java | 49 + .../EclipseLinkUserRepositoryFinderTests.java | 4 + .../jpa/repository/UserRepositoryTests.java | 9 + .../JpaCountQueryCreatorIntegrationTests.java | 28 +- .../JpaKeysetScrollQueryCreatorTests.java | 95 ++ .../JpaParametersParameterAccessorTests.java | 2 +- ...rIndexedQueryParameterSetterUnitTests.java | 20 +- .../query/ParameterBinderUnitTests.java | 6 +- .../ParameterExpressionProviderTests.java | 70 - ...meterMetadataProviderIntegrationTests.java | 15 +- .../ParameterMetadataProviderUnitTests.java | 16 +- .../PartTreeJpaQueryIntegrationTests.java | 26 +- .../QueryParameterSetterFactoryUnitTests.java | 21 +- .../modules/ROOT/pages/jpa/query-methods.adoc | 2 +- 36 files changed, 2550 insertions(+), 768 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index fb9821c184..641c16190d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -242,8 +242,8 @@ private Query applyLockMode(Query query, JpaQueryMethod method) { return lockModeType == null ? query : query.setLockMode(lockModeType); } - protected ParameterBinder createBinder() { - return ParameterBinderFactory.createBinder(getQueryMethod().getParameters()); + ParameterBinder createBinder() { + return ParameterBinderFactory.createBinder(getQueryMethod().getParameters(), false); } protected Query createQuery(JpaParametersParameterAccessor parameters) { 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 91624e2631..35e6cce708 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 @@ -51,7 +51,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final DeclaredQuery query; private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; - private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; private final Lazy countParameterBinder; @@ -124,11 +123,9 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { String sortedQueryString = getSortedQueryString(sort, returnedType); Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), returnedType); - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query); - // 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, metadata, accessor); + return parameterBinder.get().bindAndPrepare(query, accessor); } String getSortedQueryString(Sort sort, ReturnedType returnedType) { @@ -157,9 +154,8 @@ protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) { ? em.createNativeQuery(queryStringToUse) // : em.createQuery(queryStringToUse, Long.class); - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryString, query); - - countParameterBinder.get().bind(metadata.withQuery(query), accessor, QueryParameterSetter.ErrorHandling.LENIENT); + countParameterBinder.get().bind(new QueryParameterSetter.BindableQuery(query), accessor, + QueryParameterSetter.ErrorHandling.LENIENT); return query; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java index 37b06f0744..6020c50fa1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java @@ -51,7 +51,7 @@ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAcce * @param values must not be {@literal null}. * @param em must not be {@literal null}. */ - HibernateJpaParametersParameterAccessor(Parameters parameters, Object[] values, EntityManager em) { + HibernateJpaParametersParameterAccessor(JpaParameters parameters, Object[] values, EntityManager em) { super(parameters, values); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java index d9b69f362f..886cb5b4dd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java @@ -15,16 +15,12 @@ */ package org.springframework.data.jpa.repository.query; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; +import jakarta.persistence.EntityManager; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.lang.Nullable; /** * Special {@link JpaQueryCreator} that creates a count projecting query. @@ -37,39 +33,33 @@ public class JpaCountQueryCreator extends JpaQueryCreator { private final boolean distinct; + private final ReturnedType returnedType; /** - * Creates a new {@link JpaCountQueryCreator}. + * Creates a new {@link JpaCountQueryCreator} * * @param tree - * @param type - * @param builder + * @param returnedType * @param provider + * @param templates + * @param em */ - public JpaCountQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder, - ParameterMetadataProvider provider) { + public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider, + JpqlQueryTemplates templates, EntityManager em) { - super(tree, type, builder, provider); + super(tree, returnedType, provider, templates, em); this.distinct = tree.isDistinct(); + this.returnedType = returnedType; } @Override - protected CriteriaQuery createCriteriaQuery(CriteriaBuilder builder, ReturnedType type) { - return builder.createQuery(Long.class); - } - - @Override - @SuppressWarnings("unchecked") - protected CriteriaQuery complete(@Nullable Predicate predicate, Sort sort, - CriteriaQuery query, CriteriaBuilder builder, Root root) { - - CriteriaQuery select = query.select(getCountQuery(builder, root)); - return predicate == null ? select : select.where(predicate); - } + protected JpqlQueryBuilder.Select buildQuery(Sort sort) { + JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(returnedType.getDomainType()); + if (this.distinct) { + selectStep = selectStep.distinct(); + } - @SuppressWarnings("rawtypes") - private Expression getCountQuery(CriteriaBuilder builder, Root root) { - return distinct ? builder.countDistinct(root) : builder.count(root); + return selectStep.count(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java index c6f3d9b4e6..7d455e49df 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java @@ -15,16 +15,19 @@ */ package org.springframework.data.jpa.repository.query; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.lang.Nullable; @@ -39,35 +42,67 @@ class JpaKeysetScrollQueryCreator extends JpaQueryCreator { private final JpaEntityInformation entityInformation; private final KeysetScrollPosition scrollPosition; + private final ParameterMetadataProvider provider; + private final List syntheticBindings = new ArrayList<>(); - public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder, - ParameterMetadataProvider provider, JpaEntityInformation entityInformation, - KeysetScrollPosition scrollPosition) { + public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider, + JpqlQueryTemplates templates, JpaEntityInformation entityInformation, KeysetScrollPosition scrollPosition, + EntityManager em) { - super(tree, type, builder, provider); + super(tree, type, provider, templates, em); this.entityInformation = entityInformation; this.scrollPosition = scrollPosition; + this.provider = provider; } @Override - protected CriteriaQuery complete(@Nullable Predicate predicate, Sort sort, CriteriaQuery query, - CriteriaBuilder builder, Root root) { + public List getBindings() { + + List partTreeBindings = super.getBindings(); + List bindings = new ArrayList<>(partTreeBindings.size() + this.syntheticBindings.size()); + bindings.addAll(partTreeBindings); + bindings.addAll(this.syntheticBindings); + + return bindings; + } + + @Override + protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) { KeysetScrollSpecification keysetSpec = new KeysetScrollSpecification<>(scrollPosition, sort, entityInformation); - Predicate keysetPredicate = keysetSpec.createPredicate(root, builder); - CriteriaQuery queryToUse = super.complete(predicate, keysetSpec.sort(), query, builder, root); + JpqlQueryBuilder.Select query = buildQuery(keysetSpec.sort()); + + AtomicInteger counter = new AtomicInteger(provider.getBindings().size()); + JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), value -> { + + syntheticBindings.add(provider.nextSynthetic(value, scrollPosition)); + return JpqlQueryBuilder.expression(render(counter.incrementAndGet())); + }); + JpqlQueryBuilder.Predicate predicateToUse = getPredicate(predicate, keysetPredicate); + + if (predicateToUse != null) { + return query.where(predicateToUse); + } + + return query; + } + + @Nullable + private static JpqlQueryBuilder.Predicate getPredicate(@Nullable JpqlQueryBuilder.Predicate predicate, + @Nullable JpqlQueryBuilder.Predicate keysetPredicate) { if (keysetPredicate != null) { - if (queryToUse.getRestriction() != null) { - return queryToUse.where(builder.and(queryToUse.getRestriction(), keysetPredicate)); + if (predicate != null) { + return predicate.nest().and(keysetPredicate.nest()); + } else { + return keysetPredicate; } - return queryToUse.where(keysetPredicate); } - return queryToUse; + return predicate; } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java index e222439a22..2093e0d3d6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java @@ -31,14 +31,21 @@ */ public class JpaParametersParameterAccessor extends ParametersParameterAccessor { + private final JpaParameters parameters; + /** * Creates a new {@link ParametersParameterAccessor}. * * @param parameters must not be {@literal null}. * @param values must not be {@literal null}. */ - public JpaParametersParameterAccessor(Parameters parameters, Object[] values) { + public JpaParametersParameterAccessor(JpaParameters parameters, Object[] values) { super(parameters, values); + this.parameters = parameters; + } + + public JpaParameters getParameters() { + return parameters; } @Nullable diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 73bafaf249..44192fac5c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -15,28 +15,28 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryUtils.*; import static org.springframework.data.repository.query.parser.Part.Type.*; -import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.ParameterExpression; -import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import jakarta.persistence.criteria.Selection; +import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.SingularAttribute; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; -import java.util.stream.Collectors; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin; +import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; @@ -58,54 +58,50 @@ * @author Greg Turnquist * @author Jinmyeong Kim */ -public class JpaQueryCreator extends AbstractQueryCreator, Predicate> { +class JpaQueryCreator extends AbstractQueryCreator implements JpqlQueryCreator { - private final CriteriaBuilder builder; - private final Root root; - private final CriteriaQuery query; - private final ParameterMetadataProvider provider; private final ReturnedType returnedType; + private final ParameterMetadataProvider provider; + private final JpqlQueryTemplates templates; private final PartTree tree; private final EscapeCharacter escape; + private final EntityType entityType; + private final From from; + private final JpqlQueryBuilder.Entity entity; /** * Create a new {@link JpaQueryCreator}. * * @param tree must not be {@literal null}. * @param type must not be {@literal null}. - * @param builder must not be {@literal null}. + * @param templates must not be {@literal null}. * @param provider must not be {@literal null}. + * @param em must not be {@literal null}. */ - public JpaQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder, - ParameterMetadataProvider provider) { + public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider, + JpqlQueryTemplates templates, EntityManager em) { super(tree); this.tree = tree; - - CriteriaQuery criteriaQuery = createCriteriaQuery(builder, type); - - this.builder = builder; - this.query = criteriaQuery.distinct(tree.isDistinct() && !tree.isCountProjection()); - this.root = query.from(type.getDomainType()); - this.provider = provider; this.returnedType = type; + this.provider = provider; + this.templates = templates; this.escape = provider.getEscape(); + this.entityType = em.getMetamodel().entity(type.getDomainType()); + this.from = em.getCriteriaBuilder().createQuery().from(type.getDomainType()); + this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType()); } - /** - * Creates the {@link CriteriaQuery} to apply predicates on. - * - * @param builder will never be {@literal null}. - * @param type will never be {@literal null}. - * @return must not be {@literal null}. - */ - protected CriteriaQuery createCriteriaQuery(CriteriaBuilder builder, ReturnedType type) { + From getFrom() { + return from; + } - Class typeToRead = tree.isDelete() ? type.getDomainType() : type.getTypeToRead(); + JpqlQueryBuilder.Entity getEntity() { + return entity; + } - return (typeToRead == null) || tree.isExistsProjection() // - ? builder.createTupleQuery() // - : builder.createQuery(typeToRead); + public boolean useTupleQuery() { + return returnedType.needsCustomConstruction() && returnedType.getReturnedType().isInterface(); } /** @@ -113,102 +109,168 @@ protected CriteriaQuery createCriteriaQuery(CriteriaBuilder bu * * @return the parameterExpressions */ - public List> getParameterExpressions() { - return provider.getExpressions(); + public List getBindings() { + return provider.getBindings(); } @Override - protected Predicate create(Part part, Iterator iterator) { - return toPredicate(part, root); + public ParameterBinder getBinder() { + return ParameterBinderFactory.createBinder(provider.getParameters(), getBindings()); } @Override - protected Predicate and(Part part, Predicate base, Iterator iterator) { - return builder.and(base, toPredicate(part, root)); + protected JpqlQueryBuilder.Predicate create(Part part, Iterator iterator) { + return toPredicate(part); } @Override - protected Predicate or(Predicate base, Predicate predicate) { - return builder.or(base, predicate); + protected JpqlQueryBuilder.Predicate and(Part part, JpqlQueryBuilder.Predicate base, Iterator iterator) { + return base.and(toPredicate(part)); + } + + @Override + protected JpqlQueryBuilder.Predicate or(JpqlQueryBuilder.Predicate base, JpqlQueryBuilder.Predicate predicate) { + return base.or(predicate); } /** - * Finalizes the given {@link Predicate} and applies the given sort. Delegates to - * {@link #complete(Predicate, Sort, CriteriaQuery, CriteriaBuilder, Root)} and hands it the current - * {@link CriteriaQuery} and {@link CriteriaBuilder}. + * Finalizes the given {@link Predicate} and applies the given sort. Delegates to {@link #buildQuery(Sort)} and hands + * it the current {@link JpqlQueryBuilder.Predicate}. */ @Override - protected final CriteriaQuery complete(Predicate predicate, Sort sort) { - return complete(predicate, sort, query, builder, root); + protected final String complete(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) { + + JpqlQueryBuilder.AbstractJpqlQuery query = createQuery(predicate, sort); + return query.render(); + } + + protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) { + + JpqlQueryBuilder.Select query = buildQuery(sort); + + if (predicate != null) { + return query.where(predicate); + } + + return query; } /** - * Template method to finalize the given {@link Predicate} using the given {@link CriteriaQuery} and - * {@link CriteriaBuilder}. + * Template method to build a query stub using the given {@link Sort}. * - * @param predicate * @param sort - * @param query - * @param builder * @return */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected CriteriaQuery complete(@Nullable Predicate predicate, Sort sort, - CriteriaQuery query, CriteriaBuilder builder, Root root) { + protected JpqlQueryBuilder.Select buildQuery(Sort sort) { + + JpqlQueryBuilder.Select select = doSelect(sort); + + if (tree.isDelete() || tree.isCountProjection()) { + return select; + } + + for (Sort.Order order : sort) { + + JpqlQueryBuilder.Expression expression; + QueryUtils.checkSortExpression(order); + + try { + expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, + PropertyPath.from(order.getProperty(), entityType.getJavaType()))); + } catch (PropertyReferenceException e) { + + if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) { + expression = JpqlQueryBuilder.expression(order.getProperty()); + } else { + throw e; + } + } + + if (order.isIgnoreCase()) { + expression = JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expression); + } + + select.orderBy(JpqlQueryBuilder.orderBy(expression, order)); + } + + return select; + } + + private JpqlQueryBuilder.Select doSelect(Sort sort) { + + JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(entity); + + if (tree.isDelete()) { + return selectStep.entity(); + } + + if (tree.isDistinct()) { + selectStep = selectStep.distinct(); + } if (returnedType.needsCustomConstruction()) { Collection requiredSelection = getRequiredSelection(sort, returnedType); - List> selections = new ArrayList<>(); - - for (String property : requiredSelection) { - PropertyPath path = PropertyPath.from(property, returnedType.getDomainType()); - selections.add(toExpressionRecursively(root, path, true).alias(property)); + List paths = new ArrayList<>(requiredSelection.size()); + for (String selection : requiredSelection) { + paths.add( + JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(selection, from.getJavaType()), true)); } - Class typeToRead = returnedType.getReturnedType(); + if (useTupleQuery()) { - query = typeToRead.isInterface() // - ? query.multiselect(selections) // - : query.select((Selection) builder.construct(typeToRead, // - selections.toArray(new Selection[0]))); + return selectStep.select(paths); + } else { + return selectStep.instantiate(returnedType.getReturnedType(), paths); + } + } - } else if (tree.isExistsProjection()) { + if (tree.isExistsProjection()) { - if (root.getModel().hasSingleIdAttribute()) { + if (entityType.hasSingleIdAttribute()) { - SingularAttribute id = root.getModel().getId(root.getModel().getIdType().getJavaType()); - query = query.multiselect(root.get((SingularAttribute) id).alias(id.getName())); + SingularAttribute id = entityType.getId(entityType.getIdType().getJavaType()); + return selectStep.select( + JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(id.getName(), from.getJavaType()), true)); } else { - query = query.multiselect(root.getModel().getIdClassAttributes().stream()// - .map(it -> (Selection) root.get((SingularAttribute) it).alias(it.getName())) - .collect(Collectors.toList())); + List paths = entityType.getIdClassAttributes().stream()// + .map(it -> JpqlUtils.toExpressionRecursively(entity, from, + PropertyPath.from(it.getName(), from.getJavaType()), true)) + .toList(); + return selectStep.select(paths); } + } + if (tree.isCountProjection()) { + return selectStep.count(); } else { - query = query.select((Root) root); + return selectStep.entity(); } - - CriteriaQuery select = query.orderBy(QueryUtils.toOrders(sort, root, builder)); - return predicate == null ? select : select.where(predicate); } Collection getRequiredSelection(Sort sort, ReturnedType returnedType) { return returnedType.getInputProperties(); } + String render(ParameterBinding binding) { + return render(binding.getRequiredPosition()); + } + + String render(int position) { + return "?" + position; + } + /** * Creates a {@link Predicate} from the given {@link Part}. * * @param part - * @param root * @return */ - private Predicate toPredicate(Part part, Root root) { - return new PredicateBuilder(part, root).build(); + private JpqlQueryBuilder.Predicate toPredicate(Part part) { + return new PredicateBuilder(part).build(); } /** @@ -217,24 +279,20 @@ private Predicate toPredicate(Part part, Root root) { * @author Phil Webb * @author Oliver Gierke */ - @SuppressWarnings({ "unchecked", "rawtypes" }) private class PredicateBuilder { private final Part part; - private final Root root; /** - * Creates a new {@link PredicateBuilder} for the given {@link Part} and {@link Root}. + * Creates a new {@link PredicateBuilder} for the given {@link Part}. * * @param part must not be {@literal null}. - * @param root must not be {@literal null}. */ - public PredicateBuilder(Part part, Root root) { + public PredicateBuilder(Part part) { Assert.notNull(part, "Part must not be null"); - Assert.notNull(root, "Root must not be null"); + this.part = part; - this.root = root; } /** @@ -242,83 +300,85 @@ public PredicateBuilder(Part part, Root root) { * * @return */ - public Predicate build() { + public JpqlQueryBuilder.Predicate build() { PropertyPath property = part.getProperty(); Type type = part.getType(); + PathAndOrigin pas = JpqlUtils.toExpressionRecursively(entity, from, property); + JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas); + JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas)); + switch (type) { case BETWEEN: - ParameterMetadata first = provider.next(part); - ParameterMetadata second = provider.next(part); - return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression()); + PartTreeParameterBinding first = provider.next(part); + ParameterBinding second = provider.next(part); + return where.between(render(first), render(second)); case AFTER: case GREATER_THAN: - return builder.greaterThan(getComparablePath(root, part), - provider.next(part, Comparable.class).getExpression()); + return where.gt(render(provider.next(part))); case GREATER_THAN_EQUAL: - return builder.greaterThanOrEqualTo(getComparablePath(root, part), - provider.next(part, Comparable.class).getExpression()); + return where.gte(render(provider.next(part))); case BEFORE: case LESS_THAN: - return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression()); + return where.lt(render(provider.next(part))); case LESS_THAN_EQUAL: - return builder.lessThanOrEqualTo(getComparablePath(root, part), - provider.next(part, Comparable.class).getExpression()); + return where.lte(render(provider.next(part))); case IS_NULL: - return getTypedPath(root, part).isNull(); + return where.isNull(); case IS_NOT_NULL: - return getTypedPath(root, part).isNotNull(); + return where.isNotNull(); case NOT_IN: - // cast required for eclipselink workaround, see DATAJPA-433 - return upperIfIgnoreCase(getTypedPath(root, part)) - .in((Expression>) provider.next(part, Collection.class).getExpression()).not(); + return whereIgnoreCase.notIn(render(provider.next(part, Collection.class))); case IN: - // cast required for eclipselink workaround, see DATAJPA-433 - return upperIfIgnoreCase(getTypedPath(root, part)) - .in((Expression>) provider.next(part, Collection.class).getExpression()); + return whereIgnoreCase.in(render(provider.next(part, Collection.class))); case STARTING_WITH: case ENDING_WITH: case CONTAINING: case NOT_CONTAINING: if (property.getLeafProperty().isCollection()) { + where = JpqlQueryBuilder.where(entity, property); - Expression> propertyExpression = traversePath(root, property); - ParameterExpression parameterExpression = provider.next(part).getExpression(); - - // Can't just call .not() in case of negation as EclipseLink chokes on that. - return type.equals(NOT_CONTAINING) // - ? isNotMember(builder, parameterExpression, propertyExpression) // - : isMember(builder, parameterExpression, propertyExpression); + return type.equals(NOT_CONTAINING) ? where.notMemberOf(render(provider.next(part))) + : where.memberOf(render(provider.next(part))); } case LIKE: case NOT_LIKE: - Expression stringPath = getTypedPath(root, part); - Expression propertyExpression = upperIfIgnoreCase(stringPath); - Expression parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression()); - Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter()); - return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like; + + PartTreeParameterBinding parameter = provider.next(part, String.class); + JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(), + JpqlQueryBuilder.parameter(render(parameter))); + // Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter()); + String escapeChar = Character.toString(escape.getEscapeCharacter()); + return + + type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) + ? whereIgnoreCase.notLike(parameterExpression, escapeChar) + : whereIgnoreCase.like(parameterExpression, escapeChar); case TRUE: - Expression truePath = getTypedPath(root, part); - return builder.isTrue(truePath); + return where.isTrue(); case FALSE: - Expression falsePath = getTypedPath(root, part); - return builder.isFalse(falsePath); + return where.isFalse(); case SIMPLE_PROPERTY: + PartTreeParameterBinding simple = provider.next(part); + + if (simple.isIsNullParameter()) { + return where.isNull(); + } + + return whereIgnoreCase.eq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(simple)))); case NEGATING_SIMPLE_PROPERTY: - ParameterMetadata expression = provider.next(part); - Expression path = getTypedPath(root, part); + PartTreeParameterBinding negating = provider.next(part); - if (expression.isIsNullParameter()) { - return type.equals(SIMPLE_PROPERTY) ? path.isNull() : path.isNotNull(); - } else { - return type.equals(SIMPLE_PROPERTY) - ? builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())) - : builder.notEqual(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())); + if (negating.isIsNullParameter()) { + return where.isNotNull(); } + + return whereIgnoreCase + .neq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(negating)))); case IS_EMPTY: case IS_NOT_EMPTY: @@ -326,77 +386,69 @@ public Predicate build() { throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties"); } - Expression> collectionPath = traversePath(root, property); - return type.equals(IS_NOT_EMPTY) ? builder.isNotEmpty(collectionPath) : builder.isEmpty(collectionPath); + where = JpqlQueryBuilder.where(entity, property); + return type.equals(IS_NOT_EMPTY) ? where.isNotEmpty() : where.isEmpty(); default: throw new IllegalArgumentException("Unsupported keyword " + type); } } - private Predicate isMember(CriteriaBuilder builder, Expression parameter, - Expression> property) { - return builder.isMember(parameter, property); + /** + * Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part} + * requires ignoring case. + * + * @param path must not be {@literal null}. + * @return + */ + private JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.Origin source, PropertyPath path) { + return potentiallyIgnoreCase(path, JpqlQueryBuilder.expression(source, path)); } - private Predicate isNotMember(CriteriaBuilder builder, Expression parameter, - Expression> property) { - return builder.isNotMember(parameter, property); + /** + * Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part} + * requires ignoring case. + * + * @param path must not be {@literal null}. + * @return + */ + private JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin pas) { + return potentiallyIgnoreCase(pas.path(), JpqlQueryBuilder.expression(pas)); } /** * Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part} * requires ignoring case. * - * @param expression must not be {@literal null}. * @return */ - private Expression upperIfIgnoreCase(Expression expression) { + private JpqlQueryBuilder.Expression potentiallyIgnoreCase(PropertyPath path, + JpqlQueryBuilder.Expression expressionValue) { switch (part.shouldIgnoreCase()) { case ALWAYS: - Assert.state(canUpperCase(expression), "Unable to ignore case of " + expression.getJavaType().getName() + Assert.isTrue(canUpperCase(path), "Unable to ignore case of " + path.getType().getName() + " types, the property '" + part.getProperty().getSegment() + "' must reference a String"); - return (Expression) builder.upper((Expression) expression); + return JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expressionValue); case WHEN_POSSIBLE: - if (canUpperCase(expression)) { - return (Expression) builder.upper((Expression) expression); + if (canUpperCase(path)) { + return JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expressionValue); } case NEVER: default: - return (Expression) expression; + return expressionValue; } } - private boolean canUpperCase(Expression expression) { - return String.class.equals(expression.getJavaType()); - } - - /** - * Returns a path to a {@link Comparable}. - * - * @param root - * @param part - * @return - */ - private Expression getComparablePath(Root root, Part part) { - return getTypedPath(root, part); - } - - private Expression getTypedPath(Root root, Part part) { - return toExpressionRecursively(root, part.getProperty()); - } - - private Expression traversePath(Path root, PropertyPath path) { - - Path result = root.get(path.getSegment()); - return (Expression) (path.hasNext() ? traversePath(result, path.next()) : result); + private boolean canUpperCase(PropertyPath path) { + return String.class.equals(path.getType()); } } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java new file mode 100644 index 0000000000..42c8ee95d7 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -0,0 +1,1219 @@ +/* + * Copyright 2024 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 static org.springframework.data.jpa.repository.query.QueryTokens.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.util.Predicates; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A Domain-Specific Language to build JPQL queries using Java code. + * + * @author Mark Paluch + */ +@SuppressWarnings("JavadocDeclaration") +public final class JpqlQueryBuilder { + + private JpqlQueryBuilder() {} + + /** + * Create an {@link Entity} from the given {@link Class entity class}. + * + * @param from the entity type to select from. + * @return + */ + public static Entity entity(Class from) { + return new Entity(from.getName(), from.getSimpleName(), + getAlias(from.getSimpleName(), Predicates.isTrue(), () -> "r")); + } + + /** + * Create a {@link Join INNER JOIN}. + * + * @param origin the selection origin (a join or the entity itself) to select from. + * @param path + * @return + */ + public static Join innerJoin(Origin origin, String path) { + return new Join(origin, "INNER JOIN", path); + } + + /** + * Create a {@link Join LEFT JOIN}. + * + * @param origin the selection origin (a join or the entity itself) to select from. + * @param path + * @return + */ + public static Join leftJoin(Origin origin, String path) { + return new Join(origin, "LEFT JOIN", path); + } + + /** + * Start building a {@link Select} statement by selecting {@link Class from}. This is a short form for + * {@code selectFrom(entity(from))}. + * + * @param from the entity type to select from. + * @return + */ + public static SelectStep selectFrom(Class from) { + return selectFrom(entity(from)); + } + + /** + * Start building a {@link Select} statement by selecting {@link Entity from}. + * + * @param from the entity source to select from. + * @return a new select builder. + */ + public static SelectStep selectFrom(Entity from) { + + return new SelectStep() { + + boolean distinct = false; + + @Override + public SelectStep distinct() { + + distinct = true; + return this; + } + + @Override + public Select entity() { + return new Select(postProcess(new EntitySelection(from)), from); + } + + @Override + public Select count() { + return new Select(new CountSelection(from, distinct), from); + } + + @Override + public Select instantiate(String resultType, Collection paths) { + return new Select(postProcess(new ConstructorExpression(resultType, new Multiselect(from, paths))), from); + } + + @Override + public Select select(Collection paths) { + return new Select(postProcess(new Multiselect(from, paths)), from); + } + + Selection postProcess(Selection selection) { + return distinct ? new DistinctSelection(selection) : selection; + } + }; + } + + private static String getAlias(String from, java.util.function.Predicate predicate, + Supplier fallback) { + + char c = from.toLowerCase(Locale.ROOT).charAt(0); + String string = Character.toString(c); + if (Character.isJavaIdentifierPart(c) && predicate.test(string)) { + return string; + } + + return fallback.get(); + } + + /** + * Invoke a {@literal function} with the given {@code arguments}. + * + * @param function function name. + * @param arguments function arguments. + * @return an expression representing a function call. + */ + public static Expression function(String function, Expression... arguments) { + return new FunctionExpression(function, Arrays.asList(arguments)); + } + + /** + * Nest the given {@link Predicate}. + * + * @param predicate + * @return + */ + public static Predicate nested(Predicate predicate) { + return new NestedPredicate(predicate); + } + + /** + * Create a qualified expression for a {@link PropertyPath}. + * + * @param source + * @param path + * @return + */ + public static Expression expression(Origin source, PropertyPath path) { + return expression(new PathAndOrigin(path, source, false)); + } + + /** + * Create a qualified expression for a {@link PropertyPath}. + * + * @param source + * @param path + * @return + */ + public static Expression expression(PathAndOrigin pas) { + return new PathExpression(pas); + } + + /** + * Create a simple expression from a string. + * + * @param expression + * @return + */ + public static Expression expression(String expression) { + + Assert.hasText(expression, "Expression must not be empty or null"); + + return new LiteralExpression(expression); + } + + public static Expression parameter(String parameter) { + + Assert.hasText(parameter, "Parameter must not be empty or null"); + + return new ParameterExpression(parameter); + } + + public static Expression orderBy(Expression sortExpression, Sort.Order order) { + return new OrderExpression(sortExpression, order); + } + + /** + * Start building a {@link Predicate WHERE predicate} by providing the right-hand side. + * + * @param source + * @param path + * @return + */ + public static WhereStep where(Origin source, PropertyPath path) { + return where(expression(source, path)); + } + + /** + * Start building a {@link Predicate WHERE predicate} by providing the right-hand side. + * + * @param rhs + * @return + */ + public static WhereStep where(PathAndOrigin rhs) { + return where(expression(rhs)); + } + + /** + * Start building a {@link Predicate WHERE predicate} by providing the right-hand side. + * + * @param rhs + * @return + */ + public static WhereStep where(Expression rhs) { + + return new WhereStep() { + @Override + public Predicate between(Expression lower, Expression upper) { + return new BetweenPredicate(rhs, lower, upper); + } + + @Override + public Predicate gt(Expression value) { + return new OperatorPredicate(rhs, ">", value); + } + + @Override + public Predicate gte(Expression value) { + return new OperatorPredicate(rhs, ">=", value); + } + + @Override + public Predicate lt(Expression value) { + return new OperatorPredicate(rhs, "<", value); + } + + @Override + public Predicate lte(Expression value) { + return new OperatorPredicate(rhs, "<=", value); + } + + @Override + public Predicate isNull() { + return new LhsPredicate(rhs, "IS NULL"); + } + + @Override + public Predicate isNotNull() { + return new LhsPredicate(rhs, "IS NOT NULL"); + } + + @Override + public Predicate isTrue() { + return new LhsPredicate(rhs, "IS TRUE"); + } + + @Override + public Predicate isFalse() { + return new LhsPredicate(rhs, "IS FALSE"); + } + + @Override + public Predicate isEmpty() { + return new LhsPredicate(rhs, "IS EMPTY"); + } + + @Override + public Predicate isNotEmpty() { + return new LhsPredicate(rhs, "IS NOT EMPTY"); + } + + @Override + public Predicate in(Expression value) { + return new InPredicate(rhs, "IN", value); + } + + @Override + public Predicate notIn(Expression value) { + return new InPredicate(rhs, "NOT IN", value); + } + + @Override + public Predicate inMultivalued(Expression value) { + return new MemberOfPredicate(rhs, "IN", value); + } + + @Override + public Predicate notInMultivalued(Expression value) { + return new MemberOfPredicate(rhs, "NOT IN", value); + } + + @Override + public Predicate memberOf(Expression value) { + return new MemberOfPredicate(rhs, "MEMBER OF", value); + } + + @Override + public Predicate notMemberOf(Expression value) { + return new MemberOfPredicate(rhs, "NOT MEMBER OF", value); + } + + @Override + public Predicate like(Expression value, String escape) { + return new LikePredicate(rhs, "LIKE", value, escape); + } + + @Override + public Predicate notLike(Expression value, String escape) { + return new LikePredicate(rhs, "NOT LIKE", value, escape); + } + + @Override + public Predicate eq(Expression value) { + return new OperatorPredicate(rhs, "=", value); + } + + @Override + public Predicate neq(Expression value) { + return new OperatorPredicate(rhs, "!=", value); + } + }; + } + + @Nullable + public static Predicate and(List intermediate) { + + Predicate predicate = null; + + for (Predicate other : intermediate) { + + if (predicate == null) { + predicate = other; + } else { + predicate = predicate.and(other); + } + } + + return predicate; + } + + @Nullable + public static Predicate or(List intermediate) { + + Predicate predicate = null; + + for (Predicate other : intermediate) { + + if (predicate == null) { + predicate = other; + } else { + predicate = predicate.or(other); + } + } + + return predicate; + } + + /** + * Fluent interface to build a {@link Select}. + */ + public interface SelectStep { + + /** + * Apply {@code DISTINCT}. + */ + SelectStep distinct(); + + /** + * Select the entity. + */ + Select entity(); + + /** + * Select the count. + */ + Select count(); + + /** + * Provide a constructor expression to instantiate {@code resultType}. Operates on the underlying {@link Entity + * FROM}. + * + * @param resultType + * @param paths + * @return + */ + default Select instantiate(Class resultType, Collection paths) { + return instantiate(resultType.getName(), paths); + } + + /** + * Provide a constructor expression to instantiate {@code resultType}. + * + * @param resultType + * @param paths + * @return + */ + Select instantiate(String resultType, Collection paths); + + /** + * Specify a multi-select. + * + * @param paths + * @return + */ + Select select(Collection paths); + + /** + * Select a single attribute. + * + * @param name + * @return + */ + default Select select(PathAndOrigin path) { + return select(List.of(path)); + } + + } + + interface Selection { + String render(RenderContext context); + } + + /** + * {@code DISTINCT} wrapper. + * + * @param selection + */ + record DistinctSelection(Selection selection) implements Selection { + + @Override + public String render(RenderContext context) { + return "DISTINCT %s".formatted(selection.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + /** + * Entity selection. + * + * @param source + */ + record EntitySelection(Entity source) implements Selection { + + @Override + public String render(RenderContext context) { + return context.getAlias(source); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + /** + * {@code COUNT(…)} selection. + * + * @param source + * @param distinct + */ + record CountSelection(Entity source, boolean distinct) implements Selection { + + @Override + public String render(RenderContext context) { + return "COUNT(%s%s)".formatted(distinct ? "DISTINCT " : "", context.getAlias(source)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + /** + * Expression selection. + * + * @param resultType + * @param multiselect + */ + record ConstructorExpression(String resultType, Multiselect multiselect) implements Selection { + + @Override + public String render(RenderContext context) { + return "new %s(%s)".formatted(resultType, multiselect.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + /** + * Multi-select selecting one or many property paths. + * + * @param source + * @param paths + */ + record Multiselect(Origin source, Collection paths) implements Selection { + + @Override + public String render(RenderContext context) { + + StringBuilder builder = new StringBuilder(); + + for (PathAndOrigin path : paths) { + + if (!builder.isEmpty()) { + builder.append(", "); + } + + builder.append(PathExpression.render(path, context)); + builder.append(" ").append(path.path().getSegment()); + } + + return builder.toString(); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + /** + * {@code WHERE} predicate. + */ + public interface Predicate { + + /** + * Render the predicate given {@link RenderContext}. + * + * @param context + * @return + */ + String render(RenderContext context); + + /** + * {@code OR}-concatenate this predicate with {@code other}. + * + * @param other + * @return a composed predicate combining this and {@code other} using the OR operator. + */ + default Predicate or(Predicate other) { + return new OrPredicate(this, other); + } + + /** + * {@code AND}-concatenate this predicate with {@code other}. + * + * @param other + * @return a composed predicate combining this and {@code other} using the AND operator. + */ + default Predicate and(Predicate other) { + return new AndPredicate(this, other); + } + + /** + * Wrap this predicate with parenthesis {@code (…)} to nest it without affecting AND/OR concatenation precedence. + * + * @return a nested variant of this predicate. + */ + default Predicate nest() { + return new NestedPredicate(this); + } + } + + /** + * Interface specifying an expression that can be rendered to {@code String}. + */ + public interface Expression { + + /** + * Render the expression given {@link RenderContext}. + * + * @param context + * @return + */ + String render(RenderContext context); + } + + /** + * {@code SELECT} statement. + */ + public static class Select extends AbstractJpqlQuery { + + private final Selection selection; + + private final Entity entity; + + private final Map joins = new LinkedHashMap<>(); + + private final List orderBy = new ArrayList<>(); + + private Select(Selection selection, Entity entity) { + this.selection = selection; + this.entity = entity; + } + + /** + * Append a join to this select. + * + * @param join + * @return + */ + public Select join(Join join) { + + if (join.source() instanceof Join parent) { + join(parent); + } + + this.joins.put(join.joinType() + "_" + join.getName() + "_" + join.path(), join); + return this; + } + + /** + * Append an order-by expression to this select. + * + * @param orderBy + * @return + */ + public Select orderBy(Expression orderBy) { + this.orderBy.add(orderBy); + return this; + } + + @Override + String render() { + + Map aliases = new LinkedHashMap<>(); + aliases.put(entity, entity.alias); + + RenderContext renderContext = new RenderContext(aliases); + + StringBuilder where = new StringBuilder(); + StringBuilder orderby = new StringBuilder(); + StringBuilder result = new StringBuilder( + "SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.entity(), entity.alias())); + + if (getWhere() != null) { + where.append(" WHERE ").append(getWhere().render(renderContext)); + } + + if (!orderBy.isEmpty()) { + + StringBuilder builder = new StringBuilder(); + + for (Expression order : orderBy) { + if (!builder.isEmpty()) { + builder.append(", "); + } + + builder.append(order.render(renderContext)); + } + + orderby.append(" ORDER BY ").append(builder); + } + + aliases.keySet().forEach(key -> { + + if (key instanceof Join js) { + join(js); + } + }); + + for (Join join : joins.values()) { + result.append(" ").append(join.joinType()).append(" ").append(renderContext.getAlias(join.source())).append(".") + .append(join.path()).append(" ").append(renderContext.getAlias(join)); + } + + result.append(where).append(orderby); + + return result.toString(); + } + } + + /** + * Abstract base class for JPQL queries. + */ + public static abstract class AbstractJpqlQuery { + + private @Nullable Predicate where; + + public AbstractJpqlQuery where(Predicate predicate) { + this.where = predicate; + return this; + } + + @Nullable + public Predicate getWhere() { + return where; + } + + abstract String render(); + + @Override + public String toString() { + return render(); + } + } + + record OrderExpression(Expression sortExpression, Sort.Order order) implements Expression { + + @Override + public String render(RenderContext context) { + + StringBuilder builder = new StringBuilder(); + + builder.append(sortExpression.render(context)); + builder.append(" "); + + builder.append(order.isDescending() ? TOKEN_DESC : TOKEN_ASC); + + if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) { + builder.append(" NULLS FIRST"); + } else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) { + builder.append(" NULLS LAST"); + } + + return builder.toString(); + } + } + + /** + * Context used during rendering. + */ + public static class RenderContext { + + public static final RenderContext EMPTY = new RenderContext(Collections.emptyMap()) { + + @Override + public String getAlias(Origin source) { + return ""; + } + }; + + private final Map aliases; + private int counter; + + RenderContext(Map aliases) { + this.aliases = aliases; + } + + /** + * Obtain an alias for {@link Origin}. Unknown selection origins are associated with the enclosing statement if they + * are used for the first time. + * + * @param source + * @return + */ + public String getAlias(Origin source) { + + return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(), s -> { + return !aliases.containsValue(s); + }, () -> "join_" + (counter++))); + } + + /** + * Prefix {@code fragment} with the alias for {@link Origin}. Unknown selection origins are associated with the + * enclosing statement if they are used for the first time. + * + * @param source + * @return + */ + public String prefixWithAlias(Origin source, String fragment) { + + String alias = getAlias(source); + return ObjectUtils.isEmpty(source) ? fragment : alias + "." + fragment; + } + } + + /** + * An origin that is used to select data from. selection origins are used with paths to define where a path is + * anchored. + */ + public interface Origin { + + String getName(); + } + + /** + * The root entity. + * + * @param entity + * @param simpleName + * @param alias + */ + public record Entity(String entity, String simpleName, String alias) implements Origin { + + @Override + public String getName() { + return simpleName; + } + } + + /** + * A joined entity or element collection. + * + * @param source + * @param joinType + * @param path + */ + public record Join(Origin source, String joinType, String path) implements Origin, Expression { + + @Override + public String getName() { + return path; + } + + @Override + public String render(RenderContext context) { + return ""; + } + } + + /** + * Fluent interface to build a {@link Predicate}. + */ + public interface WhereStep { + + /** + * Create a {@code BETWEEN … AND …} predicate. + * + * @param lower lower boundary. + * @param upper upper boundary. + * @return + */ + default Predicate between(String lower, String upper) { + return between(expression(lower), expression(upper)); + } + + /** + * Create a {@code BETWEEN … AND …} predicate. + * + * @param lower lower boundary. + * @param upper upper boundary. + * @return + */ + Predicate between(Expression lower, Expression upper); + + /** + * Create a greater {@code > …} predicate. + * + * @param value the comparison value. + * @return + */ + default Predicate gt(String value) { + return gt(expression(value)); + } + + /** + * Create a greater {@code > …} predicate. + * + * @param value the comparison value. + * @return + */ + Predicate gt(Expression value); + + /** + * Create a greater-or-equals {@code >= …} predicate. + * + * @param value the comparison value. + * @return + */ + default Predicate gte(String value) { + return gte(expression(value)); + } + + /** + * Create a greater-or-equals {@code >= …} predicate. + * + * @param value the comparison value. + * @return + */ + Predicate gte(Expression value); + + /** + * Create a less {@code < …} predicate. + * + * @param value the comparison value. + * @return + */ + default Predicate lt(String value) { + return lt(expression(value)); + } + + /** + * Create a less {@code < …} predicate. + * + * @param value the comparison value. + * @return + */ + Predicate lt(Expression value); + + /** + * Create a less-or-equals {@code <= …} predicate. + * + * @param value the comparison value. + * @return + */ + default Predicate lte(String value) { + return lte(expression(value)); + } + + /** + * Create a less-or-equals {@code <= …} predicate. + * + * @param value the comparison value. + * @return + */ + Predicate lte(Expression value); + + Predicate isNull(); + + Predicate isNotNull(); + + Predicate isTrue(); + + Predicate isFalse(); + + Predicate isEmpty(); + + Predicate isNotEmpty(); + + default Predicate in(String value) { + return in(expression(value)); + } + + Predicate in(Expression value); + + default Predicate notIn(String value) { + return notIn(expression(value)); + } + + Predicate notIn(Expression value); + + default Predicate inMultivalued(String value) { + return inMultivalued(expression(value)); + } + + Predicate inMultivalued(Expression value); + + default Predicate notInMultivalued(String value) { + return notInMultivalued(expression(value)); + } + + Predicate notInMultivalued(Expression value); + + default Predicate memberOf(String value) { + return memberOf(expression(value)); + } + + Predicate memberOf(Expression value); + + default Predicate notMemberOf(String value) { + return notMemberOf(expression(value)); + } + + Predicate notMemberOf(Expression value); + + default Predicate like(String value, String escape) { + return like(expression(value), escape); + } + + Predicate like(Expression value, String escape); + + default Predicate notLike(String value, String escape) { + return notLike(expression(value), escape); + } + + Predicate notLike(Expression value, String escape); + + default Predicate eq(String value) { + return eq(expression(value)); + } + + Predicate eq(Expression value); + + default Predicate neq(String value) { + return neq(expression(value)); + } + + Predicate neq(Expression value); + } + + record PathExpression(PathAndOrigin pas) implements Expression { + + @Override + public String render(RenderContext context) { + return render(pas, context); + + } + + public static String render(PathAndOrigin pas, RenderContext context) { + + if (pas.path().hasNext() || !pas.onTheJoin()) { + return context.prefixWithAlias(pas.origin(), pas.path().toDotPath()); + } else { + return context.getAlias(pas.origin()); + } + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record LiteralExpression(String expression) implements Expression { + + @Override + public String render(RenderContext context) { + return expression; + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record ParameterExpression(String parameter) implements Expression { + + @Override + public String render(RenderContext context) { + return parameter; + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record FunctionExpression(String function, List arguments) implements Expression { + + @Override + public String render(RenderContext context) { + + StringBuilder builder = new StringBuilder(); + + for (Expression argument : arguments) { + + if (!builder.isEmpty()) { + builder.append(", "); + } + + builder.append(argument.render(context)); + } + + return "%s(%s)".formatted(function, builder); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record OperatorPredicate(Expression path, String operator, Expression predicate) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s %s %s".formatted(path.render(context), operator, predicate.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record MemberOfPredicate(Expression path, String operator, Expression predicate) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s %s %s".formatted(predicate.render(context), operator, path.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record LhsPredicate(Expression path, String predicate) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s %s".formatted(path.render(context), predicate); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record BetweenPredicate(Expression path, Expression lower, Expression upper) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s BETWEEN %s AND %s".formatted(path.render(context), lower.render(context), upper.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record LikePredicate(Expression left, String operator, Expression right, String escape) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s %s %s ESCAPE '%s'".formatted(left.render(context), operator, right.render(context), escape); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record InPredicate(Expression path, String operator, Expression predicate) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record AndPredicate(Predicate left, Predicate right) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s AND %s".formatted(left.render(context), right.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record OrPredicate(Predicate left, Predicate right) implements Predicate { + + @Override + public String render(RenderContext context) { + return "%s OR %s".formatted(left.render(context), right.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record NestedPredicate(Predicate delegate) implements Predicate { + + @Override + public String render(RenderContext context) { + return "(%s)".formatted(delegate.render(context)); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + /** + * Value object capturing a property path and its origin. + * + * @param path + * @param origin + * @param onTheJoin whether the path should target the join itself instead of matching {@link PropertyPath}. + */ + public record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) { + + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java new file mode 100644 index 0000000000..bbffd7c8a6 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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.springframework.data.domain.Sort; + +/** + * @author Mark Paluch + */ +interface JpqlQueryCreator { + + boolean useTupleQuery(); + + String createQuery(Sort sort); + + List getBindings(); + + ParameterBinder getBinder(); +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java new file mode 100644 index 0000000000..50da5558bb --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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.criteria.From; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; + +import java.util.Objects; + +import org.springframework.data.mapping.PropertyPath; + +/** + * @author Mark Paluch + */ +class JpqlUtils { + + static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From from, + PropertyPath property) { + return toExpressionRecursively(source, from, property, false); + } + + static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From from, + PropertyPath property, boolean isForSelection) { + return toExpressionRecursively(source, from, property, isForSelection, false); + } + + /** + * Creates an expression with proper inner and left joins by recursively navigating the path + * + * @param from the {@link From} + * @param property the property path + * @param isForSelection is the property navigated for the selection or ordering part of the query? + * @param hasRequiredOuterJoin has a parent already required an outer join? + * @param the type of the expression + * @return the expression + */ + @SuppressWarnings("unchecked") + static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From from, + PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { + + String segment = property.getSegment(); + + boolean isLeafProperty = !property.hasNext(); + + boolean requiresOuterJoin = QueryUtils.requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin); + + // if it does not require an outer join and is a leaf, simply get the segment + if (!requiresOuterJoin && isLeafProperty) { + return new JpqlQueryBuilder.PathAndOrigin(property, source, false); + } + + // get or create the join + JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment) + : JpqlQueryBuilder.innerJoin(source, segment); + JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER; + Join join = QueryUtils.getOrCreateJoin(from, segment, joinType); + + // if it's a leaf, return the join + if (isLeafProperty) { + return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true); + } + + PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null"); + + // recurse with the next property + return toExpressionRecursively(joinSource, join, nextProperty, isForSelection, requiresOuterJoin); + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java index cea64d91ad..ef9a67b697 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java @@ -134,6 +134,29 @@ protected List getResultWindow(List list, int limit) { return CollectionUtils.getFirst(limit, list); } + public Sort createSort(Sort sort, JpaEntityInformation entity) { + + Collection sortById; + Sort sortToUse; + if (entity.hasCompositeId()) { + sortById = new ArrayList<>(entity.getIdAttributeNames()); + } else { + sortById = new ArrayList<>(1); + sortById.add(entity.getRequiredIdAttribute().getName()); + } + + sort.forEach(it -> sortById.remove(it.getProperty())); + + if (sortById.isEmpty()) { + sortToUse = sort; + } else { + sortToUse = sort.and(Sort.by(sortById.toArray(new String[0]))); + } + + return getSortOrders(sortToUse); + + } + /** * Reverse scrolling variant applying {@link Direction#Backward}. In reverse scrolling, we need to flip directions for * the actual query so that we do not get everything from the top position and apply the limit but rather flip the diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index 40aa051983..59df7353b9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -22,8 +22,6 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import org.springframework.data.domain.KeysetScrollPosition; @@ -42,7 +40,7 @@ * @author Christoph Strobl * @since 3.1 */ -public record KeysetScrollSpecification (KeysetScrollPosition position, Sort sort, +public record KeysetScrollSpecification(KeysetScrollPosition position, Sort sort, JpaEntityInformation entity) implements Specification { public KeysetScrollSpecification(KeysetScrollPosition position, Sort sort, JpaEntityInformation entity) { @@ -63,24 +61,7 @@ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntit KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); - Collection sortById; - Sort sortToUse; - if (entity.hasCompositeId()) { - sortById = new ArrayList<>(entity.getIdAttributeNames()); - } else { - sortById = new ArrayList<>(1); - sortById.add(entity.getRequiredIdAttribute().getName()); - } - - sort.forEach(it -> sortById.remove(it.getProperty())); - - if (sortById.isEmpty()) { - sortToUse = sort; - } else { - sortToUse = sort.and(Sort.by(sortById.toArray(new String[0]))); - } - - return delegate.getSortOrders(sortToUse); + return delegate.createSort(sort, entity); } @Override @@ -92,16 +73,24 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild public Predicate createPredicate(Root root, CriteriaBuilder criteriaBuilder) { KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); - return delegate.createPredicate(position, sort, new JpaQueryStrategy(root, criteriaBuilder)); + return delegate.createPredicate(position, sort, new CriteriaBuilderStrategy(root, criteriaBuilder)); + } + + @Nullable + public JpqlQueryBuilder.Predicate createJpqlPredicate(From from, JpqlQueryBuilder.Entity entity, + ParameterFactory factory) { + + KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); + return delegate.createPredicate(position, sort, new JpqlStrategy(from, entity, factory)); } @SuppressWarnings("rawtypes") - private static class JpaQueryStrategy implements QueryStrategy, Predicate> { + private static class CriteriaBuilderStrategy implements QueryStrategy, Predicate> { private final From from; private final CriteriaBuilder cb; - public JpaQueryStrategy(From from, CriteriaBuilder cb) { + public CriteriaBuilderStrategy(From from, CriteriaBuilder cb) { this.from = from; this.cb = cb; @@ -136,4 +125,55 @@ public Predicate or(List intermediate) { return cb.or(intermediate.toArray(new Predicate[0])); } } + + private static class JpqlStrategy implements QueryStrategy { + + private final From from; + private final JpqlQueryBuilder.Entity entity; + private final ParameterFactory factory; + + public JpqlStrategy(From from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { + + this.from = from; + this.entity = entity; + this.factory = factory; + } + + @Override + public JpqlQueryBuilder.Expression createExpression(String property) { + + PropertyPath path = PropertyPath.from(property, from.getJavaType()); + return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, path)); + } + + @Override + public JpqlQueryBuilder.Predicate compare(Order order, JpqlQueryBuilder.Expression propertyExpression, + Object value) { + + JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression); + return order.isAscending() ? where.gt(factory.capture(value)) : where.lt(factory.capture(value)); + } + + @Override + public JpqlQueryBuilder.Predicate compare(JpqlQueryBuilder.Expression propertyExpression, @Nullable Object value) { + + JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression); + + return value == null ? where.isNull() : where.eq(factory.capture(value)); + } + + @Override + public JpqlQueryBuilder.Predicate and(List intermediate) { + return JpqlQueryBuilder.and(intermediate); + } + + @Override + public JpqlQueryBuilder.Predicate or(List intermediate) { + return JpqlQueryBuilder.or(intermediate); + } + } + + public interface ParameterFactory { + JpqlQueryBuilder.Expression capture(Object value); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index eeed1593fa..4b436fd8a0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -56,8 +56,6 @@ final class NamedQuery extends AbstractJpaQuery { private final @Nullable String countProjection; private final boolean namedCountQueryIsPresent; private final Lazy declaredQuery; - private final QueryParameterSetter.QueryMetadataCache metadataCache; - private final QueryRewriter queryRewriter; /** * Creates a new {@link NamedQuery}. @@ -100,7 +98,6 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryRewriter queryR this.declaredQuery = Lazy .of(() -> DeclaredQuery.of(queryString, method.isNativeQuery() || query.toString().contains("NativeQuery"))); - this.metadataCache = new QueryParameterSetter.QueryMetadataCache(); } /** @@ -175,9 +172,7 @@ protected Query doCreateQuery(JpaParametersParameterAccessor accessor) { ? em.createNamedQuery(queryName) // : em.createNamedQuery(queryName, typeToRead); - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryName, query); - - return parameterBinder.get().bindAndPrepare(query, metadata, accessor); + return parameterBinder.get().bindAndPrepare(query, accessor); } @Override @@ -199,9 +194,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc countQuery = em.createQuery(countQueryString, Long.class); } - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(cacheKey, countQuery); - - return parameterBinder.get().bind(countQuery, metadata, accessor); + return parameterBinder.get().bind(countQuery, accessor); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java index 7a49f584a1..8c7c458852 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling; import org.springframework.data.jpa.support.PageableUtils; import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; /** * {@link ParameterBinder} is used to bind method parameters to a {@link Query}. This is usually done whenever an @@ -33,7 +34,7 @@ * @author Jens Schauder * @author Yanming Zhou */ -public class ParameterBinder { +class ParameterBinder { static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters"; @@ -72,18 +73,18 @@ public ParameterBinder(JpaParameters parameters, Iterable this.useJpaForPaging = useJpaForPaging; } - public T bind(T jpaQuery, QueryParameterSetter.QueryMetadata metadata, + public T bind(T jpaQuery, JpaParametersParameterAccessor accessor) { - bind(metadata.withQuery(jpaQuery), accessor, ErrorHandling.STRICT); + bind(new QueryParameterSetter.BindableQuery(jpaQuery), accessor, ErrorHandling.STRICT); return jpaQuery; } public void bind(QueryParameterSetter.BindableQuery query, JpaParametersParameterAccessor accessor, - ErrorHandling errorHandling) { + ErrorHandler errorHandler) { for (QueryParameterSetter setter : parameterSetters) { - setter.setParameter(query, accessor, errorHandling); + setter.setParameter(query, accessor, errorHandler); } } @@ -91,13 +92,12 @@ public void bind(QueryParameterSetter.BindableQuery query, JpaParametersParamete * Binds the parameters to the given query and applies special parameter types (e.g. pagination). * * @param query must not be {@literal null}. - * @param metadata must not be {@literal null}. * @param accessor must not be {@literal null}. */ - Query bindAndPrepare(Query query, QueryParameterSetter.QueryMetadata metadata, + Query bindAndPrepare(Query query, JpaParametersParameterAccessor accessor) { - bind(query, metadata, accessor); + bind(query, accessor); Pageable pageable = accessor.getPageable(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 21a715e07f..384d5c16d7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -23,7 +23,6 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; import org.springframework.util.Assert; /** @@ -40,37 +39,37 @@ class ParameterBinderFactory { * otherwise. * * @param parameters method parameters that are available for binding, must not be {@literal null}. + * @param preferNamedParameters * @return a {@link ParameterBinder} that can assign values for the method parameters to query parameters of a * {@link jakarta.persistence.Query} */ - static ParameterBinder createBinder(JpaParameters parameters) { + static ParameterBinder createBinder(JpaParameters parameters, boolean preferNamedParameters) { Assert.notNull(parameters, "JpaParameters must not be null"); - QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters); + QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters, preferNamedParameters); List bindings = getBindings(parameters); return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); } /** - * Creates a {@link ParameterBinder} that just matches method parameter to parameters of a - * {@link jakarta.persistence.criteria.CriteriaQuery}. + * Creates a {@link ParameterBinder} that matches method parameter to parameters of a + * {@link jakarta.persistence.Query} and that can bind synthetic parameters. * * @param parameters method parameters that are available for binding, must not be {@literal null}. - * @param metadata must not be {@literal null}. + * @param bindings parameter bindings for method argument and synthetic parameters, 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.criteria.CriteriaQuery} + * {@link jakarta.persistence.Query} */ - static ParameterBinder createCriteriaBinder(JpaParameters parameters, List> metadata) { + static ParameterBinder createBinder(JpaParameters parameters, List bindings) { Assert.notNull(parameters, "JpaParameters must not be null"); - Assert.notNull(metadata, "Parameter metadata must not be null"); - - QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forCriteriaQuery(parameters, metadata); - List bindings = getBindings(parameters); + Assert.notNull(bindings, "Parameter bindings must not be null"); - return new ParameterBinder(parameters, createSetters(bindings, setterFactory)); + return new ParameterBinder(parameters, + createSetters(bindings, QueryParameterSetterFactory.forPartTreeQuery(parameters), + QueryParameterSetterFactory.forSynthetic())); } /** @@ -97,15 +96,16 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Declared QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser, evaluationContextProvider); - QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters); + QueryParameterSetterFactory basicSetterFactory = QueryParameterSetterFactory.basic(parameters, + query.hasNamedParameter()); return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory), !query.usesPaging()); } - private static List getBindings(JpaParameters parameters) { + static List getBindings(JpaParameters parameters) { - List result = new ArrayList<>(); + List result = new ArrayList<>(parameters.getNumberOfParameters()); int bindableParameterIndex = 0; for (JpaParameter parameter : parameters) { @@ -143,7 +143,7 @@ private static QueryParameterSetter createQueryParameterSetter(ParameterBinding for (QueryParameterSetterFactory strategy : strategies) { - QueryParameterSetter setter = strategy.create(binding, declaredQuery); + QueryParameterSetter setter = strategy.create(binding); if (setter != null) { return setter; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index e5cffccaf6..d8b8e52fa2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -21,13 +21,19 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.springframework.data.expression.ValueExpression; import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -186,6 +192,115 @@ public boolean isCompatibleWith(ParameterBinding other) { return other.getClass() == getClass() && other.getOrigin().equals(getOrigin()); } + /** + * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an + * {@code IN} parameter. + * + * @author Thomas Darimont + * @author Mark Paluch + */ + static class PartTreeParameterBinding extends ParameterBinding { + + private final Class parameterType; + private final JpqlQueryTemplates templates; + private final EscapeCharacter escape; + private final Type type; + private final boolean ignoreCase; + private final boolean noWildcards; + + public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class parameterType, + Part part, @Nullable Object value, JpqlQueryTemplates templates, EscapeCharacter escape) { + + super(identifier, origin); + + this.parameterType = parameterType; + this.templates = templates; + this.escape = escape; + + this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType(); + this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); + this.noWildcards = part.getProperty().getLeafProperty().isCollection(); + } + + /** + * Returns whether the parameter shall be considered an {@literal IS NULL} parameter. + */ + public boolean isIsNullParameter() { + return Type.IS_NULL.equals(type); + } + + @Override + public Object prepare(@Nullable Object value) { + + if (value == null || parameterType == null) { + return value; + } + + if (String.class.equals(parameterType) && !noWildcards) { + + switch (type) { + case STARTING_WITH: + return String.format("%s%%", escape.escape(value.toString())); + case ENDING_WITH: + return String.format("%%%s", escape.escape(value.toString())); + case CONTAINING: + case NOT_CONTAINING: + return String.format("%%%s%%", escape.escape(value.toString())); + default: + return value; + } + } + + return Collection.class.isAssignableFrom(parameterType) // + ? potentiallyIgnoreCase(ignoreCase, toCollection(value)) // + : value; + } + + @Nullable + @SuppressWarnings("unchecked") + private Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { + + if (!ignoreCase || CollectionUtils.isEmpty(collection)) { + return collection; + } + + return ((Collection) collection).stream() // + .map(it -> it == null // + ? null // + : templates.ignoreCase(it)) // + .collect(Collectors.toList()); + } + + /** + * Returns the given argument as {@link Collection} which means it will return it as is if it's a + * {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element + * {@link Collections}. + * + * @param value the value to be converted to a {@link Collection}. + * @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value. + */ + @Nullable + private static Collection toCollection(@Nullable Object value) { + + if (value == null) { + return null; + } + + if (value instanceof Collection collection) { + return collection.isEmpty() ? null : collection; + } + + if (ObjectUtils.isArray(value)) { + + List collection = Arrays.asList(ObjectUtils.toObjectArray(value)); + return collection.isEmpty() ? null : collection; + } + + return Collections.singleton(value); + } + + } + /** * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an * {@code IN} parameter. @@ -349,7 +464,7 @@ static Type getLikeTypeFrom(String expression) { * @author Mark Paluch * @since 3.1.2 */ - sealed interface BindingIdentifier permits Named,Indexed,NamedAndIndexed { + sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed { /** * Creates an identifier for the given {@code name}. @@ -495,7 +610,7 @@ public String toString() { * @author Mark Paluch * @since 3.1.2 */ - sealed interface ParameterOrigin permits Expression,MethodInvocationArgument { + sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic { /** * Creates a {@link Expression} for the given {@code expression}. @@ -507,6 +622,17 @@ static Expression ofExpression(ValueExpression expression) { return new Expression(expression); } + /** + * Creates a {@link Expression} for the given {@code expression} string. + * + * @param value the captured value. + * @param source source from which this value is derived. + * @return {@link Synthetic} for the given {@code value}. + */ + static Synthetic synthetic(@Nullable Object value, Object source) { + return new Synthetic(value, source); + } + /** * Creates a {@link MethodInvocationArgument} object for {@code name} * @@ -539,6 +665,16 @@ static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Int return ofParameter(identifier); } + /** + * Creates a {@link MethodInvocationArgument} object for {@code position}. + * + * @param position the parameter position (1-based) from the method invocation. + * @return {@link MethodInvocationArgument} object for {@code position}. + */ + static MethodInvocationArgument ofParameter(Parameter parameter) { + return ofParameter(parameter.getIndex() + 1); + } + /** * Creates a {@link MethodInvocationArgument} object for {@code position}. * @@ -568,6 +704,11 @@ static MethodInvocationArgument ofParameter(BindingIdentifier identifier) { * @return {@code true} if the origin is an expression. */ boolean isExpression(); + + /** + * @return {@code true} if the origin is an expression. + */ + boolean isSynthetic(); } /** @@ -588,6 +729,36 @@ public boolean isMethodArgument() { public boolean isExpression() { return true; } + + @Override + public boolean isSynthetic() { + return true; + } + } + + /** + * Value object capturing the expression of which a binding parameter originates. + * + * @param value + * @param source + * @author Mark Paluch + */ + public record Synthetic(@Nullable Object value, Object source) implements ParameterOrigin { + + @Override + public boolean isMethodArgument() { + return false; + } + + @Override + public boolean isExpression() { + return false; + } + + @Override + public boolean isSynthetic() { + return true; + } } /** @@ -608,5 +779,10 @@ public boolean isMethodArgument() { public boolean isExpression() { return false; } + + @Override + public boolean isSynthetic() { + return false; + } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java index aa96a30163..667bc9f809 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java @@ -15,8 +15,9 @@ */ package org.springframework.data.jpa.repository.query; +import static org.springframework.data.jpa.repository.query.ParameterBinding.*; + import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.ParameterExpression; import java.util.ArrayList; import java.util.Arrays; @@ -24,10 +25,10 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; @@ -56,83 +57,87 @@ */ public class ParameterMetadataProvider { - private final CriteriaBuilder builder; private final Iterator parameters; - private final List> expressions; + private final List bindings; private final @Nullable Iterator bindableParameterValues; private final EscapeCharacter escape; + private final JpqlQueryTemplates templates; + private final JpaParameters jpaParameters; + private int position; /** * Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and * {@link ParametersParameterAccessor}. * - * @param builder must not be {@literal null}. * @param accessor must not be {@literal null}. * @param escape must not be {@literal null}. + * @param templates must not be {@literal null}. */ - public ParameterMetadataProvider(CriteriaBuilder builder, ParametersParameterAccessor accessor, - EscapeCharacter escape) { - this(builder, accessor.iterator(), accessor.getParameters(), escape); + public ParameterMetadataProvider(JpaParametersParameterAccessor accessor, + EscapeCharacter escape, JpqlQueryTemplates templates) { + this(accessor.iterator(), accessor.getParameters(), escape, templates); } /** * Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and {@link Parameters} with * support for parameter value customizations via {@link PersistenceProvider}. * - * @param builder must not be {@literal null}. * @param parameters must not be {@literal null}. * @param escape must not be {@literal null}. + * @param templates must not be {@literal null}. */ - public ParameterMetadataProvider(CriteriaBuilder builder, Parameters parameters, EscapeCharacter escape) { - this(builder, null, parameters, escape); + public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escape, + JpqlQueryTemplates templates) { + this(null, parameters, escape, templates); } /** * Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} an {@link Iterable} of all * bindable parameter values, and {@link Parameters}. * - * @param builder must not be {@literal null}. * @param bindableParameterValues may be {@literal null}. * @param parameters must not be {@literal null}. * @param escape must not be {@literal null}. + * @param templates must not be {@literal null}. */ - private ParameterMetadataProvider(CriteriaBuilder builder, @Nullable Iterator bindableParameterValues, - Parameters parameters, EscapeCharacter escape) { + private ParameterMetadataProvider(@Nullable Iterator bindableParameterValues, JpaParameters parameters, + EscapeCharacter escape, JpqlQueryTemplates templates) { - Assert.notNull(builder, "CriteriaBuilder must not be null"); Assert.notNull(parameters, "Parameters must not be null"); Assert.notNull(escape, "EscapeCharacter must not be null"); + Assert.notNull(templates, "JpqlQueryTemplates must not be null"); - this.builder = builder; + this.jpaParameters = parameters; this.parameters = parameters.getBindableParameters().iterator(); - this.expressions = new ArrayList<>(); + this.bindings = new ArrayList<>(); this.bindableParameterValues = bindableParameterValues; this.escape = escape; + this.templates = templates; } /** - * Returns all {@link ParameterMetadata}s built. + * Returns all {@link ParameterBinding}s built. * - * @return the expressions + * @return the bindings. */ - public List> getExpressions() { - return expressions; + public List getBindings() { + return bindings; } /** - * Builds a new {@link ParameterMetadata} for given {@link Part} and the next {@link Parameter}. + * Builds a new {@link PartTreeParameterBinding} for given {@link Part} and the next {@link Parameter}. */ @SuppressWarnings("unchecked") - public ParameterMetadata next(Part part) { + public PartTreeParameterBinding next(Part part) { Assert.isTrue(parameters.hasNext(), () -> String.format("No parameter available for part %s", part)); Parameter parameter = parameters.next(); - return (ParameterMetadata) next(part, parameter.getType(), parameter); + return next(part, parameter.getType(), parameter); } /** - * Builds a new {@link ParameterMetadata} of the given {@link Part} and type. Forwards the underlying + * Builds a new {@link PartTreeParameterBinding} of the given {@link Part} and type. Forwards the underlying * {@link Parameters} as well. * * @param is the type parameter of the returned {@link ParameterMetadata}. @@ -140,15 +145,15 @@ public ParameterMetadata next(Part part) { * @return ParameterMetadata for the next parameter. */ @SuppressWarnings("unchecked") - public ParameterMetadata next(Part part, Class type) { + public PartTreeParameterBinding next(Part part, Class type) { Parameter parameter = parameters.next(); Class typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type; - return (ParameterMetadata) next(part, typeToUse, parameter); + return next(part, typeToUse, parameter); } /** - * Builds a new {@link ParameterMetadata} for the given type and name. + * Builds a new {@link PartTreeParameterBinding} for the given type and name. * * @param type parameter for the returned {@link ParameterMetadata}. * @param part must not be {@literal null}. @@ -156,7 +161,7 @@ public ParameterMetadata next(Part part, Class type) { * @param parameter providing the name for the returned {@link ParameterMetadata}. * @return a new {@link ParameterMetadata} for the given type and name. */ - private ParameterMetadata next(Part part, Class type, Parameter parameter) { + private PartTreeParameterBinding next(Part part, Class type, Parameter parameter) { Assert.notNull(type, "Type must not be null"); @@ -166,37 +171,57 @@ private ParameterMetadata next(Part part, Class type, Parameter parame @SuppressWarnings("unchecked") Class reifiedType = Expression.class.equals(type) ? (Class) Object.class : type; - Supplier name = () -> parameter.getName() - .orElseThrow(() -> new IllegalArgumentException("o_O Parameter needs to be named")); + Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next(); - ParameterExpression expression = parameter.isExplicitlyNamed() // - ? builder.parameter(reifiedType, name.get()) // - : builder.parameter(reifiedType); + int currentPosition = ++position; - Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next(); + BindingIdentifier bindingIdentifier = BindingIdentifier.of(currentPosition); - ParameterMetadata metadata = new ParameterMetadata<>(expression, part, value, escape); - expressions.add(metadata); + /* identifier refers to bindable parameters, not _all_ parameters index */ + MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(bindingIdentifier); + PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier, methodParameter, reifiedType, + part, value, templates, escape); - return metadata; + bindings.add(binding); + + return binding; } EscapeCharacter getEscape() { return escape; } + /** + * Builds a new synthetic {@link ParameterBinding} for the given value. + * + * @param value + * @param source + * @return a new {@link ParameterBinding} for the given value and source. + */ + public ParameterBinding nextSynthetic(Object value, Object source) { + + int currentPosition = ++position; + + return new ParameterBinding(BindingIdentifier.of(currentPosition), ParameterOrigin.synthetic(value, source)); + } + + public JpaParameters getParameters() { + return this.jpaParameters; + } + /** * @author Oliver Gierke * @author Thomas Darimont * @author Andrey Kovalev - * @param */ - public static class ParameterMetadata { + public static class ParameterMetadata { static final Object PLACEHOLDER = new Object(); + private final Class parameterType; private final Type type; - private final ParameterExpression expression; + private final int position; + private final JpqlQueryTemplates templates; private final EscapeCharacter escape; private final boolean ignoreCase; private final boolean noWildcards; @@ -204,10 +229,12 @@ public static class ParameterMetadata { /** * Creates a new {@link ParameterMetadata}. */ - public ParameterMetadata(ParameterExpression expression, Part part, @Nullable Object value, - EscapeCharacter escape) { + public ParameterMetadata(Class parameterType, Part part, @Nullable Object value, EscapeCharacter escape, + int position, JpqlQueryTemplates templates) { - this.expression = expression; + this.parameterType = parameterType; + this.position = position; + this.templates = templates; this.type = value == null && (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType())) ? Type.IS_NULL @@ -217,13 +244,12 @@ public ParameterMetadata(ParameterExpression expression, Part part, @Nullable this.escape = escape; } - /** - * Returns the {@link ParameterExpression}. - * - * @return the expression - */ - public ParameterExpression getExpression() { - return expression; + public int getPosition() { + return position; + } + + public Class getParameterType() { + return parameterType; } /** @@ -241,11 +267,11 @@ public boolean isIsNullParameter() { @Nullable public Object prepare(@Nullable Object value) { - if (value == null || expression.getJavaType() == null) { + if (value == null || parameterType == null) { return value; } - if (String.class.equals(expression.getJavaType()) && !noWildcards) { + if (String.class.equals(parameterType) && !noWildcards) { switch (type) { case STARTING_WITH: @@ -260,8 +286,8 @@ public Object prepare(@Nullable Object value) { } } - return Collection.class.isAssignableFrom(expression.getJavaType()) // - ? upperIfIgnoreCase(ignoreCase, toCollection(value)) // + return Collection.class.isAssignableFrom(parameterType) // + ? potentiallyIgnoreCase(ignoreCase, toCollection(value)) // : value; } @@ -295,7 +321,7 @@ private static Collection toCollection(@Nullable Object value) { @Nullable @SuppressWarnings("unchecked") - private static Collection upperIfIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { + private Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { if (!ignoreCase || CollectionUtils.isEmpty(collection)) { return collection; @@ -304,8 +330,9 @@ private static Collection upperIfIgnoreCase(boolean ignoreCase, @Nullable Col return ((Collection) collection).stream() // .map(it -> it == null // ? null // - : it.toUpperCase()) // + : templates.ignoreCase(it)) // .collect(Collectors.toList()); } + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index a1246ac056..8848303b8d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -18,12 +18,13 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceUnitUtil; import jakarta.persistence.Query; +import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; +import java.util.LinkedHashMap; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; +import java.util.Map; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -33,8 +34,8 @@ import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ScrollExecution; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.Part; @@ -42,6 +43,7 @@ import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * A {@link AbstractJpaQuery} implementation based on a {@link PartTree}. @@ -55,6 +57,8 @@ */ public class PartTreeJpaQuery extends AbstractJpaQuery { + private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; + private final PartTree tree; private final JpaParameters parameters; @@ -93,15 +97,12 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil(); this.entityInformation = new JpaMetamodelEntityInformation<>(domainClass, em.getMetamodel(), persistenceUnitUtil); - boolean recreationRequired = parameters.hasDynamicProjection() || parameters.potentiallySortsDynamically() - || method.isScrollQuery(); - try { this.tree = new PartTree(method.getName(), domainClass); validate(tree, parameters, method.toString()); - this.countQuery = new CountQueryPreparer(recreationRequired); - this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(recreationRequired); + this.countQuery = new CountQueryPreparer(); + this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(); } catch (Exception o_O) { throw new IllegalArgumentException( @@ -200,6 +201,7 @@ private static boolean expectsCollection(Type type) { return type == Type.IN || type == Type.NOT_IN; } + /** * Query preparer to create {@link CriteriaQuery} instances and potentially cache them. * @@ -208,50 +210,35 @@ private static boolean expectsCollection(Type type) { */ private class QueryPreparer { - private final @Nullable CriteriaQuery cachedCriteriaQuery; - private final ReentrantLock lock = new ReentrantLock(); - private final @Nullable ParameterBinder cachedParameterBinder; - private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); - - QueryPreparer(boolean recreateQueries) { - - JpaQueryCreator creator = createCreator(null); - - if (recreateQueries) { - this.cachedCriteriaQuery = null; - this.cachedParameterBinder = null; - } else { - this.cachedCriteriaQuery = creator.createQuery(); - this.cachedParameterBinder = getBinder(creator.getParameterExpressions()); + private final Map cache = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 256; } - } + }; /** * Creates a new {@link Query} for the given parameter values. */ public Query createQuery(JpaParametersParameterAccessor accessor) { - CriteriaQuery criteriaQuery = cachedCriteriaQuery; - ParameterBinder parameterBinder = cachedParameterBinder; + Sort sort = getDynamicSort(accessor); + JpqlQueryCreator creator = createCreator(sort, accessor); + String jpql = creator.createQuery(sort); + Query query; - if (cachedCriteriaQuery == null || accessor.hasBindableNullValue()) { - JpaQueryCreator creator = createCreator(accessor); - criteriaQuery = creator.createQuery(getDynamicSort(accessor)); - List> expressions = creator.getParameterExpressions(); - parameterBinder = getBinder(expressions); + try { + query = creator.useTupleQuery() ? em.createQuery(jpql, Tuple.class) : em.createQuery(jpql); + } catch (Exception e) { + throw new BadJpqlGrammarException(e.getMessage(), jpql, e); } - if (parameterBinder == null) { - throw new IllegalStateException("ParameterBinder is null"); - } - - TypedQuery query = createQuery(criteriaQuery); + ParameterBinder binder = creator.getBinder(); ScrollPosition scrollPosition = accessor.getParameters().hasScrollPositionParameter() ? accessor.getScrollPosition() : null; - return restrictMaxResultsIfNecessary(invokeBinding(parameterBinder, query, accessor, this.metadataCache), - scrollPosition); + return restrictMaxResultsIfNecessary(invokeBinding(binder, query, accessor), scrollPosition); } /** @@ -289,65 +276,85 @@ private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPositio return query; } - /** - * Checks whether we are working with a cached {@link CriteriaQuery} and synchronizes the creation of a - * {@link TypedQuery} instance from it. This is due to non-thread-safety in the {@link CriteriaQuery} implementation - * of some persistence providers (i.e. Hibernate in this case), see DATAJPA-396. - * - * @param criteriaQuery must not be {@literal null}. - */ - private TypedQuery createQuery(CriteriaQuery criteriaQuery) { - - if (this.cachedCriteriaQuery != null) { - lock.lock(); - try { - return getEntityManager().createQuery(criteriaQuery); - } finally { - lock.unlock(); + protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) { + + synchronized (cache) { + JpqlQueryCreator jpqlQueryCreator = cache.get(sort); + if (jpqlQueryCreator != null) { + return jpqlQueryCreator; } } - return getEntityManager().createQuery(criteriaQuery); + EntityManager entityManager = getEntityManager(); + ResultProcessor processor = getQueryMethod().getResultProcessor(); + + ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates); + ReturnedType returnedType = processor.withDynamicProjection(accessor).getReturnedType(); + + if (accessor.getScrollPosition() instanceof KeysetScrollPosition keyset) { + return new JpaKeysetScrollQueryCreator(tree, returnedType, provider, templates, entityInformation, keyset, + entityManager); + } + + JpqlQueryCreator creator = new CacheableJpqlQueryCreator(sort, + new JpaQueryCreator(tree, returnedType, provider, templates, em)); + + if (accessor.getParameters().hasDynamicProjection()) { + return creator; + } + + synchronized (cache) { + cache.put(sort, creator); + } + + return creator; } - protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) { + static class CacheableJpqlQueryCreator implements JpqlQueryCreator { - EntityManager entityManager = getEntityManager(); + private final Sort expectedSort; + private final String query; + private final boolean useTupleQuery; + private final List parameterBindings; + private final ParameterBinder binder; - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - ResultProcessor processor = getQueryMethod().getResultProcessor(); + public CacheableJpqlQueryCreator(Sort expectedSort, JpqlQueryCreator delegate) { - ParameterMetadataProvider provider; - ReturnedType returnedType; + this.expectedSort = expectedSort; + this.query = delegate.createQuery(expectedSort); + this.useTupleQuery = delegate.useTupleQuery(); + this.parameterBindings = delegate.getBindings(); + this.binder = delegate.getBinder(); + } + + @Override + public boolean useTupleQuery() { + return useTupleQuery; + } + + @Override + public String createQuery(Sort sort) { - if (accessor != null) { - provider = new ParameterMetadataProvider(builder, accessor, escape); - returnedType = processor.withDynamicProjection(accessor).getReturnedType(); - } else { - provider = new ParameterMetadataProvider(builder, parameters, escape); - returnedType = processor.getReturnedType(); + Assert.isTrue(sort.equals(expectedSort), "Expected sort does not match"); + return query; } - if (accessor != null && accessor.getScrollPosition() instanceof KeysetScrollPosition keyset) { - return new JpaKeysetScrollQueryCreator(tree, returnedType, builder, provider, entityInformation, keyset); + @Override + public List getBindings() { + return parameterBindings; } - return new JpaQueryCreator(tree, returnedType, builder, provider); + @Override + public ParameterBinder getBinder() { + return binder; + } } /** * Invokes parameter binding on the given {@link TypedQuery}. */ - protected Query invokeBinding(ParameterBinder binder, TypedQuery query, JpaParametersParameterAccessor accessor, - QueryParameterSetter.QueryMetadataCache metadataCache) { - - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("query", query); - - return binder.bindAndPrepare(query, metadata, accessor); - } - - private ParameterBinder getBinder(List> expressions) { - return ParameterBinderFactory.createCriteriaBinder(parameters, expressions); + protected Query invokeBinding(ParameterBinder binder, Query query, JpaParametersParameterAccessor accessor) { + return binder.bindAndPrepare(query, accessor); } private Sort getDynamicSort(JpaParametersParameterAccessor accessor) { @@ -366,37 +373,70 @@ private Sort getDynamicSort(JpaParametersParameterAccessor accessor) { */ private class CountQueryPreparer extends QueryPreparer { - CountQueryPreparer(boolean recreateQueries) { - super(recreateQueries); - } + private volatile JpqlQueryCreator cached; @Override - protected JpaQueryCreator createCreator(@Nullable JpaParametersParameterAccessor accessor) { + protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) { - EntityManager entityManager = getEntityManager(); - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + JpqlQueryCreator cached = this.cached; + + if (cached != null) { + return cached; + } - ParameterMetadataProvider provider; + ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates); + JpaCountQueryCreator creator = new JpaCountQueryCreator(tree, + getQueryMethod().getResultProcessor().getReturnedType(), provider, templates, em); - if (accessor != null) { - provider = new ParameterMetadataProvider(builder, accessor, escape); - } else { - provider = new ParameterMetadataProvider(builder, parameters, escape); + if (!accessor.getParameters().hasDynamicProjection()) { + return this.cached = new CacheableJpqlCountQueryCreator(creator); } - return new JpaCountQueryCreator(tree, getQueryMethod().getResultProcessor().getReturnedType(), builder, provider); + return creator; } /** * Customizes binding by skipping the pagination. */ @Override - protected Query invokeBinding(ParameterBinder binder, TypedQuery query, JpaParametersParameterAccessor accessor, - QueryParameterSetter.QueryMetadataCache metadataCache) { + protected Query invokeBinding(ParameterBinder binder, Query query, JpaParametersParameterAccessor accessor) { + return binder.bind(query, accessor); + } + + static class CacheableJpqlCountQueryCreator implements JpqlQueryCreator { + + private final String query; + private final boolean useTupleQuery; + private final List parameterBindings; + private final ParameterBinder binder; + + public CacheableJpqlCountQueryCreator(JpqlQueryCreator delegate) { + + this.query = delegate.createQuery(Sort.unsorted()); + this.useTupleQuery = delegate.useTupleQuery(); + this.parameterBindings = delegate.getBindings(); + this.binder = delegate.getBinder(); + } - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("countquery", query); + @Override + public boolean useTupleQuery() { + return useTupleQuery; + } + + @Override + public String createQuery(Sort sort) { + return query; + } - return binder.bind(query, metadata, accessor); + @Override + public List getBindings() { + return parameterBindings; + } + + @Override + public ParameterBinder getBinder() { + return binder; + } } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java index 727f61cc81..d88589d6ef 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java @@ -23,17 +23,16 @@ import jakarta.persistence.criteria.ParameterExpression; import java.lang.reflect.Proxy; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.Set; import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; /** * The interface encapsulates the setting of query parameters which might use a significant number of variations of @@ -45,158 +44,159 @@ */ interface QueryParameterSetter { - void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandling errorHandling); - /** Noop implementation */ - QueryParameterSetter NOOP = (query, values, errorHandling) -> {}; + QueryParameterSetter NOOP = (query, values, errorHandler) -> {}; + + /** + * Creates a new {@link QueryParameterSetter} for the given value extractor, JPA parameter and potentially the + * temporal type. + * + * @param valueExtractor + * @param parameter + * @param temporalType + * @return + */ + static QueryParameterSetter create(Function valueExtractor, + Parameter parameter, @Nullable TemporalType temporalType) { + + return temporalType == null ? new NamedOrIndexedQueryParameterSetter(valueExtractor, parameter) + : new TemporalParameterSetter(valueExtractor, parameter, temporalType); + } + + void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandler errorHandler); /** - * {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified. + * {@link QueryParameterSetter} for named or indexed parameters. */ class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter { private final Function valueExtractor; private final Parameter parameter; - private final @Nullable TemporalType temporalType; /** * @param valueExtractor must not be {@literal null}. * @param parameter must not be {@literal null}. - * @param temporalType may be {@literal null}. */ - NamedOrIndexedQueryParameterSetter(Function valueExtractor, - Parameter parameter, @Nullable TemporalType temporalType) { + private NamedOrIndexedQueryParameterSetter(Function valueExtractor, + Parameter parameter) { Assert.notNull(valueExtractor, "ValueExtractor must not be null"); this.valueExtractor = valueExtractor; this.parameter = parameter; - this.temporalType = temporalType; } - @SuppressWarnings("unchecked") @Override - public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, - ErrorHandling errorHandling) { + public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandler errorHandler) { - if (temporalType != null) { + Object value = valueExtractor.apply(accessor); - Object extractedValue = valueExtractor.apply(accessor); - - Date value = (Date) accessor.potentiallyUnwrap(extractedValue); + try { + setParameter(query, value, errorHandler); + } catch (RuntimeException e) { + errorHandler.handleError(e); + } + } - // One would think we can simply use parameter to identify the parameter we want to set. - // But that does not work with list valued parameters. At least Hibernate tries to bind them by name. - // TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is - // fixed. + @SuppressWarnings("unchecked") + private void setParameter(BindableQuery query, Object value, ErrorHandler errorHandler) { - if (parameter instanceof ParameterExpression) { - errorHandling.execute(() -> query.setParameter((Parameter) parameter, value, temporalType)); - } else if (query.hasNamedParameters() && parameter.getName() != null) { - errorHandling.execute(() -> query.setParameter(parameter.getName(), value, temporalType)); - } else { + if (parameter instanceof ParameterExpression) { + query.setParameter((Parameter) parameter, value); + } else if (query.hasNamedParameters() && parameter.getName() != null) { + query.setParameter(parameter.getName(), value); - Integer position = parameter.getPosition(); + } else { - if (position != null // - && (query.getParameters().size() >= parameter.getPosition() // - || query.registerExcessParameters() // - || errorHandling == LENIENT)) { + Integer position = parameter.getPosition(); - errorHandling.execute(() -> query.setParameter(parameter.getPosition(), value, temporalType)); - } + if (position != null // + && (query.getParameters().size() >= position // + || errorHandler == LENIENT // + || query.registerExcessParameters())) { + query.setParameter(position, value); } + } + } + } - } else { + /** + * {@link QueryParameterSetter} for named or indexed parameters that have a {@link TemporalType} specified. + */ + class TemporalParameterSetter implements QueryParameterSetter { + + private final Function valueExtractor; + private final Parameter parameter; + private final TemporalType temporalType; + + private TemporalParameterSetter(Function valueExtractor, + Parameter parameter, TemporalType temporalType) { + this.valueExtractor = valueExtractor; + this.parameter = parameter; + this.temporalType = temporalType; + } + + @Override + public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor, ErrorHandler errorHandler) { + + Date value = (Date) accessor.potentiallyUnwrap(valueExtractor.apply(accessor)); + + try { + setParameter(query, value, errorHandler); + } catch (RuntimeException e) { + errorHandler.handleError(e); + } + } + + @SuppressWarnings("unchecked") + private void setParameter(BindableQuery query, Date date, ErrorHandler errorHandler) { - Object value = valueExtractor.apply(accessor); + // One would think we can simply use parameter to identify the parameter we want to set. + // But that does not work with list valued parameters. At least Hibernate tries to bind them by name. + // TODO: move to using setParameter(Parameter, value) when https://hibernate.atlassian.net/browse/HHH-11870 is + // fixed. - if (parameter instanceof ParameterExpression) { - errorHandling.execute(() -> query.setParameter((Parameter) parameter, value)); - } else if (query.hasNamedParameters() && parameter.getName() != null) { - errorHandling.execute(() -> query.setParameter(parameter.getName(), value)); + if (parameter instanceof ParameterExpression) { + query.setParameter((Parameter) parameter, date, temporalType); + } else if (query.hasNamedParameters() && parameter.getName() != null) { + query.setParameter(parameter.getName(), date, temporalType); + } else { - } else { + Integer position = parameter.getPosition(); - Integer position = parameter.getPosition(); + if (position != null // + && (query.getParameters().size() >= parameter.getPosition() // + || query.registerExcessParameters() // + || errorHandler == LENIENT)) { - if (position != null // - && (query.getParameters().size() >= position // - || errorHandling == LENIENT // - || query.registerExcessParameters())) { - errorHandling.execute(() -> query.setParameter(position, value)); - } + query.setParameter(parameter.getPosition(), date, temporalType); } } } } - enum ErrorHandling { + enum ErrorHandling implements ErrorHandler { STRICT { @Override - public void execute(Runnable block) { - block.run(); + public void handleError(Throwable t) { + if (t instanceof RuntimeException rx) { + throw rx; + } + throw new RuntimeException(t); } }, LENIENT { @Override - public void execute(Runnable block) { - - try { - block.run(); - } catch (RuntimeException rex) { - LOG.info("Silently ignoring", rex); - } + public void handleError(Throwable t) { + LOG.info("Silently ignoring", t); } }; private static final Log LOG = LogFactory.getLog(ErrorHandling.class); - - abstract void execute(Runnable block); - } - - /** - * Cache for {@link QueryMetadata}. Optimizes for small cache sizes on a best-effort basis. - */ - class QueryMetadataCache { - - private Map cache = Collections.emptyMap(); - - /** - * Retrieve the {@link QueryMetadata} for a given {@code cacheKey}. - * - * @param cacheKey - * @param query - * @return - */ - public QueryMetadata getMetadata(String cacheKey, Query query) { - - QueryMetadata queryMetadata = cache.get(cacheKey); - - if (queryMetadata == null) { - - queryMetadata = new QueryMetadata(query); - - Map cache; - - if (this.cache.isEmpty()) { - cache = Collections.singletonMap(cacheKey, queryMetadata); - } else { - cache = new HashMap<>(this.cache); - cache.put(cacheKey, queryMetadata); - } - - synchronized (this) { - this.cache = cache; - } - } - - return queryMetadata; - } } /** @@ -224,23 +224,6 @@ class QueryMetadata { && unwrapClass(query).getName().startsWith("org.eclipse"); } - QueryMetadata(QueryMetadata metadata) { - - this.namedParameters = metadata.namedParameters; - this.parameters = metadata.parameters; - this.registerExcessParameters = metadata.registerExcessParameters; - } - - /** - * Create a {@link BindableQuery} for a {@link Query}. - * - * @param query - * @return - */ - public BindableQuery withQuery(Query query) { - return new BindableQuery(this, query); - } - /** * @return */ @@ -294,13 +277,7 @@ class BindableQuery extends QueryMetadata { private final Query query; private final Query unwrapped; - BindableQuery(QueryMetadata metadata, Query query) { - super(metadata); - this.query = query; - this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query; - } - - private BindableQuery(Query query) { + BindableQuery(Query query) { super(query); this.query = query; this.unwrapped = Proxy.isProxyClass(query.getClass()) ? query.unwrap(null) : query; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index c45c3d8aa3..9e5c378621 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -18,7 +18,6 @@ import jakarta.persistence.Query; import jakarta.persistence.TemporalType; -import java.util.List; import java.util.function.Function; import org.springframework.data.expression.ValueEvaluationContext; @@ -28,8 +27,6 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; -import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.spel.EvaluationContextProvider; @@ -49,36 +46,45 @@ */ abstract class QueryParameterSetterFactory { + /** + * Creates a {@link QueryParameterSetter} for the given {@link ParameterBinding}. This factory may return + * {@literal null} if it doesn't support the given {@link ParameterBinding}. + * + * @param binding the parameter binding to create a {@link QueryParameterSetter} for. + * @return + */ @Nullable - abstract QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery); + abstract QueryParameterSetter create(ParameterBinding binding); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. * * @param parameters must not be {@literal null}. + * @param preferNamedParameters whether to prefer named parameters. * @return a basic {@link QueryParameterSetterFactory} that can handle named and index parameters. */ - static QueryParameterSetterFactory basic(JpaParameters parameters) { - - Assert.notNull(parameters, "JpaParameters must not be null"); - - return new BasicQueryParameterSetterFactory(parameters); + static QueryParameterSetterFactory basic(JpaParameters parameters, boolean preferNamedParameters) { + return new BasicQueryParameterSetterFactory(parameters, preferNamedParameters); } /** - * Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters} and - * {@link ParameterMetadata}. + * Creates a new {@link QueryParameterSetterFactory} using the given {@link JpaParameters}. * * @param parameters must not be {@literal null}. - * @param metadata must not be {@literal null}. - * @return a {@link QueryParameterSetterFactory} for criteria Queries. + * @return a {@link QueryParameterSetterFactory} for Part-Tree Queries. */ - static QueryParameterSetterFactory forCriteriaQuery(JpaParameters parameters, List> metadata) { - - Assert.notNull(parameters, "JpaParameters must not be null"); - Assert.notNull(metadata, "ParameterMetadata must not be null"); + static QueryParameterSetterFactory forPartTreeQuery(JpaParameters parameters) { + return new PartTreeQueryParameterSetterFactory(parameters); + } - return new CriteriaQueryParameterSetterFactory(parameters, metadata); + /** + * Creates a new {@link QueryParameterSetterFactory} to bind + * {@link org.springframework.data.jpa.repository.query.ParameterBinding.Synthetic} parameters. + * + * @return a {@link QueryParameterSetterFactory} for JPQL Queries. + */ + static QueryParameterSetterFactory forSynthetic() { + return new SyntheticParameterSetterFactory(); } /** @@ -93,10 +99,6 @@ static QueryParameterSetterFactory forCriteriaQuery(JpaParameters parameters, Li */ static QueryParameterSetterFactory parsing(ValueExpressionParser parser, ValueEvaluationContextProvider evaluationContextProvider) { - - Assert.notNull(parser, "ValueExpressionParser must not be null"); - Assert.notNull(evaluationContextProvider, "ValueEvaluationContextProvider must not be null"); - return new ExpressionBasedQueryParameterSetterFactory(parser, evaluationContextProvider); } @@ -115,7 +117,7 @@ private static QueryParameterSetter createSetter(Function s.value(), binding, null); + } + } + /** * Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters. * @@ -217,30 +238,33 @@ private Object evaluateExpression(ValueExpression expression, JpaParametersParam private static class BasicQueryParameterSetterFactory extends QueryParameterSetterFactory { private final JpaParameters parameters; + private final boolean preferNamedParameters; /** * @param parameters must not be {@literal null}. + * @param preferNamedParameters whether to use named parameters. */ - BasicQueryParameterSetterFactory(JpaParameters parameters) { + BasicQueryParameterSetterFactory(JpaParameters parameters, boolean preferNamedParameters) { Assert.notNull(parameters, "JpaParameters must not be null"); this.parameters = parameters; + this.preferNamedParameters = preferNamedParameters; } @Override - public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { + public QueryParameterSetter create(ParameterBinding binding) { Assert.notNull(binding, "Binding must not be null"); - JpaParameter parameter; if (!(binding.getOrigin() instanceof MethodInvocationArgument mia)) { - return QueryParameterSetter.NOOP; + return null; } BindingIdentifier identifier = mia.identifier(); + JpaParameter parameter; - if (declaredQuery.hasNamedParameter() && identifier.hasName()) { + if (preferNamedParameters && identifier.hasName()) { parameter = findParameterForBinding(parameters, identifier.getName()); } else if (identifier.hasPosition()) { parameter = findParameterForBinding(parameters, identifier.getPosition() - 1); @@ -255,7 +279,7 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla } @Nullable - private Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { + protected Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { return accessor.getValue(parameter); } } @@ -263,60 +287,46 @@ private Object getValue(JpaParametersParameterAccessor accessor, Parameter param /** * @author Jens Schauder * @author Oliver Gierke + * @author Mark Paluch * @see QueryParameterSetterFactory */ - private static class CriteriaQueryParameterSetterFactory extends QueryParameterSetterFactory { + private static class PartTreeQueryParameterSetterFactory extends BasicQueryParameterSetterFactory { private final JpaParameters parameters; - private final List> parameterMetadata; - /** - * Creates a new {@link QueryParameterSetterFactory} from the given {@link JpaParameters} and - * {@link ParameterMetadata}. - * - * @param parameters must not be {@literal null}. - * @param metadata must not be {@literal null}. - */ - CriteriaQueryParameterSetterFactory(JpaParameters parameters, List> metadata) { - - Assert.notNull(parameters, "JpaParameters must not be null"); - Assert.notNull(metadata, "Expressions must not be null"); - - this.parameters = parameters; - this.parameterMetadata = metadata; + private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { + super(parameters, false); + this.parameters = parameters.getBindableParameters(); } @Override - public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery declaredQuery) { + public QueryParameterSetter create(ParameterBinding binding) { + + if (!binding.getOrigin().isMethodArgument()) { + return null; + } int parameterIndex = binding.getRequiredPosition() - 1; Assert.isTrue( // - parameterIndex < parameterMetadata.size(), // + parameterIndex < parameters.getNumberOfParameters(), // () -> String.format( // "At least %s parameter(s) provided but only %s parameter(s) present in query", // binding.getRequiredPosition(), // - parameterMetadata.size() // + parameters.getNumberOfParameters() // ) // ); - ParameterMetadata metadata = parameterMetadata.get(parameterIndex); + if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) { - if (metadata.isIsNullParameter()) { - return QueryParameterSetter.NOOP; - } + if (ptb.isIsNullParameter()) { + return QueryParameterSetter.NOOP; + } - JpaParameter parameter = parameters.getBindableParameter(parameterIndex); - TemporalType temporalType = parameter.isTemporalParameter() ? parameter.getRequiredTemporalType() : null; + return super.create(binding); + } - return new NamedOrIndexedQueryParameterSetter(values -> getAndPrepare(parameter, metadata, values), - metadata.getExpression(), temporalType); - } - - @Nullable - private Object getAndPrepare(JpaParameter parameter, ParameterMetadata metadata, - JpaParametersParameterAccessor accessor) { - return metadata.prepare(accessor.getValue(parameter)); + return null; } } @@ -360,7 +370,6 @@ public Integer getPosition() { public Class getParameterType() { return parameterType; } - } } 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 371dc0b6cc..9922c47150 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 @@ -801,7 +801,7 @@ static Expression toExpressionRecursively(From from, PropertyPath p * @param hasRequiredOuterJoin has a parent already required an outer join? * @return whether an outer join is to be used for integrating this attribute in a query. */ - private static boolean requiresOuterJoin(From from, PropertyPath property, boolean isForSelection, + static boolean requiresOuterJoin(From from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { // already inner joined so outer join is useless @@ -871,7 +871,7 @@ private static T getAnnotationProperty(Attribute attribute, String pro * @param joinType the join type to create if none was found * @return will never be {@literal null}. */ - private static Join getOrCreateJoin(From from, String attribute, JoinType joinType) { + static Join getOrCreateJoin(From from, String attribute, JoinType joinType) { for (Fetch fetch : from.getFetches()) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java index 8ff29f4ba2..e91ffbffb1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java @@ -50,7 +50,6 @@ class StoredProcedureJpaQuery extends AbstractJpaQuery { private final StoredProcedureAttributes procedureAttributes; private final boolean useNamedParameters; - private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache(); /** * Creates a new {@link StoredProcedureJpaQuery}. @@ -90,9 +89,7 @@ protected StoredProcedureQuery createQuery(JpaParametersParameterAccessor access protected StoredProcedureQuery doCreateQuery(JpaParametersParameterAccessor accessor) { StoredProcedureQuery storedProcedure = createStoredProcedure(); - QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata("singleton", storedProcedure); - - return parameterBinder.get().bind(storedProcedure, metadata, accessor); + return parameterBinder.get().bind(storedProcedure, accessor); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java new file mode 100644 index 0000000000..24180ae6fc --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.support; + +import java.util.function.Function; + +/** + * @author Mark Paluch + */ +public class JpqlQueryTemplates { + + public static final JpqlQueryTemplates UPPER = new JpqlQueryTemplates("UPPER", String::toUpperCase); + + public static final JpqlQueryTemplates LOWER = new JpqlQueryTemplates("LOWER", String::toLowerCase); + + private final String ignoreCaseOperator; + + private final Function ignoreCaseFunction; + + JpqlQueryTemplates(String ignoreCaseOperator, Function ignoreCaseFunction) { + this.ignoreCaseOperator = ignoreCaseOperator; + this.ignoreCaseFunction = ignoreCaseFunction; + } + + public static JpqlQueryTemplates of(String ignoreCaseOperator, Function ignoreCaseFunction) { + return new JpqlQueryTemplates(ignoreCaseOperator, ignoreCaseFunction); + } + + public String ignoreCase(String value) { + return ignoreCaseFunction.apply(value); + } + + public String getIgnoreCaseOperator() { + return ignoreCaseOperator; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java index 8593c1ed3e..31d4a44d42 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java @@ -36,6 +36,10 @@ void executesNotInQueryCorrectly() {} @Override void executesInKeywordForPageCorrectly() {} + @Disabled + @Override + void shouldProjectWithKeysetScrolling() {} + @Disabled @Override void rawMapProjectionWithEntityAndAggregatedValue() {} 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 ab620ee482..7feedd68fc 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 @@ -3319,6 +3319,15 @@ void findByElementCollectionInAttributeIgnoreCase() { flushTestUsers(); + /* + TODO: Hibernate-generated HQL for the CriteriaBuilder-based API. Yields only one result in contrast to the CriteriaBuilder one. + Query query = em.createQuery("select alias_544097980 from org.springframework.data.jpa.domain.sample.User alias_544097980 left join alias_544097980.attributes alias_975381534 where alias_975381534 in (?1)") + .setParameter(1, asList("cOOl", "hIP")); + + List resultList = query.getResultList(); + + */ + List result = repository.findByAttributesIgnoreCaseIn(new HashSet<>(asList("cOOl", "hIP"))); assertThat(result).containsOnly(firstUser, secondUser); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java index 9afcf27d56..d44c40301c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java @@ -19,21 +19,17 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import jakarta.persistence.TypedQuery; import java.lang.reflect.Method; import java.util.List; -import org.hibernate.query.spi.SqmQuery; -import org.hibernate.query.sqm.tree.expression.SqmDistinct; -import org.hibernate.query.sqm.tree.expression.SqmFunction; -import org.hibernate.query.sqm.tree.select.SqmSelectClause; -import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; @@ -63,25 +59,15 @@ void distinctFlagOnCountQueryIssuesCountDistinct() throws Exception { AbstractRepositoryMetadata.getMetadata(SomeRepository.class), new SpelAwareProxyProjectionFactory(), provider); PartTree tree = new PartTree("findDistinctByRolesIn", User.class); - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(entityManager.getCriteriaBuilder(), - queryMethod.getParameters(), EscapeCharacter.DEFAULT); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider( + queryMethod.getParameters(), EscapeCharacter.DEFAULT, JpqlQueryTemplates.UPPER); JpaCountQueryCreator creator = new JpaCountQueryCreator(tree, queryMethod.getResultProcessor().getReturnedType(), - entityManager.getCriteriaBuilder(), metadataProvider); - - TypedQuery query = entityManager.createQuery(creator.createQuery()); - - SqmQuery sqmQuery = ((SqmQuery) query); - SqmSelectStatement select = (SqmSelectStatement) sqmQuery.getSqmStatement(); + metadataProvider, JpqlQueryTemplates.UPPER, entityManager); - // Verify distinct (should this even be there for a count query?) - SqmSelectClause clause = select.getQuerySpec().getSelectClause(); - assertThat(clause.isDistinct()).isTrue(); + String query = creator.createQuery(); - // Verify count(distinct(…)) - SqmFunction function = ((SqmFunction) clause.getSelectionItems().get(0)); - assertThat(function.getFunctionName()).isEqualTo("count"); - assertThat(function.getArguments().get(0)).isInstanceOf(SqmDistinct.class); + assertThat(query).startsWith("SELECT COUNT(DISTINCT u)"); } interface SomeRepository extends Repository { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java new file mode 100644 index 0000000000..2221d3a87a --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Unit tests for {@link JpaKeysetScrollQueryCreator}. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration("classpath:infrastructure.xml") +class JpaKeysetScrollQueryCreatorTests { + + @PersistenceContext EntityManager entityManager; + + @Test // GH-3588 + void shouldCreateContinuationQuery() throws Exception { + + Map keys = Map.of("id", "10", "firstname", "John", "emailAddress", "john@example.com"); + KeysetScrollPosition position = ScrollPosition.of(keys, ScrollPosition.Direction.BACKWARD); + + Method method = MyRepo.class.getMethod("findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc", + String.class, ScrollPosition.class); + + PersistenceProvider provider = PersistenceProvider.fromEntityManager(entityManager); + JpaQueryMethod queryMethod = new JpaQueryMethod(method, AbstractRepositoryMetadata.getMetadata(MyRepo.class), + new SpelAwareProxyProjectionFactory(), provider); + + PartTree tree = new PartTree("findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc", User.class); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider( + queryMethod.getParameters(), EscapeCharacter.DEFAULT, JpqlQueryTemplates.UPPER); + + JpaMetamodelEntityInformation entityInformation = new JpaMetamodelEntityInformation<>(User.class, + entityManager.getMetamodel(), entityManager.getEntityManagerFactory().getPersistenceUnitUtil()); + JpaKeysetScrollQueryCreator creator = new JpaKeysetScrollQueryCreator(tree, + queryMethod.getResultProcessor().getReturnedType(), metadataProvider, JpqlQueryTemplates.UPPER, + entityInformation, position, entityManager); + + String query = creator.createQuery(); + + assertThat(query).containsIgnoringWhitespaces(""" + SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE (u.firstname LIKE ?1 ESCAPE '\\') + AND (u.firstname < ?2 + OR u.firstname = ?3 AND u.emailAddress < ?4 + OR u.firstname = ?5 AND u.emailAddress = ?6 AND u.id < ?7) + ORDER BY u.firstname desc, u.emailAddress desc, u.id desc + """); + } + + interface MyRepo extends Repository { + + Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(String firstname, + ScrollPosition position); + + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java index 6d7b55dbf1..0c2727ece4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java @@ -69,7 +69,7 @@ void createsHibernateParametersParameterAccessor() throws Exception { private void bind(JpaParameters parameters, JpaParametersParameterAccessor accessor) { - ParameterBinderFactory.createBinder(parameters) + ParameterBinderFactory.createBinder(parameters, true) .bind( // QueryParameterSetter.BindableQuery.from(query), // accessor, // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java index 6f1692142d..e85ff114f1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java @@ -18,6 +18,7 @@ import static jakarta.persistence.TemporalType.*; import static java.util.Arrays.*; import static org.mockito.Mockito.*; +import static org.springframework.data.jpa.repository.query.QueryParameterSetter.*; import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*; import jakarta.persistence.Parameter; @@ -34,7 +35,8 @@ import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter; + +import org.springframework.data.jpa.repository.query.QueryParameterSetter.*; /** * Unit tests fir {@link NamedOrIndexedQueryParameterSetter}. @@ -79,7 +81,7 @@ void strictErrorHandlingThrowsExceptionForAllVariationsOfParameters() { for (Parameter parameter : parameters) { for (TemporalType temporalType : temporalTypes) { - NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( // + QueryParameterSetter setter = QueryParameterSetter.create( // firstValueExtractor, // parameter, // temporalType // @@ -87,7 +89,7 @@ void strictErrorHandlingThrowsExceptionForAllVariationsOfParameters() { softly .assertThatThrownBy( - () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, STRICT)) // + () -> setter.setParameter(BindableQuery.from(query), methodArguments, STRICT)) // .describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", // parameter.getClass(), // parameter.getName(), // @@ -108,7 +110,7 @@ void lenientErrorHandlingThrowsNoExceptionForAllVariationsOfParameters() { for (Parameter parameter : parameters) { for (TemporalType temporalType : temporalTypes) { - NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( // + QueryParameterSetter setter = QueryParameterSetter.create( // firstValueExtractor, // parameter, // temporalType // @@ -116,7 +118,7 @@ void lenientErrorHandlingThrowsNoExceptionForAllVariationsOfParameters() { softly .assertThatCode( - () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT)) // + () -> setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT)) // .describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", // parameter.getClass(), // parameter.getName(), // @@ -141,13 +143,13 @@ void lenientSetsParameterWhenSuccessIsUnsure() { for (TemporalType temporalType : temporalTypes) { - NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( // + QueryParameterSetter setter = QueryParameterSetter.create( // firstValueExtractor, // new ParameterImpl(null, 11), // parameter position is beyond number of parametes in query (0) temporalType // ); - setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT); + setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT); if (temporalType == null) { verify(query).setParameter(eq(11), any(Date.class)); @@ -171,13 +173,13 @@ void parameterNotSetWhenSuccessImpossible() { for (TemporalType temporalType : temporalTypes) { - NamedOrIndexedQueryParameterSetter setter = new NamedOrIndexedQueryParameterSetter( // + QueryParameterSetter setter = QueryParameterSetter.create( // firstValueExtractor, // new ParameterImpl(null, null), // no position (and no name) makes a success of a setParameter impossible temporalType // ); - setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT); + setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT); if (temporalType == null) { verify(query, never()).setParameter(anyInt(), any(Date.class)); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java index e80d9a8692..360dcf4be1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java @@ -274,13 +274,13 @@ private void bind(Method method, Object[] values) { } private void bind(Method method, JpaParameters parameters, Object[] values) { - ParameterBinderFactory.createBinder(parameters).bind(QueryParameterSetter.BindableQuery.from(query), + ParameterBinderFactory.createBinder(parameters, false).bind(QueryParameterSetter.BindableQuery.from(query), getAccessor(method, values), QueryParameterSetter.ErrorHandling.STRICT); } private void bindAndPrepare(Method method, Object[] values) { - ParameterBinderFactory.createBinder(createParameters(method)).bindAndPrepare(query, - new QueryParameterSetter.QueryMetadata(query), getAccessor(method, values)); + ParameterBinderFactory.createBinder(createParameters(method), false).bindAndPrepare(query, + getAccessor(method, values)); } private JpaParametersParameterAccessor getAccessor(Method method, Object... values) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java deleted file mode 100644 index b706551305..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017-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 static org.assertj.core.api.Assertions.*; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.ParameterExpression; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.data.jpa.domain.sample.User; -import org.springframework.data.repository.query.DefaultParameters; -import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.ParametersSource; -import org.springframework.data.repository.query.parser.Part; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * Integration tests for {@link ParameterMetadataProvider}. - * - * @author Oliver Gierke - * @author Jens Schauder - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration("classpath:infrastructure.xml") -class ParameterExpressionProviderTests { - - @PersistenceContext EntityManager em; - - @Test // DATADOC-99 - @SuppressWarnings("rawtypes") - void createsParameterExpressionWithMostConcreteType() throws Exception { - - Method method = SampleRepository.class.getMethod("findByIdGreaterThan", int.class); - Parameters parameters = new DefaultParameters(ParametersSource.of(method)); - ParametersParameterAccessor accessor = new ParametersParameterAccessor(parameters, new Object[] { 1 }); - Part part = new Part("IdGreaterThan", User.class); - - CriteriaBuilder builder = em.getCriteriaBuilder(); - ParameterMetadataProvider provider = new ParameterMetadataProvider(builder, accessor, EscapeCharacter.DEFAULT); - ParameterExpression expression = provider.next(part, Comparable.class).getExpression(); - - assertThat(expression.getParameterType()).isEqualTo(Integer.class); - } - - interface SampleRepository { - - User findByIdGreaterThan(int id); - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java index c0f86397d3..355a34aff3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java @@ -27,7 +27,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.data.jpa.domain.sample.User; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; @@ -48,14 +48,14 @@ class ParameterMetadataProviderIntegrationTests { @PersistenceContext EntityManager em; - + /* TODO @Test // DATAJPA-758 void forwardsParameterNameIfTransparentlyNamed() throws Exception { ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByFirstname", String.class)); ParameterMetadata metadata = provider.next(new Part("firstname", User.class)); - assertThat(metadata.getExpression().getName()).isEqualTo("name"); + assertThat(metadata.getName()).isEqualTo("name"); } @Test // DATAJPA-758 @@ -65,15 +65,15 @@ void forwardsParameterNameIfExplicitlyAnnotated() throws Exception { ParameterMetadata metadata = provider.next(new Part("lastname", User.class)); assertThat(metadata.getExpression().getName()).isNull(); - } + } */ @Test // DATAJPA-772 void doesNotApplyLikeExpansionOnNonStringProperties() throws Exception { ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByAgeContaining", Integer.class)); - ParameterMetadata metadata = provider.next(new Part("ageContaining", User.class)); + ParameterBinding.PartTreeParameterBinding binding = provider.next(new Part("ageContaining", User.class)); - assertThat(metadata.prepare(1)).isEqualTo(1); + assertThat(binding.prepare(1)).isEqualTo(1); } private ParameterMetadataProvider createProvider(Method method) { @@ -81,7 +81,8 @@ private ParameterMetadataProvider createProvider(Method method) { JpaParameters parameters = new JpaParameters(ParametersSource.of(method)); simulateDiscoveredParametername(parameters); - return new ParameterMetadataProvider(em.getCriteriaBuilder(), parameters, EscapeCharacter.DEFAULT); + return new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, + JpqlQueryTemplates.UPPER); } @SuppressWarnings({ "unchecked", "ConstantConditions" }) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java index 86a4de3ab2..4ad41bfd14 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java @@ -18,8 +18,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import jakarta.persistence.criteria.CriteriaBuilder; - import java.util.Collections; import org.eclipse.persistence.internal.jpa.querydef.ParameterExpressionImpl; @@ -30,7 +28,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.data.repository.query.Parameters; + +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.parser.Part; /** @@ -51,13 +50,11 @@ class ParameterMetadataProviderUnitTests { @Test // DATAJPA-863 void errorMessageMentionsParametersWhenParametersAreExhausted() { - CriteriaBuilder builder = mock(CriteriaBuilder.class); - - Parameters parameters = mock(Parameters.class, RETURNS_DEEP_STUBS); + JpaParameters parameters = mock(JpaParameters.class, RETURNS_DEEP_STUBS); when(parameters.getBindableParameters().iterator()).thenReturn(Collections.emptyListIterator()); - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(builder, parameters, - EscapeCharacter.DEFAULT); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, + EscapeCharacter.DEFAULT, JpqlQueryTemplates.UPPER); assertThatExceptionOfType(RuntimeException.class) // .isThrownBy(() -> metadataProvider.next(mock(Part.class))) // @@ -68,6 +65,7 @@ void errorMessageMentionsParametersWhenParametersAreExhausted() { void returnAugmentedValueForStringExpressions() { when(part.getProperty().getLeafProperty().isCollection()).thenReturn(false); + when(part.getProperty().getType()).thenReturn((Class) String.class); assertThat(createParameterMetadata(Part.Type.STARTING_WITH).prepare("starting with")).isEqualTo("starting with%"); assertThat(createParameterMetadata(Part.Type.ENDING_WITH).prepare("ending with")).isEqualTo("%ending with"); @@ -82,6 +80,6 @@ void returnAugmentedValueForStringExpressions() { private ParameterMetadataProvider.ParameterMetadata createParameterMetadata(Part.Type partType) { when(part.getType()).thenReturn(partType); - return new ParameterMetadataProvider.ParameterMetadata<>(parameterExpression, part, null, EscapeCharacter.DEFAULT); + return new ParameterMetadataProvider.ParameterMetadata(part.getProperty().getType(), part, null, EscapeCharacter.DEFAULT, 1, JpqlQueryTemplates.LOWER); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index 604864545b..69f73f5bc1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -17,9 +17,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.*; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -39,9 +37,11 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.HibernateUtils; import org.springframework.data.jpa.provider.PersistenceProvider; @@ -151,7 +151,7 @@ void isEmptyCollection() throws Exception { Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] {})); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles is empty"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles IS EMPTY"); } @Test // DATAJPA-1074, HHH-15432 @@ -162,7 +162,18 @@ void isNotEmptyCollection() throws Exception { Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] {})); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles is not empty"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("roles IS NOT EMPTY"); + } + + @Test // + void containingCollection() throws Exception { + + JpaQueryMethod queryMethod = getQueryMethod("findByRolesContaining", Role.class); + PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); + + Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { new Role() })); + + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("MEMBER OF u.roles"); } @Test // DATAJPA-1074 @@ -170,7 +181,8 @@ void rejectsIsEmptyOnNonCollectionProperty() throws Exception { JpaQueryMethod method = getQueryMethod("findByFirstnameIsEmpty"); - assertThatIllegalArgumentException().isThrownBy(() -> new PartTreeJpaQuery(method, entityManager)); + assertThatIllegalArgumentException().isThrownBy( + () -> new PartTreeJpaQuery(method, entityManager).createQuery(getAccessor(method, new Object[] {}))); } @Test // DATAJPA-1182 @@ -297,6 +309,8 @@ interface UserRepository extends Repository { List findByFirstnameIsEmpty(); + List findByRolesContaining(Role role); + // should fail, since we can't compare scalar values to collections List findById(Collection ids); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java index 0b35d49b04..4640443b99 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java @@ -18,13 +18,12 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import java.util.Collections; -import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; + import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; @@ -48,12 +47,12 @@ void before() { // we have one bindable parameter when(parameters.getBindableParameters().iterator()).thenReturn(Stream.of(mock(JpaParameter.class)).iterator()); - setterFactory = QueryParameterSetterFactory.basic(parameters); + setterFactory = QueryParameterSetterFactory.basic(parameters, true); } @Test // DATAJPA-1058 void noExceptionWhenQueryDoesNotContainNamedParameters() { - setterFactory.create(binding, DeclaredQuery.of("from Employee e", false)); + setterFactory.create(binding); } @Test // DATAJPA-1058 @@ -62,8 +61,8 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter("NamedParameter", 1)); assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(() -> setterFactory.create(binding, - DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) // + .isThrownBy(() -> setterFactory.create(binding + )) // .withMessageContaining("Java 8") // .withMessageContaining("@Param") // .withMessageContaining("-parameters"); @@ -73,16 +72,14 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { void exceptionWhenCriteriaQueryContainsInsufficientAmountOfParameters() { // no parameter present in the criteria query - List> metadata = Collections.emptyList(); - QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forCriteriaQuery(parameters, metadata); + QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forPartTreeQuery(parameters); // one argument present in the method signature when(binding.getRequiredPosition()).thenReturn(1); when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding, - DeclaredQuery.of("from Employee e where e.name = :NamedParameter", false))) // + .isThrownBy(() -> setterFactory.create(binding)) // .withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query"); } @@ -90,14 +87,14 @@ void exceptionWhenCriteriaQueryContainsInsufficientAmountOfParameters() { void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { // no parameter present in the criteria query - QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters); + QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.basic(parameters, false); // one argument present in the method signature when(binding.getRequiredPosition()).thenReturn(1); when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding, DeclaredQuery.of("from Employee e where e.name = ?1", false))) // + .isThrownBy(() -> setterFactory.create(binding)) // .withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query"); } } diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index e2f6112c82..116676fba7 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -32,7 +32,7 @@ public interface UserRepository extends Repository { List findByEmailAddressAndLastname(String emailAddress, String lastname); } ---- -We create a query using the JPA criteria API from this, but, essentially, this translates into the following query: `select u from User u where u.emailAddress = ?1 and u.lastname = ?2`. Spring Data JPA does a property check and traverses nested properties, as described in xref:repositories/query-methods-details.adoc#repositories.query-methods.query-property-expressions[Property Expressions]. +We create a query using JPQL translating into the following query: `select u from User u where u.emailAddress = ?1 and u.lastname = ?2`. Spring Data JPA does a property check and traverses nested properties, as described in xref:repositories/query-methods-details.adoc#repositories.query-methods.query-property-expressions[Property Expressions]. ==== The following table describes the keywords supported for JPA and what a method containing that keyword translates to: From b88a205b43e83e871bb33ad080252e2fe471386b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 5 Nov 2024 16:17:01 +0100 Subject: [PATCH 05/94] Polishing. Make usage of ParameterExpression more explicit. Add JPQL rendering tests. Favor Metamodel over From for building jpql queries. Align IsNull and IsNotNull handling. Support Derived Delete and Exists, consider null values when caching queries. See #3588 Original pull request: #3653 --- .../query/JpaKeysetScrollQueryCreator.java | 2 +- .../jpa/repository/query/JpaParameters.java | 2 +- .../jpa/repository/query/JpaQueryCreator.java | 94 +- .../repository/query/JpqlQueryBuilder.java | 127 +- .../data/jpa/repository/query/JpqlUtils.java | 167 ++- .../query/KeysetScrollSpecification.java | 16 +- .../repository/query/ParameterBinding.java | 6 +- .../repository/query/PartTreeJpaQuery.java | 17 +- .../repository/query/PartTreeQueryCache.java | 100 ++ .../data/jpa/repository/query/QueryUtils.java | 2 +- .../query/JpaQueryCreatorTests.java | 1039 +++++++++++++++++ .../query/JpqlQueryBuilderUnitTests.java | 265 +++++ .../PartTreeJpaQueryIntegrationTests.java | 2 +- .../query/PartTreeQueryCacheUnitTests.java | 116 ++ .../StubJpaParameterParameterAccessor.java | 93 ++ .../data/jpa/util/TestMetaModel.java | 119 ++ .../test/resources/META-INF/persistence.xml | 8 + 17 files changed, 2085 insertions(+), 90 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java index 7d455e49df..ce0d5a5a1f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java @@ -79,7 +79,7 @@ protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuil JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), value -> { syntheticBindings.add(provider.nextSynthetic(value, scrollPosition)); - return JpqlQueryBuilder.expression(render(counter.incrementAndGet())); + return placeholder(counter.incrementAndGet()); }); JpqlQueryBuilder.Predicate predicateToUse = getPredicate(predicate, keysetPredicate); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java index 6d5244e95a..74f4d84a05 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java @@ -63,7 +63,7 @@ protected JpaParameters(ParametersSource parametersSource, super(parametersSource, parameterFactory); } - private JpaParameters(List parameters) { + JpaParameters(List parameters) { super(parameters); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 44192fac5c..ec3739b3cc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -15,23 +15,30 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.repository.query.parser.Part.Type.*; +import static org.springframework.data.repository.query.parser.Part.Type.IS_NOT_EMPTY; +import static org.springframework.data.repository.query.parser.Part.Type.NOT_CONTAINING; +import static org.springframework.data.repository.query.parser.Part.Type.NOT_LIKE; +import static org.springframework.data.repository.query.parser.Part.Type.SIMPLE_PROPERTY; import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.Bindable; import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.SingularAttribute; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder; import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin; import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; @@ -56,6 +63,7 @@ * @author Moritz Becker * @author Andrey Kovalev * @author Greg Turnquist + * @author Christoph Strobl * @author Jinmyeong Kim */ class JpaQueryCreator extends AbstractQueryCreator implements JpqlQueryCreator { @@ -66,8 +74,8 @@ class JpaQueryCreator extends AbstractQueryCreator entityType; - private final From from; private final JpqlQueryBuilder.Entity entity; + private final Metamodel metamodel; /** * Create a new {@link JpaQueryCreator}. @@ -88,12 +96,12 @@ public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvid this.templates = templates; this.escape = provider.getEscape(); this.entityType = em.getMetamodel().entity(type.getDomainType()); - this.from = em.getCriteriaBuilder().createQuery().from(type.getDomainType()); this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType()); + this.metamodel = em.getMetamodel(); } - From getFrom() { - return from; + Bindable getFrom() { + return entityType; } JpqlQueryBuilder.Entity getEntity() { @@ -175,7 +183,7 @@ protected JpqlQueryBuilder.Select buildQuery(Sort sort) { QueryUtils.checkSortExpression(order); try { - expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, + expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, PropertyPath.from(order.getProperty(), entityType.getJavaType()))); } catch (PropertyReferenceException e) { @@ -210,12 +218,19 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { if (returnedType.needsCustomConstruction()) { - Collection requiredSelection = getRequiredSelection(sort, returnedType); + Collection requiredSelection = null; + if (returnedType.getReturnedType().getPackageName().startsWith("java.util") + || returnedType.getReturnedType().getPackageName().startsWith("jakarta.persistence")) { + requiredSelection = metamodel.managedType(returnedType.getDomainType()).getAttributes().stream() + .map(Attribute::getName).collect(Collectors.toList()); + } else { + requiredSelection = getRequiredSelection(sort, returnedType); + } List paths = new ArrayList<>(requiredSelection.size()); for (String selection : requiredSelection) { - paths.add( - JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(selection, from.getJavaType()), true)); + paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, + PropertyPath.from(selection, returnedType.getDomainType()), true)); } if (useTupleQuery()) { @@ -231,14 +246,14 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { if (entityType.hasSingleIdAttribute()) { SingularAttribute id = entityType.getId(entityType.getIdType().getJavaType()); - return selectStep.select( - JpqlUtils.toExpressionRecursively(entity, from, PropertyPath.from(id.getName(), from.getJavaType()), true)); + return selectStep.select(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, + PropertyPath.from(id.getName(), returnedType.getDomainType()), true)); } else { List paths = entityType.getIdClassAttributes().stream()// - .map(it -> JpqlUtils.toExpressionRecursively(entity, from, - PropertyPath.from(it.getName(), from.getJavaType()), true)) + .map(it -> JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, + PropertyPath.from(it.getName(), returnedType.getDomainType()), true)) .toList(); return selectStep.select(paths); } @@ -255,12 +270,12 @@ Collection getRequiredSelection(Sort sort, ReturnedType returnedType) { return returnedType.getInputProperties(); } - String render(ParameterBinding binding) { - return render(binding.getRequiredPosition()); + JpqlQueryBuilder.Expression placeholder(ParameterBinding binding) { + return placeholder(binding.getRequiredPosition()); } - String render(int position) { - return "?" + position; + JpqlQueryBuilder.Expression placeholder(int position) { + return JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(position)); } /** @@ -305,7 +320,7 @@ public JpqlQueryBuilder.Predicate build() { PropertyPath property = part.getProperty(); Type type = part.getType(); - PathAndOrigin pas = JpqlUtils.toExpressionRecursively(entity, from, property); + PathAndOrigin pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property); JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas); JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas)); @@ -313,25 +328,25 @@ public JpqlQueryBuilder.Predicate build() { case BETWEEN: PartTreeParameterBinding first = provider.next(part); ParameterBinding second = provider.next(part); - return where.between(render(first), render(second)); + return where.between(placeholder(first), placeholder(second)); case AFTER: case GREATER_THAN: - return where.gt(render(provider.next(part))); + return where.gt(placeholder(provider.next(part))); case GREATER_THAN_EQUAL: - return where.gte(render(provider.next(part))); + return where.gte(placeholder(provider.next(part))); case BEFORE: case LESS_THAN: - return where.lt(render(provider.next(part))); + return where.lt(placeholder(provider.next(part))); case LESS_THAN_EQUAL: - return where.lte(render(provider.next(part))); + return where.lte(placeholder(provider.next(part))); case IS_NULL: return where.isNull(); case IS_NOT_NULL: return where.isNotNull(); case NOT_IN: - return whereIgnoreCase.notIn(render(provider.next(part, Collection.class))); + return whereIgnoreCase.notIn(placeholder(provider.next(part, Collection.class))); case IN: - return whereIgnoreCase.in(render(provider.next(part, Collection.class))); + return whereIgnoreCase.in(placeholder(provider.next(part, Collection.class))); case STARTING_WITH: case ENDING_WITH: case CONTAINING: @@ -340,8 +355,8 @@ public JpqlQueryBuilder.Predicate build() { if (property.getLeafProperty().isCollection()) { where = JpqlQueryBuilder.where(entity, property); - return type.equals(NOT_CONTAINING) ? where.notMemberOf(render(provider.next(part))) - : where.memberOf(render(provider.next(part))); + return type.equals(NOT_CONTAINING) ? where.notMemberOf(placeholder(provider.next(part))) + : where.memberOf(placeholder(provider.next(part))); } case LIKE: @@ -349,7 +364,7 @@ public JpqlQueryBuilder.Predicate build() { PartTreeParameterBinding parameter = provider.next(part, String.class); JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(), - JpqlQueryBuilder.parameter(render(parameter))); + placeholder(parameter)); // Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter()); String escapeChar = Character.toString(escape.getEscapeCharacter()); return @@ -362,23 +377,16 @@ public JpqlQueryBuilder.Predicate build() { case FALSE: return where.isFalse(); case SIMPLE_PROPERTY: - PartTreeParameterBinding simple = provider.next(part); - - if (simple.isIsNullParameter()) { - return where.isNull(); - } - - return whereIgnoreCase.eq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(simple)))); case NEGATING_SIMPLE_PROPERTY: - PartTreeParameterBinding negating = provider.next(part); + PartTreeParameterBinding simple = provider.next(part); - if (negating.isIsNullParameter()) { - return where.isNotNull(); + if (simple.isIsNullParameter()) { + return type.equals(SIMPLE_PROPERTY) ? where.isNull() : where.isNotNull(); } - return whereIgnoreCase - .neq(potentiallyIgnoreCase(property, JpqlQueryBuilder.expression(render(negating)))); + JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(metadata)); + return type.equals(SIMPLE_PROPERTY) ? whereIgnoreCase.eq(expression) : whereIgnoreCase.neq(expression); case IS_EMPTY: case IS_NOT_EMPTY: @@ -412,8 +420,8 @@ private JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.O * @param path must not be {@literal null}. * @return */ - private JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin pas) { - return potentiallyIgnoreCase(pas.path(), JpqlQueryBuilder.expression(pas)); + private JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) { + return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path)); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index 42c8ee95d7..cb53998c3f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -15,7 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.*; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_ASC; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DESC; import java.util.ArrayList; import java.util.Arrays; @@ -32,7 +33,9 @@ import org.springframework.data.util.Predicates; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * A Domain-Specific Language to build JPQL queries using Java code. @@ -189,7 +192,7 @@ public static Expression expression(PathAndOrigin pas) { } /** - * Create a simple expression from a string. + * Create a simple expression from a string as is. * * @param expression * @return @@ -201,11 +204,19 @@ public static Expression expression(String expression) { return new LiteralExpression(expression); } + public static Expression stringLiteral(String literal) { + return new StringLiteralExpression(literal); + } + public static Expression parameter(String parameter) { Assert.hasText(parameter, "Parameter must not be empty or null"); - return new ParameterExpression(parameter); + return new ParameterExpression(new ParameterPlaceholder(parameter)); + } + + public static Expression parameter(ParameterPlaceholder placeholder) { + return new ParameterExpression(placeholder); } public static Expression orderBy(Expression sortExpression, Sort.Order order) { @@ -279,12 +290,12 @@ public Predicate isNotNull() { @Override public Predicate isTrue() { - return new LhsPredicate(rhs, "IS TRUE"); + return new LhsPredicate(rhs, "= TRUE"); } @Override public Predicate isFalse() { - return new LhsPredicate(rhs, "IS FALSE"); + return new LhsPredicate(rhs, "= FALSE"); } @Override @@ -309,7 +320,7 @@ public Predicate notIn(Expression value) { @Override public Predicate inMultivalued(Expression value) { - return new MemberOfPredicate(rhs, "IN", value); + return new MemberOfPredicate(rhs, "IN", value); // TODO: that does not line up in my head - ahahah } @Override @@ -466,6 +477,42 @@ public String toString() { } } + static PathAndOrigin path(Origin origin, String path) { + + if(origin instanceof Entity entity) { + + try { + PropertyPath from = PropertyPath.from(path, ClassUtils.forName(entity.entity, Entity.class.getClassLoader())); + return new PathAndOrigin(from, entity, false); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + if(origin instanceof Join join) { + + Origin parent = join.source; + List segments = new ArrayList<>(); + segments.add(join.path); + while(!(parent instanceof Entity)) { + if(parent instanceof Join pj) { + parent = pj.source; + segments.add(pj.path); + } else { + parent = null; + } + } + + if(parent instanceof Entity entity) { + Collections.reverse(segments); + segments.add(path); + PathAndOrigin path1 = path(parent, StringUtils.collectionToDelimitedString(segments, ".")); + return new PathAndOrigin(path1.path().getLeafProperty(), origin, false); + } + } + throw new IllegalStateException(" oh no "); + + } + /** * Entity selection. * @@ -513,7 +560,9 @@ record ConstructorExpression(String resultType, Multiselect multiselect) impleme @Override public String render(RenderContext context) { - return "new %s(%s)".formatted(resultType, multiselect.render(context)); + + + return "new %s(%s)".formatted(resultType, multiselect.render(new ConstructorContext(context))); } @Override @@ -542,7 +591,9 @@ public String render(RenderContext context) { } builder.append(PathExpression.render(path, context)); - builder.append(" ").append(path.path().getSegment()); + if(!context.isConstructorContext()) { + builder.append(" ").append(path.path().getSegment()); + } } return builder.toString(); @@ -583,7 +634,7 @@ default Predicate or(Predicate other) { * @param other * @return a composed predicate combining this and {@code other} using the AND operator. */ - default Predicate and(Predicate other) { + default Predicate and(Predicate other) { // don't like the structuring of this and the nest() thing return new AndPredicate(this, other); } @@ -799,6 +850,22 @@ public String prefixWithAlias(Origin source, String fragment) { String alias = getAlias(source); return ObjectUtils.isEmpty(source) ? fragment : alias + "." + fragment; } + + public boolean isConstructorContext() { + return false; + } + } + + static class ConstructorContext extends RenderContext { + + ConstructorContext(RenderContext rootContext) { + super(rootContext.aliases); + } + + @Override + public boolean isConstructorContext() { + return true; + } } /** @@ -807,7 +874,7 @@ public String prefixWithAlias(Origin source, String fragment) { */ public interface Origin { - String getName(); + String getName(); // TODO: mainly used along records - shoule we call this just name()? } /** @@ -1051,11 +1118,28 @@ public String toString() { } } - record ParameterExpression(String parameter) implements Expression { + record StringLiteralExpression(String literal) implements Expression { @Override public String render(RenderContext context) { - return parameter; + return "'%s'".formatted(literal.replaceAll("'", "''")); + } + + public String raw() { + return literal; + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } + } + + record ParameterExpression(ParameterPlaceholder parameter) implements Expression { + + @Override + public String render(RenderContext context) { + return parameter.placeholder; } @Override @@ -1158,6 +1242,8 @@ record InPredicate(Expression path, String operator, Expression predicate) imple @Override public String render(RenderContext context) { + + //TODO: should we rather wrap it with nested or check if its a nested predicate before we call render return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context)); } @@ -1216,4 +1302,21 @@ public String toString() { public record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) { } + + public record ParameterPlaceholder(String placeholder) { + + public ParameterPlaceholder { + Assert.hasText(placeholder, "Placeholder must not be null nor empty"); + } + + public static ParameterPlaceholder indexed(int index) { + return new ParameterPlaceholder("?%s".formatted(index)); + } + + public static ParameterPlaceholder named(String name) { + + Assert.hasText(name, "Placeholder name must not be empty"); + return new ParameterPlaceholder(":%s".formatted(name)); + } + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java index 50da5558bb..d3b32380cd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -15,27 +15,64 @@ */ package org.springframework.data.jpa.repository.query; +import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ELEMENT_COLLECTION; +import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_MANY; +import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_ONE; +import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_MANY; +import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_ONE; + +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.Attribute.PersistentAttributeType; +import jakarta.persistence.metamodel.Bindable; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.Metamodel; +import jakarta.persistence.metamodel.PluralAttribute; +import jakarta.persistence.metamodel.SingularAttribute; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Member; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.mapping.PropertyPath; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; /** * @author Mark Paluch */ class JpqlUtils { - static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From from, - PropertyPath property) { - return toExpressionRecursively(source, from, property, false); + private static final Map> ASSOCIATION_TYPES; + + static { + Map> persistentAttributeTypes = new HashMap<>(); + persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class); + persistentAttributeTypes.put(ONE_TO_MANY, null); + persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class); + persistentAttributeTypes.put(MANY_TO_MANY, null); + persistentAttributeTypes.put(ELEMENT_COLLECTION, null); + + ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes); } - static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From from, - PropertyPath property, boolean isForSelection) { - return toExpressionRecursively(source, from, property, isForSelection, false); + static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + Bindable from, PropertyPath property) { + return toExpressionRecursively(metamodel, source, from, property, false); + } + + static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + Bindable from, PropertyPath property, boolean isForSelection) { + return toExpressionRecursively(metamodel, source, from, property, isForSelection, false); } /** @@ -45,18 +82,18 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.O * @param property the property path * @param isForSelection is the property navigated for the selection or ordering part of the query? * @param hasRequiredOuterJoin has a parent already required an outer join? - * @param the type of the expression * @return the expression */ @SuppressWarnings("unchecked") - static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.Origin source, From from, - PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { + static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + Bindable from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { String segment = property.getSegment(); boolean isLeafProperty = !property.hasNext(); - boolean requiresOuterJoin = QueryUtils.requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin); + boolean requiresOuterJoin = requiresOuterJoin(metamodel, source, from, property, isForSelection, + hasRequiredOuterJoin); // if it does not require an outer join and is a leaf, simply get the segment if (!requiresOuterJoin && isLeafProperty) { @@ -66,9 +103,10 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.O // get or create the join JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment) : JpqlQueryBuilder.innerJoin(source, segment); - JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER; - Join join = QueryUtils.getOrCreateJoin(from, segment, joinType); +// JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER; +// Join join = QueryUtils.getOrCreateJoin(from, segment, joinType); +// // if it's a leaf, return the join if (isLeafProperty) { return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true); @@ -76,7 +114,110 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(JpqlQueryBuilder.O PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null"); +// ManagedType managedType = ; + Bindable managedTypeForModel = (Bindable) getManagedTypeForModel(from); +// Attribute joinAttribute = getModelForPath(metamodel, property, getManagedTypeForModel(from), null); // recurse with the next property - return toExpressionRecursively(joinSource, join, nextProperty, isForSelection, requiresOuterJoin); + return toExpressionRecursively(metamodel, joinSource, managedTypeForModel, nextProperty, isForSelection, requiresOuterJoin); + } + + /** + * Checks if this attribute requires an outer join. This is the case e.g. if it hadn't already been fetched with an + * inner join and if it's an optional association, and if previous paths has already required outer joins. It also + * ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999) + * + * @param metamodel + * @param source + * @param bindable + * @param propertyPath + * @param isForSelection + * @param hasRequiredOuterJoin + * @return + */ + static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable bindable, + PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) { + + ManagedType managedType = getManagedTypeForModel(bindable); + Attribute attribute = getModelForPath(metamodel, propertyPath, managedType, bindable); + + boolean isPluralAttribute = bindable instanceof PluralAttribute; + if (attribute == null) { + return isPluralAttribute; + } + + if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) { + return false; + } + + boolean isCollection = attribute.isCollection(); + + // if this path is an optional one to one attribute navigated from the not owning side we also need an + // explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712 + // and https://github.com/eclipse-ee4j/jpa-api/issues/170 + boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType() + && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", "")); + + boolean isLeafProperty = !propertyPath.hasNext(); + if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) { + return false; + } + + return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); + } + + @Nullable + private static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { + + Class associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType()); + + if (associationAnnotation == null) { + return defaultValue; + } + + Member member = attribute.getJavaMember(); + + if (!(member instanceof AnnotatedElement annotatedMember)) { + return defaultValue; + } + + Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation); + return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName); + } + + @Nullable + private static ManagedType getManagedTypeForModel(Bindable model) { + + if (model instanceof ManagedType managedType) { + return managedType; + } + + if (!(model instanceof SingularAttribute singularAttribute)) { + return null; + } + + return singularAttribute.getType() instanceof ManagedType managedType ? managedType : null; + } + + @Nullable + private static Attribute getModelForPath(Metamodel metamodel, PropertyPath path, + @Nullable ManagedType managedType, Bindable fallback) { + + String segment = path.getSegment(); + if (managedType != null) { + try { + return managedType.getAttribute(segment); + } catch (IllegalArgumentException ex) { + // ManagedType may be erased for some vendor if the attribute is declared as generic + } + } + + Class fallbackType = fallback.getBindableJavaType(); + try { + return metamodel.managedType(fallbackType).getAttribute(segment); + } catch (IllegalArgumentException e) { + + } + + return null; } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index 59df7353b9..844de60594 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -24,6 +24,8 @@ import java.util.List; +import jakarta.persistence.metamodel.Bindable; +import jakarta.persistence.metamodel.Metamodel; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; @@ -77,11 +79,11 @@ public Predicate createPredicate(Root root, CriteriaBuilder criteriaBuilder) } @Nullable - public JpqlQueryBuilder.Predicate createJpqlPredicate(From from, JpqlQueryBuilder.Entity entity, + public JpqlQueryBuilder.Predicate createJpqlPredicate(Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); - return delegate.createPredicate(position, sort, new JpqlStrategy(from, entity, factory)); + return delegate.createPredicate(position, sort, new JpqlStrategy(null, from, entity, factory)); } @SuppressWarnings("rawtypes") @@ -128,22 +130,24 @@ public Predicate or(List intermediate) { private static class JpqlStrategy implements QueryStrategy { - private final From from; + private final Bindable from; private final JpqlQueryBuilder.Entity entity; private final ParameterFactory factory; + private final Metamodel metamodel; - public JpqlStrategy(From from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { + public JpqlStrategy(Metamodel metamodel, Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { this.from = from; this.entity = entity; this.factory = factory; + this.metamodel = metamodel; } @Override public JpqlQueryBuilder.Expression createExpression(String property) { - PropertyPath path = PropertyPath.from(property, from.getJavaType()); - return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(entity, from, path)); + PropertyPath path = PropertyPath.from(property, from.getBindableJavaType()); + return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, from, path)); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index d8b8e52fa2..f8c567f352 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -42,6 +42,7 @@ * * @author Thomas Darimont * @author Mark Paluch + * @author Christoph Strobl */ class ParameterBinding { @@ -217,7 +218,10 @@ public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin or this.templates = templates; this.escape = escape; - this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType(); + this.type = value == null + && (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType())) + ? Type.IS_NULL + : part.getType(); this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); this.noWildcards = part.getProperty().getLeafProperty().isCollection(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index 8848303b8d..806d379539 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -62,7 +62,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { private final PartTree tree; private final JpaParameters parameters; - private final QueryPreparer query; + private final QueryPreparer queryPreparer; private final QueryPreparer countQuery; private final EntityManager em; private final EscapeCharacter escape; @@ -102,7 +102,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { this.tree = new PartTree(method.getName(), domainClass); validate(tree, parameters, method.toString()); this.countQuery = new CountQueryPreparer(); - this.query = tree.isCountProjection() ? countQuery : new QueryPreparer(); + this.queryPreparer = tree.isCountProjection() ? countQuery : new QueryPreparer(); } catch (Exception o_O) { throw new IllegalArgumentException( @@ -112,7 +112,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { @Override public Query doCreateQuery(JpaParametersParameterAccessor accessor) { - return query.createQuery(accessor); + return queryPreparer.createQuery(accessor); } @Override @@ -210,12 +210,7 @@ private static boolean expectsCollection(Type type) { */ private class QueryPreparer { - private final Map cache = new LinkedHashMap() { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > 256; - } - }; + private final PartTreeQueryCache cache = new PartTreeQueryCache(); /** * Creates a new {@link Query} for the given parameter values. @@ -279,7 +274,7 @@ private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPositio protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) { synchronized (cache) { - JpqlQueryCreator jpqlQueryCreator = cache.get(sort); + JpqlQueryCreator jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for simple properties if (jpqlQueryCreator != null) { return jpqlQueryCreator; } @@ -304,7 +299,7 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess } synchronized (cache) { - cache.put(sort, creator); + cache.put(sort, accessor, creator); } return creator; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java new file mode 100644 index 0000000000..71f952c2c8 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 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.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +class PartTreeQueryCache { + + private final Map cache = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 256; + } + }; + + @Nullable + JpqlQueryCreator get(Sort sort, JpaParametersParameterAccessor accessor) { + return cache.get(CacheKey.of(sort, accessor)); + } + + @Nullable + JpqlQueryCreator put(Sort sort, JpaParametersParameterAccessor accessor, JpqlQueryCreator creator) { + return cache.put(CacheKey.of(sort, accessor), creator); + } + + static class CacheKey { + + private final Sort sort; + private final Map params; + + public CacheKey(Sort sort, Map params) { + this.sort = sort; + this.params = params; + } + + static CacheKey of(Sort sort, JpaParametersParameterAccessor accessor) { + + Object[] values = accessor.getValues(); + if (ObjectUtils.isEmpty(values)) { + return new CacheKey(sort, Map.of()); + } + + return new CacheKey(sort, toNullableMap(values)); + } + + static Map toNullableMap(Object[] args) { + + Map paramMap = new HashMap<>(args.length); + for (int i = 0; i < args.length; i++) { + paramMap.put(i, args[i] != null ? Nulled.NO : Nulled.YES); + } + return paramMap; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CacheKey cacheKey = (CacheKey) o; + return sort.equals(cacheKey.sort) && params.equals(cacheKey.params); + } + + @Override + public int hashCode() { + return Objects.hash(sort, params); + } + } + + enum Nulled { + YES, NO + } + +} 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 9922c47150..c75137267b 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 @@ -896,7 +896,7 @@ private static T getAnnotationProperty(Attribute attribute, String pro * @param attribute the attribute name to check. * @return true if the attribute has already been inner joined */ - private static boolean isAlreadyInnerJoined(From from, String attribute) { + static boolean isAlreadyInnerJoined(From from, String attribute) { for (Fetch fetch : from.getFetches()) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java new file mode 100644 index 0000000000..dc2866fa8b --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java @@ -0,0 +1,1039 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Tuple; +import jakarta.persistence.metamodel.Metamodel; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.jpa.util.TestMetaModel; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.Lazy; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class JpaQueryCreatorTests { + + private static final TestMetaModel ORDER = TestMetaModel.hibernateModel(Order.class, LineItem.class, Product.class); + private static final TestMetaModel PERSON = TestMetaModel.hibernateModel(Person.class); + + static List ignoreCaseTemplates = List.of(JpqlQueryTemplates.LOWER, JpqlQueryTemplates.UPPER); + + @Test + void simpleProperty() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountry") // + .withParameters("AT") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void simpleNullProperty() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountry") // + .withParameterTypes(String.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country IS NULL", Order.class.getName()) // + .validateQuery(); + } + + @Test + void negatingSimpleProperty() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryNot") // + .withParameters("US") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country != ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void negatingSimpleNullProperty() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryIsNot") // + .withParameterTypes(String.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country IS NOT NULL", Order.class.getName()) // + .validateQuery(); + } + + @Test + void simpleAnd() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryAndDate") // + .withParameters("GB", new Date()) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 AND o.date = ?2", Order.class.getName()) // + .validateQuery(); + } + + @Test + void simpleOr() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryOrDate") // + .withParameters("BE", new Date()) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 OR o.date = ?2", Order.class.getName()) // + .validateQuery(); + } + + @Test + void simpleAndOr() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryAndDateOrCompleted") // + .withParameters("IT", new Date(), Boolean.FALSE) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 AND o.date = ?2 OR o.completed = ?3", + Order.class.getName()) // + .validateQuery(); + } + + @Test + void distinct() { + + queryCreator(ORDER) // + .forTree(Order.class, "findDistinctOrderByCountry") // + .withParameters("AU") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT DISTINCT o FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void count() { + + queryCreator(ORDER) // + .forTree(Order.class, "countOrderByCountry") // + .returing(Long.class) // + .withParameters("AU") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT COUNT(o) FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void countWithJoins() { + + queryCreator(ORDER) // + .forTree(Order.class, "countOrderByLineItemsQuantityGreaterThan") // + .returing(Long.class) // + .withParameterTypes(Integer.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT COUNT(o) FROM %s o LEFT JOIN o.lineItems l WHERE l.quantity > ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void countDistinct() { + + queryCreator(ORDER) // + .forTree(Order.class, "countDistinctOrderByCountry") // + .returing(Long.class) // + .withParameters("AU") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT COUNT(DISTINCT o) FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void simplePropertyIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("BB") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE %s(o.country) = %s(?1)", Order.class.getName(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void simplePropertyAllIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameAndProductTypeAllIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("spring", "data") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) = %s(?1) AND %s(p.productType) = %s(?2)", + Product.class.getName(), ingnoreCaseTemplate.getIgnoreCaseOperator(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator(), + ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void simplePropertyMixedCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameAndProductTypeIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("spring", "data") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name = ?1 AND %s(p.productType) = %s(?2)", Product.class.getName(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator(), + ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .validateQuery(); + } + + @Test + void lessThan() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateLessThan") // + .withParameterTypes(Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date < ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void lessThanEqual() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateLessThanEqual") // + .withParameterTypes(Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date <= ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void greaterThan() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateGreaterThan") // + .withParameterTypes(Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date > ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void before() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateBefore") // + .withParameterTypes(Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date < ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void after() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateAfter") // + .withParameterTypes(Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date > ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void between() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateBetween") // + .withParameterTypes(Date.class, Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date BETWEEN ?1 AND ?2", Order.class.getName()) // + .validateQuery(); + } + + @Test + void isNull() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateIsNull") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date IS NULL", Order.class.getName()) // + .validateQuery(); + } + + @Test + void isNotNull() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateIsNotNull") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date IS NOT NULL", Order.class.getName()) // + .validateQuery(); + } + + @ParameterizedTest + @ValueSource(strings = { "", "spring", "%spring", "spring%", "%spring%" }) + void like(String parameterValue) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameLike") // + .withParameters(parameterValue) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectPlaceholderValue("?1", parameterValue) // + .validateQuery(); + } + + @Test + void containingString() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameContaining") // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectPlaceholderValue("?1", "%spring%") // + .validateQuery(); + } + + @Test + void notContainingString() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameNotContaining") // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name NOT LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectPlaceholderValue("?1", "%spring%") // + .validateQuery(); + } + + @Test + void in() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameIn") // + .withParameters(List.of("spring", "data")) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name IN (?1)", Product.class.getName()) // + .expectPlaceholderValue("?1", List.of("spring", "data")) // + .validateQuery(); + } + + @Test + void notIn() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameNotIn") // + .withParameters(List.of("spring", "data")) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name NOT IN (?1)", Product.class.getName()) // + .expectPlaceholderValue("?1", List.of("spring", "data")) // + .validateQuery(); + } + + @Test + void containingSingleEntryElementCollection() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByCategoriesContaining") // + .withParameterTypes(String.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE ?1 MEMBER OF p.categories", Product.class.getName()) // + .validateQuery(); + } + + @Test + void notContainingSingleEntryElementCollection() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByCategoriesNotContaining") // + .withParameterTypes(String.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE ?1 NOT MEMBER OF p.categories", Product.class.getName()) // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void likeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameLikeIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("%spring%") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .expectPlaceholderValue("?1", "%spring%") // + .validateQuery(); + } + + @ParameterizedTest + @ValueSource(strings = { "", "spring", "%spring", "spring%", "%spring%" }) + void notLike(String parameterValue) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameNotLike") // + .withParameters(parameterValue) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name NOT LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectPlaceholderValue("?1", parameterValue) // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void notLikeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameNotLikeIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("%spring%") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) NOT LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .expectPlaceholderValue("?1", "%spring%") // + .validateQuery(); + } + + @Test + void startingWith() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameStartingWith") // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectPlaceholderValue("?1", "spring%") // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void startingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameStartingWithIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .expectPlaceholderValue("?1", "spring%") // + .validateQuery(); + } + + @Test + void endingWith() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameEndingWith") // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectPlaceholderValue("?1", "%spring") // + .validateQuery(); + } + + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void endingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { + + queryCreator(ORDER) // + .forTree(Product.class, "findProductByNameEndingWithIgnoreCase") // + .ingnoreCaseAs(ingnoreCaseTemplate) // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // + .expectPlaceholderValue("?1", "%spring") // + .validateQuery(); + } + + @Test + void greaterThanEqual() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByDateGreaterThanEqual") // + .withParameterTypes(Date.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.date >= ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void isTrue() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCompletedIsTrue") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.completed = TRUE", Order.class.getName()) // + .validateQuery(); + } + + @Test + void isFalse() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCompletedIsFalse") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.completed = FALSE", Order.class.getName()) // + .validateQuery(); + } + + @Test + void empty() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsEmpty") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.lineItems IS EMPTY", Order.class.getName()) // + .validateQuery(); + } + + @Test + void notEmpty() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsNotEmpty") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.lineItems IS NOT EMPTY", Order.class.getName()) // + .validateQuery(); + } + + @Test + void sortBySingle() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByCountryOrderByDate") // + .withParameters("CA") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 ORDER BY o.date asc", Order.class.getName()) // + .validateQuery(); + } + + @Test + void sortByMulti() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByOrderByCountryAscDateDesc") // + .withParameters() // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o ORDER BY o.country asc, o.date desc", Order.class.getName()) // + .validateQuery(); + } + + @Disabled("should we support this?") + @ParameterizedTest + @FieldSource("ignoreCaseTemplates") + void sortBySingleIngoreCase(JpqlQueryTemplates ingoreCase) { + + String jpql = queryCreator(ORDER) // + .forTree(Order.class, "findOrderByOrderByCountryAscAllIgnoreCase") // + .render(); + + assertThat(jpql).isEqualTo("SELECT o FROM %s o ORDER BY %s(o.date) asc", Order.class.getName(), + ingoreCase.getIgnoreCaseOperator()); + } + + @Test + void matchSimpleJoin() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsQuantityGreaterThan") // + .withParameterTypes(Integer.class) // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l WHERE l.quantity > ?1", Order.class.getName()) // + .validateQuery(); + } + + @Test + void matchSimpleNestedJoin() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsProductNameIs") // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE p.name = ?1", + Order.class.getName()) // + .validateQuery(); + } + + @Test + void matchMultiOnNestedJoin() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsQuantityGreaterThanAndLineItemsProductNameIs") // + .withParameters(10, "spring") // + .as(QueryCreatorTester::create) // + .expectJpql( + "SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE l.quantity > ?1 AND p.name = ?2", + Order.class.getName()) // + .validateQuery(); + } + + @Test + void matchSameEntityMultipleTimes() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsProductNameIsAndLineItemsProductNameIsNot") // + .withParameters("spring", "sukrauq") // + .as(QueryCreatorTester::create) // + .expectJpql( + "SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE p.name = ?1 AND p.name != ?2", + Order.class.getName()) // + .validateQuery(); + } + + @Test + void matchSameEntityMultipleTimesViaDifferentProperties() { + + queryCreator(ORDER) // + .forTree(Order.class, "findOrderByLineItemsProductNameIsAndLineItemsProduct2NameIs") // + .withParameters(10, "spring") // + .as(QueryCreatorTester::create) // + .expectJpql( + "SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p INNER JOIN l.product2 join_0 WHERE p.name = ?1 AND join_0.name = ?2", + Order.class.getName()) // + .validateQuery(); + } + + @Test + void dtoProjection() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProjectionByNameIs") // + .returing(DtoProductProjection.class) // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT new %s(p.name, p.productType) FROM %s p WHERE p.name = ?1", + DtoProductProjection.class.getName(), Product.class.getName()) // + .validateQuery(); + } + + @Test + void interfaceProjection() { + + queryCreator(ORDER) // + .forTree(Product.class, "findProjectionByNameIs") // + .returing(InterfaceProductProjection.class) // + .withParameters("spring") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p.name name, p.productType productType FROM %s p WHERE p.name = ?1", + Product.class.getName()) // + .validateQuery(); + } + + @ParameterizedTest + @ValueSource(classes = { Tuple.class, Map.class }) + void tupleProjection(Class resultType) { + + queryCreator(PERSON) // + .forTree(Person.class, "findProjectionByFirstnameIs") // + .returing(resultType) // + .withParameters("chris") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p.id id, p.firstname firstname, p.lastname lastname FROM %s p WHERE p.firstname = ?1", + Person.class.getName()) // + .validateQuery(); + } + + @ParameterizedTest + @ValueSource(classes = { Long.class, List.class, Person.class }) + void delete(Class resultType) { + + queryCreator(PERSON) // + .forTree(Person.class, "deletePersonByFirstname") // + .returing(resultType) // + .withParameters("chris") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p FROM %s p WHERE p.firstname = ?1", Person.class.getName()) // + .validateQuery(); + } + + @Test + void exists() { + + queryCreator(PERSON) // + .forTree(Person.class, "existsPersonByFirstname") // + .returing(Long.class).withParameters("chris") // + .as(QueryCreatorTester::create) // + .expectJpql("SELECT p.id id FROM %s p WHERE p.firstname = ?1", Person.class.getName()) // + .validateQuery(); + } + + QueryCreatorBuilder queryCreator(Metamodel metamodel) { + return new DefaultCreatorBuilder(metamodel); + } + + JpaQueryCreator queryCreator(PartTree tree, ReturnedType returnedType, Metamodel metamodel, Object... arguments) { + return queryCreator(tree, returnedType, metamodel, JpqlQueryTemplates.UPPER, arguments); + } + + JpaQueryCreator queryCreator(PartTree tree, ReturnedType returnedType, Metamodel metamodel, + JpqlQueryTemplates templates, Object... arguments) { + + ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider( + StubJpaParameterParameterAccessor.accessor(arguments), EscapeCharacter.DEFAULT, templates); + return queryCreator(tree, returnedType, metamodel, templates, parameterMetadataProvider); + } + + JpaQueryCreator queryCreator(PartTree tree, ReturnedType returnedType, Metamodel metamodel, + JpqlQueryTemplates templates, Class... argumentTypes) { + + ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider( + StubJpaParameterParameterAccessor.accessor(argumentTypes), EscapeCharacter.DEFAULT, templates); + return queryCreator(tree, returnedType, metamodel, templates, parameterMetadataProvider); + } + + JpaQueryCreator queryCreator(PartTree tree, ReturnedType returnedType, Metamodel metamodel, + JpqlQueryTemplates templates, JpaParametersParameterAccessor parameterAccessor) { + + EntityManager entityManager = mock(EntityManager.class); + when(entityManager.getMetamodel()).thenReturn(metamodel); + + ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(parameterAccessor, + EscapeCharacter.DEFAULT, templates); + return new JpaQueryCreator(tree, returnedType, parameterMetadataProvider, templates, entityManager); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private JpaParametersParameterAccessor accessor(Class... argumentTypes) { + + return StubJpaParameterParameterAccessor.accessor(argumentTypes); + } + + @jakarta.persistence.Entity + static class Order { + + @Id Long id; + Date date; + String country; + Boolean completed; + + @OneToMany List lineItems; + } + + @jakarta.persistence.Entity + static class LineItem { + + @Id Long id; + + @ManyToOne Product product; + @ManyToOne Product product2; + int quantity; + + } + + @jakarta.persistence.Entity + static class Person { + @Id Long id; + String firstname; + String lastname; + } + + @jakarta.persistence.Entity + static class Product { + + @Id Long id; + + String name; + String productType; + + @ElementCollection List categories; + } + + static class DtoProductProjection { + + String name; + String productType; + + DtoProductProjection(String name, String productType) { + this.name = name; + this.productType = productType; + } + } + + interface InterfaceProductProjection { + String getName(); + + String getProductType(); + } + + static class QueryCreatorTester { + + QueryCreatorBuilder builder; + Lazy jpql; + + private QueryCreatorTester(QueryCreatorBuilder builder) { + this.builder = builder; + this.jpql = Lazy.of(builder::render); + } + + static QueryCreatorTester create(QueryCreatorBuilder builder) { + return new QueryCreatorTester(builder); + } + + QueryCreatorTester expectJpql(String jpql, Object... args) { + + assertThat(this.jpql.get()).isEqualTo(jpql, args); + return this; + } + + QueryCreatorTester expectPlaceholderValue(String placeholder, Object value) { + return expectBindingAt(builder.bindingIndexFor(placeholder), value); + } + + QueryCreatorTester expectBindingAt(int position, Object value) { + + Object current = builder.bindableParameters().getBindableValue(position - 1); + assertThat(current).isEqualTo(value); + return this; + } + + QueryCreatorTester validateQuery() { + + if (builder instanceof DefaultCreatorBuilder dcb && dcb.metamodel instanceof TestMetaModel tmm) { + return validateQuery(tmm.entityManager()); + } + + throw new IllegalStateException("No EntityManager found, plase provide one via [verify(EntityManager)]"); + } + + QueryCreatorTester validateQuery(EntityManager entityManager) { + + if (builder instanceof DefaultCreatorBuilder dcb) { + entityManager.createQuery(this.jpql.get(), dcb.returnedType.getReturnedType()); + } else { + entityManager.createQuery(this.jpql.get()); + } + return this; + } + + } + + interface QueryCreatorBuilder { + + QueryCreatorBuilder returing(ReturnedType returnedType); + + QueryCreatorBuilder forTree(Class root, String querySource); + + QueryCreatorBuilder withParameters(Object... arguments); + + QueryCreatorBuilder withParameterTypes(Class... argumentTypes); + + QueryCreatorBuilder ingnoreCaseAs(JpqlQueryTemplates queryTemplate); + + default T as(Function transformer) { + return transformer.apply(this); + } + + default String render() { + return render(null); + } + + ParameterAccessor bindableParameters(); + + int bindingIndexFor(String placeholder); + + String render(@Nullable Sort sort); + + QueryCreatorBuilder returing(Class type); + } + + private class DefaultCreatorBuilder implements QueryCreatorBuilder { + + private static final ProjectionFactory PROJECTION_FACTORY = new SpelAwareProxyProjectionFactory(); + + private final Metamodel metamodel; + private ReturnedType returnedType; + private PartTree partTree; + private Object[] arguments; + private Class[] argumentTypes; + private JpqlQueryTemplates queryTemplates; + private Lazy queryCreator = Lazy.of(this::initJpaQueryCreator); + private Lazy parameterAccessor = Lazy.of(this::initParameterAccessor); + + public DefaultCreatorBuilder(Metamodel metamodel) { + this.metamodel = metamodel; + arguments = new Object[0]; + queryTemplates = JpqlQueryTemplates.UPPER; + } + + @Override + public QueryCreatorBuilder returing(ReturnedType returnedType) { + this.returnedType = returnedType; + return this; + } + + @Override + public QueryCreatorBuilder returing(Class type) { + + if (this.returnedType != null) { + return returing(ReturnedType.of(type, returnedType.getDomainType(), PROJECTION_FACTORY)); + } + + return returing(ReturnedType.of(type, type, PROJECTION_FACTORY)); + } + + @Override + public QueryCreatorBuilder forTree(Class root, String querySource) { + + this.partTree = new PartTree(querySource, root); + if (returnedType == null) { + returnedType = ReturnedType.of(root, root, PROJECTION_FACTORY); + } + return this; + } + + @Override + public QueryCreatorBuilder withParameters(Object... arguments) { + this.arguments = arguments; + return this; + } + + @Override + public QueryCreatorBuilder withParameterTypes(Class... argumentTypes) { + this.argumentTypes = argumentTypes; + return this; + } + + @Override + public QueryCreatorBuilder ingnoreCaseAs(JpqlQueryTemplates queryTemplate) { + this.queryTemplates = queryTemplate; + return this; + } + + @Override + public String render(@Nullable Sort sort) { + return queryCreator.get().createQuery(sort != null ? sort : Sort.unsorted()); + } + + @Override + public int bindingIndexFor(String placeholder) { + + return queryCreator.get().getBindings().stream().filter(binding -> { + + if (binding.getIdentifier().hasPosition() && placeholder.startsWith("?")) { + return binding.getPosition() == Integer.parseInt(placeholder.substring(1)); + } + + if (!binding.getIdentifier().hasName()) { + return false; + } + + return binding.getIdentifier().getName().equals(placeholder); + }).findFirst().map(ParameterBinding::getPosition).orElse(-1); + } + + @Override + public ParameterAccessor bindableParameters() { + + return new ParameterAccessor() { + @Nullable + @Override + public ScrollPosition getScrollPosition() { + return null; + } + + @Override + public Pageable getPageable() { + return null; + } + + @Override + public Sort getSort() { + return null; + } + + @Nullable + @Override + public Class findDynamicProjection() { + return null; + } + + @Nullable + @Override + public Object getBindableValue(int index) { + + ParameterBinding parameterBinding = queryCreator.get().getBindings().get(index); + return parameterBinding.prepare(parameterAccessor.get().getBindableValue(index)); + } + + @Override + public boolean hasBindableNullValue() { + return false; + } + + @Override + public Iterator iterator() { + return null; + } + }; + + } + + JpaParametersParameterAccessor initParameterAccessor() { + + if (arguments.length > 0 || argumentTypes == null) { + return StubJpaParameterParameterAccessor.accessor(arguments); + } + return StubJpaParameterParameterAccessor.accessor(argumentTypes); + } + + JpaQueryCreator initJpaQueryCreator() { + + if (arguments.length > 0 || argumentTypes == null) { + return queryCreator(partTree, returnedType, metamodel, queryTemplates, parameterAccessor.get()); + } + return queryCreator(partTree, returnedType, metamodel, queryTemplates, parameterAccessor.get()); + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java new file mode 100644 index 0000000000..04fb7079de --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java @@ -0,0 +1,265 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.AbstractJpqlQuery; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Entity; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Expression; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Join; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.OrderExpression; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Predicate; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.RenderContext; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.SelectStep; +import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.WhereStep; + +/** + * @author Christoph Strobl + */ +class JpqlQueryBuilderUnitTests { + + @Test + void placeholdersRenderCorrectly() { + + assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1"); + assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.named("arg1")).render(RenderContext.EMPTY)) + .isEqualTo(":arg1"); + assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1"); + } + + @Test + void placeholdersErrorOnInvaludInput() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> JpqlQueryBuilder.parameter((String) null)); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JpqlQueryBuilder.parameter("")); + } + + @Test + void stringLiteralRendersAsQuotedString() { + + assertThat(JpqlQueryBuilder.stringLiteral("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'"); + + /* JPA Spec - 4.6.1 Literals: + > A string literal that includes a single quote is represented by two single quotes--for example: 'literal''s'. */ + assertThat(JpqlQueryBuilder.stringLiteral("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'"); + } + + @Test + void entity() { + + Entity entity = JpqlQueryBuilder.entity(Order.class); + assertThat(entity.alias()).isEqualTo("o"); + assertThat(entity.entity()).isEqualTo(Order.class.getName()); + assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); // TODO: this really confusing + assertThat(entity.simpleName()).isEqualTo(Order.class.getSimpleName()); + } + + @Test + void literalExpressionRendersAsIs() { + Expression expression = JpqlQueryBuilder.expression("CONCAT(person.lastName, ‘, ’, person.firstName))"); + assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))"); + } + + @Test + void xxx() { + + Entity entity = JpqlQueryBuilder.entity(Order.class); + PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date"); + + String fragment = JpqlQueryBuilder.where(orderDate).eq("{d '2024-11-05'}").render(ctx(entity)); + + assertThat(fragment).isEqualTo("o.date = {d '2024-11-05'}"); + + // JpqlQueryBuilder.where(PathAndOrigin) + } + + @Test + void predicateRendering() { + + + Entity entity = JpqlQueryBuilder.entity(Order.class); + WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); + + assertThat(where.between("'AT'", "'DE'").render(ctx(entity))).isEqualTo("o.country BETWEEN 'AT' AND 'DE'"); + assertThat(where.eq("'AT'").render(ctx(entity))).isEqualTo("o.country = 'AT'"); + assertThat(where.eq(JpqlQueryBuilder.stringLiteral("AT")).render(ctx(entity))).isEqualTo("o.country = 'AT'"); + assertThat(where.gt("'AT'").render(ctx(entity))).isEqualTo("o.country > 'AT'"); + assertThat(where.gte("'AT'").render(ctx(entity))).isEqualTo("o.country >= 'AT'"); + // TODO: that is really really bad + // lange namen + assertThat(where.in("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); + + // 1 in age - cleanup what is not used - remove everything eles + // assertThat(where.inMultivalued("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); // + assertThat(where.isEmpty().render(ctx(entity))).isEqualTo("o.country IS EMPTY"); + assertThat(where.isNotEmpty().render(ctx(entity))).isEqualTo("o.country IS NOT EMPTY"); + assertThat(where.isTrue().render(ctx(entity))).isEqualTo("o.country = TRUE"); + assertThat(where.isFalse().render(ctx(entity))).isEqualTo("o.country = FALSE"); + assertThat(where.isNull().render(ctx(entity))).isEqualTo("o.country IS NULL"); + assertThat(where.isNotNull().render(ctx(entity))).isEqualTo("o.country IS NOT NULL"); + assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity))) + .isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'"); + assertThat(where.notLike("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity))) + .isEqualTo("o.country NOT LIKE '\\_%' ESCAPE '\\'"); + assertThat(where.lt("'AT'").render(ctx(entity))).isEqualTo("o.country < 'AT'"); + assertThat(where.lte("'AT'").render(ctx(entity))).isEqualTo("o.country <= 'AT'"); + assertThat(where.memberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' MEMBER OF o.country"); + // TODO: can we have this where.value(foo).memberOf(pathAndOrigin); + assertThat(where.notMemberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' NOT MEMBER OF o.country"); + assertThat(where.neq("'AT'").render(ctx(entity))).isEqualTo("o.country != 'AT'"); + } + + @Test + void selectRendering() { + + // make sure things are immutable + SelectStep select = JpqlQueryBuilder.selectFrom(Order.class); // the select step is mutable - not sure i like it + // hm, I somehow exepect this to render only the selection part + assertThat(select.count().render()).startsWith("SELECT COUNT(o)"); + assertThat(select.distinct().entity().render()).startsWith("SELECT DISTINCT o "); + assertThat(select.distinct().count().render()).startsWith("SELECT COUNT(DISTINCT o) "); + assertThat(JpqlQueryBuilder.selectFrom(Order.class).select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render()) + .startsWith("SELECT o.country "); + } + +// @Test +// void sorting() { +// +// JpqlQueryBuilder.orderBy(new OrderExpression() , Sort.Order.asc("country")); +// +// Entity entity = JpqlQueryBuilder.entity(Order.class); +// +// AbstractJpqlQuery query = JpqlQueryBuilder.selectFrom(Order.class) +// .entity() +// .orderBy() +// .where(context -> "1 = 1"); +// +// } + + @Test + void joins() { + + Entity entity = JpqlQueryBuilder.entity(LineItem.class); + Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); + Join li_pr2 = JpqlQueryBuilder.innerJoin(entity, "product2"); + + PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name"); + PathAndOrigin personName = JpqlQueryBuilder.path(li_pr2, "name"); + + String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30")) + .and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("ex40"))).render(ctx(entity)); + + assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'"); + } + + @Test + void x2() { + + Entity entity = JpqlQueryBuilder.entity(LineItem.class); + Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); + Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person"); + + PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name"); + PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name"); + + String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30")) + .and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity)); + + assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'"); + } + + @Test + void x3() { + + Entity entity = JpqlQueryBuilder.entity(LineItem.class); + Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); + Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person"); + + PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name"); + PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name"); + + // JpqlQueryBuilder.and("x = y", "a = b"); -> x = y AND a = b + + // JpqlQueryBuilder.nested(JpqlQueryBuilder.and("x = y", "a = b")) (x = y AND a = b) + + String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30")) + .and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity)); + + assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'"); + } + + static RenderContext ctx(Entity... entities) { + Map aliases = new LinkedHashMap<>(entities.length); + for (Entity entity : entities) { + aliases.put(entity, entity.alias()); + } + + return new RenderContext(aliases); + } + + @jakarta.persistence.Entity + static class Order { + + @Id Long id; + Date date; + String country; + + @OneToMany List lineItems; + } + + @jakarta.persistence.Entity + static class LineItem { + + @Id Long id; + + @ManyToOne Product product; + @ManyToOne Product product2; + @ManyToOne Product person; + + } + + @jakarta.persistence.Entity + static class Person { + @Id Long id; + String name; + } + + @jakarta.persistence.Entity + static class Product { + + @Id Long id; + + String name; + String productType; + + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index 69f73f5bc1..b99e50071d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -112,7 +112,7 @@ void recreatesQueryIfNullValueIsGiven(String criteria) throws Exception { Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) })); assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))) - .contains("firstname %s :".formatted(criteria.endsWith("Not") ? "<>" : "=")); + .contains("firstname %s ?".formatted(criteria.endsWith("Not") ? "!=" : "=")); query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) })); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java new file mode 100644 index 0000000000..aa3911473f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024 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 static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +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.FieldSource; +import org.mockito.Mockito; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +/** + * @author Christoph Strobl + */ +public class PartTreeQueryCacheUnitTests { + + PartTreeQueryCache cache; + + static Supplier> cacheInput = () -> Stream.of( + Arguments.arguments(Sort.unsorted(), StubJpaParameterParameterAccessor.accessor()), // + Arguments.arguments(Sort.by(Direction.ASC, "one"), StubJpaParameterParameterAccessor.accessor()), // + Arguments.arguments(Sort.by(Direction.DESC, "one"), StubJpaParameterParameterAccessor.accessor()), // + Arguments.arguments(Sort.unsorted(), + StubJpaParameterParameterAccessor.accessorFor(String.class).withValues("value")), // + Arguments.arguments(Sort.unsorted(), + StubJpaParameterParameterAccessor.accessorFor(String.class).withValues(new Object[] { null })), // + Arguments.arguments(Sort.by(Direction.ASC, "one"), + StubJpaParameterParameterAccessor.accessorFor(String.class).withValues("value")), // + Arguments.arguments(Sort.by(Direction.ASC, "one"), + StubJpaParameterParameterAccessor.accessorFor(String.class).withValues(new Object[] { null }))); + + @BeforeEach + void beforeEach() { + cache = new PartTreeQueryCache(); + } + + @ParameterizedTest + @FieldSource("cacheInput") + void getReturnsNullForEmptyCache(Sort sort, JpaParametersParameterAccessor accessor) { + assertThat(cache.get(sort, accessor)).isNull(); + } + + @ParameterizedTest + @FieldSource("cacheInput") + void getReturnsCachedInstance(Sort sort, JpaParametersParameterAccessor accessor) { + + JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class); + + assertThat(cache.put(sort, accessor, queryCreator)).isNull(); + assertThat(cache.get(sort, accessor)).isSameAs(queryCreator); + } + + @ParameterizedTest + @FieldSource("cacheInput") + void cacheGetWithSort(Sort sort, JpaParametersParameterAccessor accessor) { + + JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class); + assertThat(cache.put(Sort.by("not-in-cache"), accessor, queryCreator)).isNull(); + + assertThat(cache.get(sort, accessor)).isNull(); + } + + @ParameterizedTest + @FieldSource("cacheInput") + void cacheGetWithccessor(Sort sort, JpaParametersParameterAccessor accessor) { + + JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class); + assertThat(cache.put(sort, StubJpaParameterParameterAccessor.accessor("spring", "data"), queryCreator)).isNull(); + + assertThat(cache.get(sort, accessor)).isNull(); + } + + @Test + void cachesOnNullableNotArgumentType() { + + JpaQueryCreator queryCreator = Mockito.mock(JpaQueryCreator.class); + Sort sort = Sort.unsorted(); + assertThat(cache.put(sort, StubJpaParameterParameterAccessor.accessor("spring", "data"), queryCreator)).isNull(); + + assertThat(cache.get(sort, + StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "spring", null))) + .isNull(); + + assertThat(cache.get(sort, + StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, null, "data"))).isNull(); + + assertThat(cache.get(sort, + StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "data", "spring"))) + .isSameAs(queryCreator); + + assertThat(cache.get(Sort.by("not-in-cache"), + StubJpaParameterParameterAccessor.accessor(new Class[] { String.class, String.class }, "data", "spring"))) + .isNull(); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java new file mode 100644 index 0000000000..c5794c9644 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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 static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.mockito.Mockito; +import org.springframework.core.MethodParameter; +import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + */ +public class StubJpaParameterParameterAccessor extends JpaParametersParameterAccessor { + + private StubJpaParameterParameterAccessor(JpaParameters parameters, Object[] values) { + super(parameters, values); + } + + static JpaParametersParameterAccessor accessor(Object... values) { + + Class[] parameterTypes = Arrays.stream(values).map(it -> it != null ? it.getClass() : Object.class) + .toArray(Class[]::new); + return accessor(parameterTypes, values); + } + + static JpaParametersParameterAccessor accessor(Class... parameterTypes) { + return accessor(parameterTypes, new Object[parameterTypes.length]); + } + + static AccessorBuilder accessorFor(Class... parameterTypes) { + return arguments -> accessor(parameterTypes, arguments); + + } + + interface AccessorBuilder { + JpaParametersParameterAccessor withValues(Object... arguments); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + static JpaParametersParameterAccessor accessor(Class[] parameterTypes, Object... parameters) { + + List parametersList = new ArrayList<>(parameterTypes.length); + List valueList = new ArrayList<>(parameterTypes.length); + + for (int i = 0; i < parameterTypes.length; i++) { + + if (i < parameters.length) { + valueList.add(parameters[i]); + } + + Class parameterType = parameterTypes[i]; + MethodParameter mock = Mockito.mock(MethodParameter.class); + when(mock.getParameterType()).thenReturn((Class) parameterType); + JpaParameter parameter = new JpaParameter(mock, TypeInformation.of(parameterType)); + parametersList.add(parameter); + } + + return new StubJpaParameterParameterAccessor(new JpaParameters(parametersList), valueList.toArray()); + } + + @Override + public String toString() { + List parameters = new ArrayList<>(getParameters().getNumberOfParameters()); + + for (int i = 0; i < getParameters().getNumberOfParameters(); i++) { + Object value = getValue(i); + if (value == null) { + value = "null"; + } + parameters.add("%s: %s (%s)".formatted(i, value, getParameters().getParameter(i).getType().getSimpleName())); + } + return "%s".formatted(parameters); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java new file mode 100644 index 0000000000..a755ba222b --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java @@ -0,0 +1,119 @@ +/* + * Copyright 2024 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.util; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.metamodel.EmbeddableType; +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.Metamodel; +import jakarta.persistence.spi.ClassTransformer; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; +import org.springframework.data.util.Lazy; +import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; +import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; + +/** + * @author Christoph Strobl + */ +public class TestMetaModel implements Metamodel { + + private final String persistenceUnit; + private final Set> managedTypes; + private final Lazy entityManagerFactory = Lazy.of(this::init); + private final Lazy metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel()); + private Lazy enityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); + + TestMetaModel(Set> managedTypes) { + this("dynamic-tests", managedTypes); + } + + TestMetaModel(String persistenceUnit, Set> managedTypes) { + this.persistenceUnit = persistenceUnit; + this.managedTypes = managedTypes; + } + + public static TestMetaModel hibernateModel(Class... types) { + return new TestMetaModel(Set.of(types)); + } + + public static TestMetaModel hibernateModel(String persistenceUnit, Class... types) { + return new TestMetaModel(persistenceUnit, Set.of(types)); + } + + public EntityType entity(Class cls) { + return metamodel.get().entity(cls); + } + + public ManagedType managedType(Class cls) { + return metamodel.get().managedType(cls); + } + + public EmbeddableType embeddable(Class cls) { + return metamodel.get().embeddable(cls); + } + + public Set> getManagedTypes() { + return metamodel.get().getManagedTypes(); + } + + public Set> getEntities() { + return metamodel.get().getEntities(); + } + + public Set> getEmbeddables() { + return metamodel.get().getEmbeddables(); + } + + public EntityManager entityManager() { + return enityManager.get(); + } + + EntityManagerFactory init() { + + MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { + @Override + public ClassLoader getNewTempClassLoader() { + return new SimpleThrowawayClassLoader(this.getClass().getClassLoader()); + } + + @Override + public void addTransformer(ClassTransformer classTransformer) { + // just ingnore it + } + }; + + persistenceUnitInfo.setPersistenceUnitName(persistenceUnit); + this.managedTypes.stream().map(Class::getName).forEach(persistenceUnitInfo::addManagedClassName); + + persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName()); + + return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(persistenceUnitInfo) { + @Override + public List getManagedClassNames() { + return persistenceUnitInfo.getManagedClassNames(); + } + }, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build(); + } +} diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence.xml b/spring-data-jpa/src/test/resources/META-INF/persistence.xml index 1c3be472e0..4f904373c3 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence.xml @@ -102,6 +102,14 @@ + + org.hibernate.jpa.HibernatePersistenceProvider + true + + + + + From 8e3eb4fe26ef477a18f76adaf12cc15313de46d6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Nov 2024 14:50:07 +0100 Subject: [PATCH 06/94] Polishing. Remove method overloads accepting pure strings. Use switch-expressions. Correctly navigate nested joins. Introduce PathExpression interface, refine naming. See #3588 Original pull request: #3653 --- .../jpa/repository/query/JpaQueryCreator.java | 22 +- .../repository/query/JpqlQueryBuilder.java | 474 +++++++++++------- .../data/jpa/repository/query/JpqlUtils.java | 103 +--- .../query/KeysetScrollSpecification.java | 6 +- .../repository/query/ParameterBinding.java | 19 +- .../repository/query/PartTreeJpaQuery.java | 23 +- .../repository/query/PartTreeQueryCache.java | 31 +- .../data/jpa/repository/query/QueryUtils.java | 7 +- .../query/JpaQueryCreatorTests.java | 124 ++--- .../query/JpqlQueryBuilderUnitTests.java | 152 ++---- ...meterMetadataProviderIntegrationTests.java | 18 +- 11 files changed, 493 insertions(+), 486 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index ec3739b3cc..12073a595d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -15,10 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.repository.query.parser.Part.Type.IS_NOT_EMPTY; -import static org.springframework.data.repository.query.parser.Part.Type.NOT_CONTAINING; -import static org.springframework.data.repository.query.parser.Part.Type.NOT_LIKE; -import static org.springframework.data.repository.query.parser.Part.Type.SIMPLE_PROPERTY; +import static org.springframework.data.repository.query.parser.Part.Type.*; import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaQuery; @@ -39,7 +36,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin; import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.mapping.PropertyPath; @@ -183,8 +179,8 @@ protected JpqlQueryBuilder.Select buildQuery(Sort sort) { QueryUtils.checkSortExpression(order); try { - expression = JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, - PropertyPath.from(order.getProperty(), entityType.getJavaType()))); + expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, + PropertyPath.from(order.getProperty(), entityType.getJavaType())); } catch (PropertyReferenceException e) { if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) { @@ -227,7 +223,7 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { requiredSelection = getRequiredSelection(sort, returnedType); } - List paths = new ArrayList<>(requiredSelection.size()); + List paths = new ArrayList<>(requiredSelection.size()); for (String selection : requiredSelection) { paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, PropertyPath.from(selection, returnedType.getDomainType()), true)); @@ -251,7 +247,7 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { } else { - List paths = entityType.getIdClassAttributes().stream()// + List paths = entityType.getIdClassAttributes().stream()// .map(it -> JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, PropertyPath.from(it.getName(), returnedType.getDomainType()), true)) .toList(); @@ -320,7 +316,7 @@ public JpqlQueryBuilder.Predicate build() { PropertyPath property = part.getProperty(); Type type = part.getType(); - PathAndOrigin pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property); + JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, property); JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(pas); JpqlQueryBuilder.WhereStep whereIgnoreCase = JpqlQueryBuilder.where(potentiallyIgnoreCase(pas)); @@ -385,7 +381,7 @@ public JpqlQueryBuilder.Predicate build() { return type.equals(SIMPLE_PROPERTY) ? where.isNull() : where.isNotNull(); } - JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(metadata)); + JpqlQueryBuilder.Expression expression = potentiallyIgnoreCase(property, placeholder(simple)); return type.equals(SIMPLE_PROPERTY) ? whereIgnoreCase.eq(expression) : whereIgnoreCase.neq(expression); case IS_EMPTY: case IS_NOT_EMPTY: @@ -420,8 +416,8 @@ private JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.O * @param path must not be {@literal null}. * @return */ - private JpqlQueryBuilder.Expression potentiallyIgnoreCase(PathAndOrigin path) { - return potentiallyIgnoreCase(path.path(), JpqlQueryBuilder.expression(path)); + private JpqlQueryBuilder.Expression potentiallyIgnoreCase(JpqlQueryBuilder.PathExpression path) { + return potentiallyIgnoreCase(path.getPropertyPath(), path); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index cb53998c3f..db6697a9d5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -15,8 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_ASC; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DESC; +import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.ArrayList; import java.util.Arrays; @@ -26,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; import org.springframework.data.domain.Sort; @@ -121,12 +121,12 @@ public Select count() { } @Override - public Select instantiate(String resultType, Collection paths) { + public Select instantiate(String resultType, Collection paths) { return new Select(postProcess(new ConstructorExpression(resultType, new Multiselect(from, paths))), from); } @Override - public Select select(Collection paths) { + public Select select(Collection paths) { return new Select(postProcess(new Multiselect(from, paths)), from); } @@ -177,22 +177,11 @@ public static Predicate nested(Predicate predicate) { * @return */ public static Expression expression(Origin source, PropertyPath path) { - return expression(new PathAndOrigin(path, source, false)); + return new PathAndOrigin(path, source, false); } /** - * Create a qualified expression for a {@link PropertyPath}. - * - * @param source - * @param path - * @return - */ - public static Expression expression(PathAndOrigin pas) { - return new PathExpression(pas); - } - - /** - * Create a simple expression from a string as is. + * Create a simple expression from a string as-is. * * @param expression * @return @@ -204,10 +193,32 @@ public static Expression expression(String expression) { return new LiteralExpression(expression); } - public static Expression stringLiteral(String literal) { + /** + * Create a simple numeric literal. + * + * @param literal + * @return + */ + public static Expression literal(Number literal) { + return new LiteralExpression(literal.toString()); + } + + /** + * Create a simple literal from a string by quoting it. + * + * @param literal + * @return + */ + public static Expression literal(String literal) { return new StringLiteralExpression(literal); } + /** + * A parameter placeholder. + * + * @param parameter + * @return + */ public static Expression parameter(String parameter) { Assert.hasText(parameter, "Parameter must not be empty or null"); @@ -215,10 +226,23 @@ public static Expression parameter(String parameter) { return new ParameterExpression(new ParameterPlaceholder(parameter)); } + /** + * A parameter placeholder. + * + * @param placeholder the placeholder to use. + * @return + */ public static Expression parameter(ParameterPlaceholder placeholder) { return new ParameterExpression(placeholder); } + /** + * Create a new ordering expression. + * + * @param sortExpression + * @param order + * @return + */ public static Expression orderBy(Expression sortExpression, Sort.Order order) { return new OrderExpression(sortExpression, order); } @@ -234,16 +258,6 @@ public static WhereStep where(Origin source, PropertyPath path) { return where(expression(source, path)); } - /** - * Start building a {@link Predicate WHERE predicate} by providing the right-hand side. - * - * @param rhs - * @return - */ - public static WhereStep where(PathAndOrigin rhs) { - return where(expression(rhs)); - } - /** * Start building a {@link Predicate WHERE predicate} by providing the right-hand side. * @@ -318,16 +332,6 @@ public Predicate notIn(Expression value) { return new InPredicate(rhs, "NOT IN", value); } - @Override - public Predicate inMultivalued(Expression value) { - return new MemberOfPredicate(rhs, "IN", value); // TODO: that does not line up in my head - ahahah - } - - @Override - public Predicate notInMultivalued(Expression value) { - return new MemberOfPredicate(rhs, "NOT IN", value); - } - @Override public Predicate memberOf(Expression value) { return new MemberOfPredicate(rhs, "MEMBER OF", value); @@ -422,7 +426,7 @@ public interface SelectStep { * @param paths * @return */ - default Select instantiate(Class resultType, Collection paths) { + default Select instantiate(Class resultType, Collection paths) { return instantiate(resultType.getName(), paths); } @@ -433,7 +437,7 @@ default Select instantiate(Class resultType, Collection paths) * @param paths * @return */ - Select instantiate(String resultType, Collection paths); + Select instantiate(String resultType, Collection paths); /** * Specify a multi-select. @@ -441,7 +445,7 @@ default Select instantiate(Class resultType, Collection paths) * @param paths * @return */ - Select select(Collection paths); + Select select(Collection paths); /** * Select a single attribute. @@ -449,7 +453,7 @@ default Select instantiate(Class resultType, Collection paths) * @param name * @return */ - default Select select(PathAndOrigin path) { + default Select select(JpqlQueryBuilder.PathExpression path) { return select(List.of(path)); } @@ -479,22 +483,22 @@ public String toString() { static PathAndOrigin path(Origin origin, String path) { - if(origin instanceof Entity entity) { + if (origin instanceof Entity entity) { - try { + try { PropertyPath from = PropertyPath.from(path, ClassUtils.forName(entity.entity, Entity.class.getClassLoader())); return new PathAndOrigin(from, entity, false); } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - if(origin instanceof Join join) { + throw new RuntimeException(e); + } + } + if (origin instanceof Join join) { Origin parent = join.source; List segments = new ArrayList<>(); segments.add(join.path); - while(!(parent instanceof Entity)) { - if(parent instanceof Join pj) { + while (!(parent instanceof Entity)) { + if (parent instanceof Join pj) { parent = pj.source; segments.add(pj.path); } else { @@ -502,7 +506,7 @@ static PathAndOrigin path(Origin origin, String path) { } } - if(parent instanceof Entity entity) { + if (parent instanceof Entity) { Collections.reverse(segments); segments.add(path); PathAndOrigin path1 = path(parent, StringUtils.collectionToDelimitedString(segments, ".")); @@ -561,7 +565,6 @@ record ConstructorExpression(String resultType, Multiselect multiselect) impleme @Override public String render(RenderContext context) { - return "new %s(%s)".formatted(resultType, multiselect.render(new ConstructorContext(context))); } @@ -577,22 +580,22 @@ public String toString() { * @param source * @param paths */ - record Multiselect(Origin source, Collection paths) implements Selection { + record Multiselect(Origin source, Collection paths) implements Selection { @Override public String render(RenderContext context) { StringBuilder builder = new StringBuilder(); - for (PathAndOrigin path : paths) { + for (PathExpression path : paths) { if (!builder.isEmpty()) { builder.append(", "); } - builder.append(PathExpression.render(path, context)); - if(!context.isConstructorContext()) { - builder.append(" ").append(path.path().getSegment()); + builder.append(path.render(context)); + if (!context.isConstructorContext()) { + builder.append(" ").append(path.getPropertyPath().getSegment()); } } @@ -662,6 +665,18 @@ public interface Expression { String render(RenderContext context); } + /** + * Extension to {@link Expression} that contains a {@link PropertyPath}. Typically used to represent a selection + * expression or an expression used within sorting or {@code WHERE} clauses. + */ + public interface PathExpression extends Expression { + + /** + * @return the associated {@link PropertyPath}. + */ + PropertyPath getPropertyPath(); + } + /** * {@code SELECT} statement. */ @@ -718,7 +733,7 @@ String render() { StringBuilder where = new StringBuilder(); StringBuilder orderby = new StringBuilder(); StringBuilder result = new StringBuilder( - "SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.entity(), entity.alias())); + "SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.getEntity(), entity.getAlias())); if (getWhere() != null) { where.append(" WHERE ").append(getWhere().render(renderContext)); @@ -874,32 +889,100 @@ public boolean isConstructorContext() { */ public interface Origin { - String getName(); // TODO: mainly used along records - shoule we call this just name()? + /** + * Returns the simple name of the origin (e.g. {@link Class#getSimpleName()} or JOIN path name). + * + * @return the simple name of the origin (e.g. {@link Class#getSimpleName()}) + */ + String getName(); + } + + /** + * An origin that is used to select data from. selection origins are used with paths to define where a path is + * anchored. + */ + public interface Bindable { + + boolean isRoot(); } /** * The root entity. - * - * @param entity - * @param simpleName - * @param alias */ - public record Entity(String entity, String simpleName, String alias) implements Origin { + public static final class Entity implements Origin { + + private final String entity; + private final String simpleName; + private final String alias; + + /** + * @param entity fully-qualified entity name. + * @param simpleName simple class name. + * @param alias alias to use. + */ + Entity(String entity, String simpleName, String alias) { + this.entity = entity; + this.simpleName = simpleName; + this.alias = alias; + } + + public String getEntity() { + return entity; + } @Override public String getName() { return simpleName; } + + public String getAlias() { + return alias; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + var that = (Entity) obj; + return Objects.equals(this.entity, that.entity) && Objects.equals(this.simpleName, that.simpleName) + && Objects.equals(this.alias, that.alias); + } + + @Override + public int hashCode() { + return Objects.hash(entity, simpleName, alias); + } + + @Override + public String toString() { + return "Entity[" + "entity=" + entity + ", " + "simpleName=" + simpleName + ", " + "alias=" + alias + ']'; + } + } /** * A joined entity or element collection. - * - * @param source - * @param joinType - * @param path */ - public record Join(Origin source, String joinType, String path) implements Origin, Expression { + public static final class Join implements Origin, Expression { + + private final Origin source; + private final String joinType; + private final String path; + + /** + * @param source + * @param joinType + * @param path + */ + Join(Origin source, String joinType, String path) { + this.source = source; + this.joinType = joinType; + this.path = path; + } @Override public String getName() { @@ -908,8 +991,44 @@ public String getName() { @Override public String render(RenderContext context) { - return ""; + return "%s %s %s".formatted(joinType, context.getAlias(source), path); } + + public Origin source() { + return source; + } + + public String joinType() { + return joinType; + } + + public String path() { + return path; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + var that = (Join) obj; + return Objects.equals(this.source, that.source) && Objects.equals(this.joinType, that.joinType) + && Objects.equals(this.path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(source, joinType, path); + } + + @Override + public String toString() { + return "Join[" + "source=" + source + ", " + "joinType=" + joinType + ", " + "path=" + path + ']'; + } + } /** @@ -917,17 +1036,6 @@ public String render(RenderContext context) { */ public interface WhereStep { - /** - * Create a {@code BETWEEN … AND …} predicate. - * - * @param lower lower boundary. - * @param upper upper boundary. - * @return - */ - default Predicate between(String lower, String upper) { - return between(expression(lower), expression(upper)); - } - /** * Create a {@code BETWEEN … AND …} predicate. * @@ -943,168 +1051,143 @@ default Predicate between(String lower, String upper) { * @param value the comparison value. * @return */ - default Predicate gt(String value) { - return gt(expression(value)); - } + Predicate gt(Expression value); /** - * Create a greater {@code > …} predicate. + * Create a greater-or-equals {@code >= …} predicate. * * @param value the comparison value. * @return */ - Predicate gt(Expression value); + Predicate gte(Expression value); /** - * Create a greater-or-equals {@code >= …} predicate. + * Create a less {@code < …} predicate. * * @param value the comparison value. * @return */ - default Predicate gte(String value) { - return gte(expression(value)); - } + Predicate lt(Expression value); /** - * Create a greater-or-equals {@code >= …} predicate. + * Create a less-or-equals {@code <= …} predicate. * * @param value the comparison value. * @return */ - Predicate gte(Expression value); + Predicate lte(Expression value); /** - * Create a less {@code < …} predicate. + * Create a {@code IS NULL} predicate. * - * @param value the comparison value. * @return */ - default Predicate lt(String value) { - return lt(expression(value)); - } + Predicate isNull(); /** - * Create a less {@code < …} predicate. + * Create a {@code IS NOT NULL} predicate. * - * @param value the comparison value. * @return */ - Predicate lt(Expression value); + Predicate isNotNull(); /** - * Create a less-or-equals {@code <= …} predicate. + * Create a {@code IS TRUE} predicate. * - * @param value the comparison value. * @return */ - default Predicate lte(String value) { - return lte(expression(value)); - } + Predicate isTrue(); /** - * Create a less-or-equals {@code <= …} predicate. + * Create a {@code IS FALSE} predicate. * - * @param value the comparison value. * @return */ - Predicate lte(Expression value); - - Predicate isNull(); - - Predicate isNotNull(); - - Predicate isTrue(); - Predicate isFalse(); + /** + * Create a {@code IS EMPTY} predicate. + * + * @return + */ Predicate isEmpty(); + /** + * Create a {@code IS NOT EMPTY} predicate. + * + * @return + */ Predicate isNotEmpty(); - default Predicate in(String value) { - return in(expression(value)); - } - + /** + * Create a {@code IN} predicate. + * + * @param value + * @return + */ Predicate in(Expression value); - default Predicate notIn(String value) { - return notIn(expression(value)); - } - + /** + * Create a {@code NOT IN} predicate. + * + * @param value + * @return + */ Predicate notIn(Expression value); - default Predicate inMultivalued(String value) { - return inMultivalued(expression(value)); - } - - Predicate inMultivalued(Expression value); - - default Predicate notInMultivalued(String value) { - return notInMultivalued(expression(value)); - } - - Predicate notInMultivalued(Expression value); - - default Predicate memberOf(String value) { - return memberOf(expression(value)); - } - + /** + * Create a {@code MEMBER OF <collection>} predicate. + * + * @param value + * @return + */ Predicate memberOf(Expression value); - default Predicate notMemberOf(String value) { - return notMemberOf(expression(value)); - } - + /** + * Create a {@code NOT MEMBER OF <collection>} predicate. + * + * @param value + * @return + */ Predicate notMemberOf(Expression value); default Predicate like(String value, String escape) { return like(expression(value), escape); } + /** + * Create a {@code LIKE … ESCAPE} predicate. + * + * @param value + * @return + */ Predicate like(Expression value, String escape); - default Predicate notLike(String value, String escape) { - return notLike(expression(value), escape); - } - + /** + * Create a {@code NOT LIKE … ESCAPE} predicate. + * + * @param value + * @return + */ Predicate notLike(Expression value, String escape); - default Predicate eq(String value) { - return eq(expression(value)); - } - + /** + * Create a {@code =} (equals) predicate. + * + * @param value + * @return + */ Predicate eq(Expression value); - default Predicate neq(String value) { - return neq(expression(value)); - } - + /** + * Create a {@code <>} (not equals) predicate. + * + * @param value + * @return + */ Predicate neq(Expression value); } - record PathExpression(PathAndOrigin pas) implements Expression { - - @Override - public String render(RenderContext context) { - return render(pas, context); - - } - - public static String render(PathAndOrigin pas, RenderContext context) { - - if (pas.path().hasNext() || !pas.onTheJoin()) { - return context.prefixWithAlias(pas.origin(), pas.path().toDotPath()); - } else { - return context.getAlias(pas.origin()); - } - } - - @Override - public String toString() { - return render(RenderContext.EMPTY); - } - } - record LiteralExpression(String expression) implements Expression { @Override @@ -1243,7 +1326,7 @@ record InPredicate(Expression path, String operator, Expression predicate) imple @Override public String render(RenderContext context) { - //TODO: should we rather wrap it with nested or check if its a nested predicate before we call render + // TODO: should we rather wrap it with nested or check if its a nested predicate before we call render return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context)); } @@ -1299,20 +1382,51 @@ public String toString() { * @param origin * @param onTheJoin whether the path should target the join itself instead of matching {@link PropertyPath}. */ - public record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) { + record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) implements PathExpression { + + @Override + public PropertyPath getPropertyPath() { + return path; + } + + @Override + public String render(RenderContext context) { + if (path().hasNext() || !onTheJoin()) { + return context.prefixWithAlias(origin(), path().toDotPath()); + } else { + return context.getAlias(origin()); + } + } } + /** + * Value object capturing parameter placeholder. + * + * @param placeholder + */ public record ParameterPlaceholder(String placeholder) { public ParameterPlaceholder { Assert.hasText(placeholder, "Placeholder must not be null nor empty"); } + /** + * Factory method to create a parameter placeholder using a parameter {@code index}. + * + * @param index the parameter index. + * @return an indexed parameter placeholder. + */ public static ParameterPlaceholder indexed(int index) { return new ParameterPlaceholder("?%s".formatted(index)); } + /** + * Factory method to create a parameter placeholder using a parameter {@code name}. + * + * @param name the parameter name. + * @return a named parameter placeholder. + */ public static ParameterPlaceholder named(String name) { Assert.hasText(name, "Placeholder name must not be empty"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java index d3b32380cd..500a7d4e84 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -15,34 +15,16 @@ */ package org.springframework.data.jpa.repository.query; -import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ELEMENT_COLLECTION; -import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_MANY; -import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.MANY_TO_ONE; -import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_MANY; -import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.ONE_TO_ONE; - -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; import jakarta.persistence.criteria.From; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.JoinType; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Attribute.PersistentAttributeType; import jakarta.persistence.metamodel.Bindable; import jakarta.persistence.metamodel.ManagedType; import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.PluralAttribute; -import jakarta.persistence.metamodel.SingularAttribute; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Member; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; + import java.util.Objects; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.mapping.PropertyPath; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -52,25 +34,12 @@ */ class JpqlUtils { - private static final Map> ASSOCIATION_TYPES; - - static { - Map> persistentAttributeTypes = new HashMap<>(); - persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class); - persistentAttributeTypes.put(ONE_TO_MANY, null); - persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class); - persistentAttributeTypes.put(MANY_TO_MANY, null); - persistentAttributeTypes.put(ELEMENT_COLLECTION, null); - - ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes); - } - - static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property) { return toExpressionRecursively(metamodel, source, from, property, false); } - static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property, boolean isForSelection) { return toExpressionRecursively(metamodel, source, from, property, isForSelection, false); } @@ -84,16 +53,13 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode * @param hasRequiredOuterJoin has a parent already required an outer join? * @return the expression */ - @SuppressWarnings("unchecked") - static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { String segment = property.getSegment(); boolean isLeafProperty = !property.hasNext(); - - boolean requiresOuterJoin = requiresOuterJoin(metamodel, source, from, property, isForSelection, - hasRequiredOuterJoin); + boolean requiresOuterJoin = requiresOuterJoin(metamodel, from, property, isForSelection, hasRequiredOuterJoin); // if it does not require an outer join and is a leaf, simply get the segment if (!requiresOuterJoin && isLeafProperty) { @@ -103,10 +69,7 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode // get or create the join JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment) : JpqlQueryBuilder.innerJoin(source, segment); -// JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER; -// Join join = QueryUtils.getOrCreateJoin(from, segment, joinType); -// // if it's a leaf, return the join if (isLeafProperty) { return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true); @@ -114,11 +77,11 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null"); -// ManagedType managedType = ; - Bindable managedTypeForModel = (Bindable) getManagedTypeForModel(from); -// Attribute joinAttribute = getModelForPath(metamodel, property, getManagedTypeForModel(from), null); - // recurse with the next property - return toExpressionRecursively(metamodel, joinSource, managedTypeForModel, nextProperty, isForSelection, requiresOuterJoin); + ManagedType managedTypeForModel = QueryUtils.getManagedTypeForModel(from); + Attribute nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from); + + return toExpressionRecursively(metamodel, joinSource, (Bindable) nextAttribute, nextProperty, isForSelection, + requiresOuterJoin); } /** @@ -127,17 +90,16 @@ static JpqlQueryBuilder.PathAndOrigin toExpressionRecursively(Metamodel metamode * ensures outer joins are used even when Hibernate defaults to inner joins (HHH-12712 and HHH-12999) * * @param metamodel - * @param source * @param bindable * @param propertyPath * @param isForSelection * @param hasRequiredOuterJoin * @return */ - static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable bindable, - PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) { + static boolean requiresOuterJoin(Metamodel metamodel, Bindable bindable, PropertyPath propertyPath, + boolean isForSelection, boolean hasRequiredOuterJoin) { - ManagedType managedType = getManagedTypeForModel(bindable); + ManagedType managedType = QueryUtils.getManagedTypeForModel(bindable); Attribute attribute = getModelForPath(metamodel, propertyPath, managedType, bindable); boolean isPluralAttribute = bindable instanceof PluralAttribute; @@ -145,7 +107,7 @@ static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin so return isPluralAttribute; } - if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) { + if (!QueryUtils.ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) { return false; } @@ -155,47 +117,14 @@ static boolean requiresOuterJoin(Metamodel metamodel, JpqlQueryBuilder.Origin so // explicit outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712 // and https://github.com/eclipse-ee4j/jpa-api/issues/170 boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType() - && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", "")); + && StringUtils.hasText(QueryUtils.getAnnotationProperty(attribute, "mappedBy", "")); boolean isLeafProperty = !propertyPath.hasNext(); if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) { return false; } - return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); - } - - @Nullable - private static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { - - Class associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType()); - - if (associationAnnotation == null) { - return defaultValue; - } - - Member member = attribute.getJavaMember(); - - if (!(member instanceof AnnotatedElement annotatedMember)) { - return defaultValue; - } - - Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation); - return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName); - } - - @Nullable - private static ManagedType getManagedTypeForModel(Bindable model) { - - if (model instanceof ManagedType managedType) { - return managedType; - } - - if (!(model instanceof SingularAttribute singularAttribute)) { - return null; - } - - return singularAttribute.getType() instanceof ManagedType managedType ? managedType : null; + return hasRequiredOuterJoin || QueryUtils.getAnnotationProperty(attribute, "optional", true); } @Nullable diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index 844de60594..9ef9d4e790 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -21,11 +21,11 @@ import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import jakarta.persistence.metamodel.Bindable; +import jakarta.persistence.metamodel.Metamodel; import java.util.List; -import jakarta.persistence.metamodel.Bindable; -import jakarta.persistence.metamodel.Metamodel; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; @@ -147,7 +147,7 @@ public JpqlStrategy(Metamodel metamodel, Bindable from, JpqlQueryBuilder.Enti public JpqlQueryBuilder.Expression createExpression(String property) { PropertyPath path = PropertyPath.from(property, from.getBindableJavaType()); - return JpqlQueryBuilder.expression(JpqlUtils.toExpressionRecursively(metamodel, entity, from, path)); + return JpqlUtils.toExpressionRecursively(metamodel, entity, from, path); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index f8c567f352..922719633d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -242,17 +242,12 @@ public Object prepare(@Nullable Object value) { if (String.class.equals(parameterType) && !noWildcards) { - switch (type) { - case STARTING_WITH: - return String.format("%s%%", escape.escape(value.toString())); - case ENDING_WITH: - return String.format("%%%s", escape.escape(value.toString())); - case CONTAINING: - case NOT_CONTAINING: - return String.format("%%%s%%", escape.escape(value.toString())); - default: - return value; - } + return switch (type) { + case STARTING_WITH -> String.format("%s%%", escape.escape(value.toString())); + case ENDING_WITH -> String.format("%%%s", escape.escape(value.toString())); + case CONTAINING, NOT_CONTAINING -> String.format("%%%s%%", escape.escape(value.toString())); + default -> value; + }; } return Collection.class.isAssignableFrom(parameterType) // @@ -710,7 +705,7 @@ static MethodInvocationArgument ofParameter(BindingIdentifier identifier) { boolean isExpression(); /** - * @return {@code true} if the origin is an expression. + * @return {@code true} if the origin is synthetic (contributed by e.g. KeysetPagination) */ boolean isSynthetic(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index 806d379539..dfde858dfa 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -22,9 +22,10 @@ import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaQuery; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -57,6 +58,7 @@ */ public class PartTreeJpaQuery extends AbstractJpaQuery { + private static final Logger log = LoggerFactory.getLogger(PartTreeJpaQuery.class); private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; private final PartTree tree; @@ -201,7 +203,6 @@ private static boolean expectsCollection(Type type) { return type == Type.IN || type == Type.NOT_IN; } - /** * Query preparer to create {@link CriteriaQuery} instances and potentially cache them. * @@ -222,6 +223,11 @@ public Query createQuery(JpaParametersParameterAccessor accessor) { String jpql = creator.createQuery(sort); Query query; + if (log.isDebugEnabled()) { + log.debug(String.format("%s: Derived query for query method [%s]: '%s'", getClass().getSimpleName(), + getQueryMethod(), jpql)); + } + try { query = creator.useTupleQuery() ? em.createQuery(jpql, Tuple.class) : em.createQuery(jpql); } catch (Exception e) { @@ -273,11 +279,14 @@ private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPositio protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccessor accessor) { + JpqlQueryCreator jpqlQueryCreator; synchronized (cache) { - JpqlQueryCreator jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for simple properties - if (jpqlQueryCreator != null) { - return jpqlQueryCreator; - } + jpqlQueryCreator = cache.get(sort, accessor); // this caching thingy is broken due to IS NULL rendering for + // simple properties + } + + if (jpqlQueryCreator != null) { + return jpqlQueryCreator; } EntityManager entityManager = getEntityManager(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java index 71f952c2c8..51183f4c6c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.HashMap; +import java.util.BitSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -25,11 +25,13 @@ import org.springframework.util.ObjectUtils; /** + * Cache for PartTree queries. + * * @author Christoph Strobl */ class PartTreeQueryCache { - private final Map cache = new LinkedHashMap() { + private final Map cache = new LinkedHashMap<>() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 256; @@ -49,9 +51,14 @@ JpqlQueryCreator put(Sort sort, JpaParametersParameterAccessor accessor, JpqlQue static class CacheKey { private final Sort sort; - private final Map params; - public CacheKey(Sort sort, Map params) { + /** + * Bitset of null/non-null parameter values. A 0 bit means the parameter value is {@code null}, a 1 bit means the + * parameter is not {@code null}. + */ + private final BitSet params; + + public CacheKey(Sort sort, BitSet params) { this.sort = sort; this.params = params; } @@ -59,20 +66,22 @@ public CacheKey(Sort sort, Map params) { static CacheKey of(Sort sort, JpaParametersParameterAccessor accessor) { Object[] values = accessor.getValues(); + if (ObjectUtils.isEmpty(values)) { - return new CacheKey(sort, Map.of()); + return new CacheKey(sort, new BitSet()); } return new CacheKey(sort, toNullableMap(values)); } - static Map toNullableMap(Object[] args) { + static BitSet toNullableMap(Object[] args) { - Map paramMap = new HashMap<>(args.length); + BitSet bitSet = new BitSet(args.length); for (int i = 0; i < args.length; i++) { - paramMap.put(i, args[i] != null ? Nulled.NO : Nulled.YES); + bitSet.set(i, args[i] != null); } - return paramMap; + + return bitSet; } @Override @@ -93,8 +102,4 @@ public int hashCode() { } } - enum Nulled { - YES, NO - } - } 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 c75137267b..71919e5ffa 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 @@ -130,7 +130,7 @@ public abstract class QueryUtils { private static final Pattern CONSTRUCTOR_EXPRESSION; - private static final Map> ASSOCIATION_TYPES; + static final Map> ASSOCIATION_TYPES; private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3; private static final int VARIABLE_NAME_GROUP_INDEX = 4; @@ -844,8 +844,7 @@ static boolean requiresOuterJoin(From from, PropertyPath property, boolean return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); } - @Nullable - private static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { + static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { Class associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType()); @@ -974,7 +973,7 @@ private static Bindable getModelForPath(PropertyPath path, @Nullable ManagedT * @return */ @Nullable - private static ManagedType getManagedTypeForModel(Bindable model) { + static ManagedType getManagedTypeForModel(Bindable model) { if (model instanceof ManagedType managedType) { return managedType; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java index dc2866fa8b..9073848ff2 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java @@ -15,9 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.ElementCollection; import jakarta.persistence.EntityManager; @@ -38,6 +37,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.FieldSource; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; @@ -52,6 +52,8 @@ import org.springframework.lang.Nullable; /** + * Unit tests for {@link JpaQueryCreator}. + * * @author Christoph Strobl */ class JpaQueryCreatorTests { @@ -61,7 +63,7 @@ class JpaQueryCreatorTests { static List ignoreCaseTemplates = List.of(JpqlQueryTemplates.LOWER, JpqlQueryTemplates.UPPER); - @Test + @Test // GH-3588 void simpleProperty() { queryCreator(ORDER) // @@ -72,7 +74,7 @@ void simpleProperty() { .validateQuery(); } - @Test + @Test // GH-3588 void simpleNullProperty() { queryCreator(ORDER) // @@ -83,7 +85,7 @@ void simpleNullProperty() { .validateQuery(); } - @Test + @Test // GH-3588 void negatingSimpleProperty() { queryCreator(ORDER) // @@ -94,7 +96,7 @@ void negatingSimpleProperty() { .validateQuery(); } - @Test + @Test // GH-3588 void negatingSimpleNullProperty() { queryCreator(ORDER) // @@ -105,7 +107,7 @@ void negatingSimpleNullProperty() { .validateQuery(); } - @Test + @Test // GH-3588 void simpleAnd() { queryCreator(ORDER) // @@ -116,7 +118,7 @@ void simpleAnd() { .validateQuery(); } - @Test + @Test // GH-3588 void simpleOr() { queryCreator(ORDER) // @@ -127,7 +129,7 @@ void simpleOr() { .validateQuery(); } - @Test + @Test // GH-3588 void simpleAndOr() { queryCreator(ORDER) // @@ -139,7 +141,7 @@ void simpleAndOr() { .validateQuery(); } - @Test + @Test // GH-3588 void distinct() { queryCreator(ORDER) // @@ -150,7 +152,7 @@ void distinct() { .validateQuery(); } - @Test + @Test // GH-3588 void count() { queryCreator(ORDER) // @@ -162,7 +164,7 @@ void count() { .validateQuery(); } - @Test + @Test // GH-3588 void countWithJoins() { queryCreator(ORDER) // @@ -174,7 +176,7 @@ void countWithJoins() { .validateQuery(); } - @Test + @Test // GH-3588 void countDistinct() { queryCreator(ORDER) // @@ -186,7 +188,7 @@ void countDistinct() { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void simplePropertyIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -200,7 +202,7 @@ void simplePropertyIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void simplePropertyAllIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -216,7 +218,7 @@ void simplePropertyAllIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void simplePropertyMixedCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -231,7 +233,7 @@ void simplePropertyMixedCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @Test + @Test // GH-3588 void lessThan() { queryCreator(ORDER) // @@ -242,7 +244,7 @@ void lessThan() { .validateQuery(); } - @Test + @Test // GH-3588 void lessThanEqual() { queryCreator(ORDER) // @@ -253,7 +255,7 @@ void lessThanEqual() { .validateQuery(); } - @Test + @Test // GH-3588 void greaterThan() { queryCreator(ORDER) // @@ -264,7 +266,7 @@ void greaterThan() { .validateQuery(); } - @Test + @Test // GH-3588 void before() { queryCreator(ORDER) // @@ -275,7 +277,7 @@ void before() { .validateQuery(); } - @Test + @Test // GH-3588 void after() { queryCreator(ORDER) // @@ -286,7 +288,7 @@ void after() { .validateQuery(); } - @Test + @Test // GH-3588 void between() { queryCreator(ORDER) // @@ -297,7 +299,7 @@ void between() { .validateQuery(); } - @Test + @Test // GH-3588 void isNull() { queryCreator(ORDER) // @@ -307,7 +309,7 @@ void isNull() { .validateQuery(); } - @Test + @Test // GH-3588 void isNotNull() { queryCreator(ORDER) // @@ -317,7 +319,7 @@ void isNotNull() { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @ValueSource(strings = { "", "spring", "%spring", "spring%", "%spring%" }) void like(String parameterValue) { @@ -330,7 +332,7 @@ void like(String parameterValue) { .validateQuery(); } - @Test + @Test // GH-3588 void containingString() { queryCreator(ORDER) // @@ -342,7 +344,7 @@ void containingString() { .validateQuery(); } - @Test + @Test // GH-3588 void notContainingString() { queryCreator(ORDER) // @@ -354,7 +356,7 @@ void notContainingString() { .validateQuery(); } - @Test + @Test // GH-3588 void in() { queryCreator(ORDER) // @@ -366,7 +368,7 @@ void in() { .validateQuery(); } - @Test + @Test // GH-3588 void notIn() { queryCreator(ORDER) // @@ -378,7 +380,7 @@ void notIn() { .validateQuery(); } - @Test + @Test // GH-3588 void containingSingleEntryElementCollection() { queryCreator(ORDER) // @@ -389,7 +391,7 @@ void containingSingleEntryElementCollection() { .validateQuery(); } - @Test + @Test // GH-3588 void notContainingSingleEntryElementCollection() { queryCreator(ORDER) // @@ -400,7 +402,7 @@ void notContainingSingleEntryElementCollection() { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void likeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -415,7 +417,7 @@ void likeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @ValueSource(strings = { "", "spring", "%spring", "spring%", "%spring%" }) void notLike(String parameterValue) { @@ -428,7 +430,7 @@ void notLike(String parameterValue) { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void notLikeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -443,7 +445,7 @@ void notLikeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @Test + @Test // GH-3588 void startingWith() { queryCreator(ORDER) // @@ -455,7 +457,7 @@ void startingWith() { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void startingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -470,7 +472,7 @@ void startingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @Test + @Test // GH-3588 void endingWith() { queryCreator(ORDER) // @@ -482,7 +484,7 @@ void endingWith() { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void endingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { @@ -497,7 +499,7 @@ void endingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .validateQuery(); } - @Test + @Test // GH-3588 void greaterThanEqual() { queryCreator(ORDER) // @@ -508,7 +510,7 @@ void greaterThanEqual() { .validateQuery(); } - @Test + @Test // GH-3588 void isTrue() { queryCreator(ORDER) // @@ -518,7 +520,7 @@ void isTrue() { .validateQuery(); } - @Test + @Test // GH-3588 void isFalse() { queryCreator(ORDER) // @@ -528,7 +530,7 @@ void isFalse() { .validateQuery(); } - @Test + @Test // GH-3588 void empty() { queryCreator(ORDER) // @@ -538,7 +540,7 @@ void empty() { .validateQuery(); } - @Test + @Test // GH-3588 void notEmpty() { queryCreator(ORDER) // @@ -548,7 +550,7 @@ void notEmpty() { .validateQuery(); } - @Test + @Test // GH-3588 void sortBySingle() { queryCreator(ORDER) // @@ -559,7 +561,7 @@ void sortBySingle() { .validateQuery(); } - @Test + @Test // GH-3588 void sortByMulti() { queryCreator(ORDER) // @@ -571,7 +573,7 @@ void sortByMulti() { } @Disabled("should we support this?") - @ParameterizedTest + @ParameterizedTest // GH-3588 @FieldSource("ignoreCaseTemplates") void sortBySingleIngoreCase(JpqlQueryTemplates ingoreCase) { @@ -583,7 +585,7 @@ void sortBySingleIngoreCase(JpqlQueryTemplates ingoreCase) { ingoreCase.getIgnoreCaseOperator()); } - @Test + @Test // GH-3588 void matchSimpleJoin() { queryCreator(ORDER) // @@ -594,19 +596,19 @@ void matchSimpleJoin() { .validateQuery(); } - @Test + @Test // GH-3588 void matchSimpleNestedJoin() { queryCreator(ORDER) // .forTree(Order.class, "findOrderByLineItemsProductNameIs") // .withParameters("spring") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE p.name = ?1", + .expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE p.name = ?1", Order.class.getName()) // .validateQuery(); } - @Test + @Test // GH-3588 void matchMultiOnNestedJoin() { queryCreator(ORDER) // @@ -614,12 +616,12 @@ void matchMultiOnNestedJoin() { .withParameters(10, "spring") // .as(QueryCreatorTester::create) // .expectJpql( - "SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE l.quantity > ?1 AND p.name = ?2", + "SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE l.quantity > ?1 AND p.name = ?2", Order.class.getName()) // .validateQuery(); } - @Test + @Test // GH-3588 void matchSameEntityMultipleTimes() { queryCreator(ORDER) // @@ -627,12 +629,12 @@ void matchSameEntityMultipleTimes() { .withParameters("spring", "sukrauq") // .as(QueryCreatorTester::create) // .expectJpql( - "SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p WHERE p.name = ?1 AND p.name != ?2", + "SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE p.name = ?1 AND p.name != ?2", Order.class.getName()) // .validateQuery(); } - @Test + @Test // GH-3588 void matchSameEntityMultipleTimesViaDifferentProperties() { queryCreator(ORDER) // @@ -640,12 +642,12 @@ void matchSameEntityMultipleTimesViaDifferentProperties() { .withParameters(10, "spring") // .as(QueryCreatorTester::create) // .expectJpql( - "SELECT o FROM %s o LEFT JOIN o.lineItems l INNER JOIN l.product p INNER JOIN l.product2 join_0 WHERE p.name = ?1 AND join_0.name = ?2", + "SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p LEFT JOIN l.product2 join_0 WHERE p.name = ?1 AND join_0.name = ?2", Order.class.getName()) // .validateQuery(); } - @Test + @Test // GH-3588 void dtoProjection() { queryCreator(ORDER) // @@ -658,7 +660,7 @@ void dtoProjection() { .validateQuery(); } - @Test + @Test // GH-3588 void interfaceProjection() { queryCreator(ORDER) // @@ -671,7 +673,7 @@ void interfaceProjection() { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @ValueSource(classes = { Tuple.class, Map.class }) void tupleProjection(Class resultType) { @@ -685,7 +687,7 @@ void tupleProjection(Class resultType) { .validateQuery(); } - @ParameterizedTest + @ParameterizedTest // GH-3588 @ValueSource(classes = { Long.class, List.class, Person.class }) void delete(Class resultType) { @@ -698,7 +700,7 @@ void delete(Class resultType) { .validateQuery(); } - @Test + @Test // GH-3588 void exists() { queryCreator(PERSON) // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java index 04fb7079de..1146713058 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java @@ -15,8 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.repository.query.JpqlQueryBuilder.*; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; @@ -28,26 +28,15 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.AbstractJpqlQuery; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Entity; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Expression; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Join; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.OrderExpression; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.PathAndOrigin; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Predicate; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.RenderContext; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.SelectStep; -import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.WhereStep; /** + * Unit tests for {@link JpqlQueryBuilder}. + * * @author Christoph Strobl */ class JpqlQueryBuilderUnitTests { - @Test + @Test // GH-3588 void placeholdersRenderCorrectly() { assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1"); @@ -56,89 +45,88 @@ void placeholdersRenderCorrectly() { assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1"); } - @Test - void placeholdersErrorOnInvaludInput() { + @Test // GH-3588 + void placeholdersErrorOnInvalidInput() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> JpqlQueryBuilder.parameter((String) null)); assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> JpqlQueryBuilder.parameter("")); } - @Test + @Test // GH-3588 void stringLiteralRendersAsQuotedString() { - assertThat(JpqlQueryBuilder.stringLiteral("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'"); + assertThat(literal("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'"); /* JPA Spec - 4.6.1 Literals: > A string literal that includes a single quote is represented by two single quotes--for example: 'literal''s'. */ - assertThat(JpqlQueryBuilder.stringLiteral("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'"); + assertThat(literal("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'"); } - @Test + @Test // GH-3588 void entity() { Entity entity = JpqlQueryBuilder.entity(Order.class); - assertThat(entity.alias()).isEqualTo("o"); - assertThat(entity.entity()).isEqualTo(Order.class.getName()); - assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); // TODO: this really confusing - assertThat(entity.simpleName()).isEqualTo(Order.class.getSimpleName()); + assertThat(entity.getAlias()).isEqualTo("o"); + assertThat(entity.getEntity()).isEqualTo(Order.class.getName()); + assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); } - @Test + @Test // GH-3588 void literalExpressionRendersAsIs() { - Expression expression = JpqlQueryBuilder.expression("CONCAT(person.lastName, ‘, ’, person.firstName))"); + Expression expression = expression("CONCAT(person.lastName, ‘, ’, person.firstName))"); assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))"); } - @Test + @Test // GH-3588 void xxx() { Entity entity = JpqlQueryBuilder.entity(Order.class); PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date"); - String fragment = JpqlQueryBuilder.where(orderDate).eq("{d '2024-11-05'}").render(ctx(entity)); + String fragment = JpqlQueryBuilder.where(orderDate).eq(expression("{d '2024-11-05'}")).render(ctx(entity)); assertThat(fragment).isEqualTo("o.date = {d '2024-11-05'}"); - - // JpqlQueryBuilder.where(PathAndOrigin) } - @Test + @Test // GH-3588 void predicateRendering() { - Entity entity = JpqlQueryBuilder.entity(Order.class); WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); + RenderContext context = ctx(entity); + + assertThat(where.between(expression("'AT'"), expression("'DE'")).render(context)) + .isEqualTo("o.country BETWEEN 'AT' AND 'DE'"); + assertThat(where.eq(expression("'AT'")).render(context)).isEqualTo("o.country = 'AT'"); + assertThat(where.eq(literal("AT")).render(context)).isEqualTo("o.country = 'AT'"); + assertThat(where.gt(expression("'AT'")).render(context)).isEqualTo("o.country > 'AT'"); + assertThat(where.gte(expression("'AT'")).render(context)).isEqualTo("o.country >= 'AT'"); - assertThat(where.between("'AT'", "'DE'").render(ctx(entity))).isEqualTo("o.country BETWEEN 'AT' AND 'DE'"); - assertThat(where.eq("'AT'").render(ctx(entity))).isEqualTo("o.country = 'AT'"); - assertThat(where.eq(JpqlQueryBuilder.stringLiteral("AT")).render(ctx(entity))).isEqualTo("o.country = 'AT'"); - assertThat(where.gt("'AT'").render(ctx(entity))).isEqualTo("o.country > 'AT'"); - assertThat(where.gte("'AT'").render(ctx(entity))).isEqualTo("o.country >= 'AT'"); // TODO: that is really really bad // lange namen - assertThat(where.in("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); + assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')"); // 1 in age - cleanup what is not used - remove everything eles // assertThat(where.inMultivalued("'AT', 'DE'").render(ctx(entity))).isEqualTo("o.country IN ('AT', 'DE')"); // - assertThat(where.isEmpty().render(ctx(entity))).isEqualTo("o.country IS EMPTY"); - assertThat(where.isNotEmpty().render(ctx(entity))).isEqualTo("o.country IS NOT EMPTY"); - assertThat(where.isTrue().render(ctx(entity))).isEqualTo("o.country = TRUE"); - assertThat(where.isFalse().render(ctx(entity))).isEqualTo("o.country = FALSE"); - assertThat(where.isNull().render(ctx(entity))).isEqualTo("o.country IS NULL"); - assertThat(where.isNotNull().render(ctx(entity))).isEqualTo("o.country IS NOT NULL"); - assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity))) + assertThat(where.isEmpty().render(context)).isEqualTo("o.country IS EMPTY"); + assertThat(where.isNotEmpty().render(context)).isEqualTo("o.country IS NOT EMPTY"); + assertThat(where.isTrue().render(context)).isEqualTo("o.country = TRUE"); + assertThat(where.isFalse().render(context)).isEqualTo("o.country = FALSE"); + assertThat(where.isNull().render(context)).isEqualTo("o.country IS NULL"); + assertThat(where.isNotNull().render(context)).isEqualTo("o.country IS NOT NULL"); + assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context)) .isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'"); - assertThat(where.notLike("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(ctx(entity))) + assertThat(where.notLike(expression("'\\_%'"), "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context)) .isEqualTo("o.country NOT LIKE '\\_%' ESCAPE '\\'"); - assertThat(where.lt("'AT'").render(ctx(entity))).isEqualTo("o.country < 'AT'"); - assertThat(where.lte("'AT'").render(ctx(entity))).isEqualTo("o.country <= 'AT'"); - assertThat(where.memberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' MEMBER OF o.country"); + assertThat(where.lt(expression("'AT'")).render(context)).isEqualTo("o.country < 'AT'"); + assertThat(where.lte(expression("'AT'")).render(context)).isEqualTo("o.country <= 'AT'"); + assertThat(where.memberOf(expression("'AT'")).render(context)).isEqualTo("'AT' MEMBER OF o.country"); // TODO: can we have this where.value(foo).memberOf(pathAndOrigin); - assertThat(where.notMemberOf("'AT'").render(ctx(entity))).isEqualTo("'AT' NOT MEMBER OF o.country"); - assertThat(where.neq("'AT'").render(ctx(entity))).isEqualTo("o.country != 'AT'"); + assertThat(where.notMemberOf(expression("'AT'")).render(context)).isEqualTo("'AT' NOT MEMBER OF o.country"); + assertThat(where.neq(expression("'AT'")).render(context)).isEqualTo("o.country != 'AT'"); } - @Test + @Test // GH-3588 void selectRendering() { // make sure things are immutable @@ -147,25 +135,12 @@ void selectRendering() { assertThat(select.count().render()).startsWith("SELECT COUNT(o)"); assertThat(select.distinct().entity().render()).startsWith("SELECT DISTINCT o "); assertThat(select.distinct().count().render()).startsWith("SELECT COUNT(DISTINCT o) "); - assertThat(JpqlQueryBuilder.selectFrom(Order.class).select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render()) - .startsWith("SELECT o.country "); + assertThat(JpqlQueryBuilder.selectFrom(Order.class) + .select(JpqlQueryBuilder.path(JpqlQueryBuilder.entity(Order.class), "country")).render()) + .startsWith("SELECT o.country "); } -// @Test -// void sorting() { -// -// JpqlQueryBuilder.orderBy(new OrderExpression() , Sort.Order.asc("country")); -// -// Entity entity = JpqlQueryBuilder.entity(Order.class); -// -// AbstractJpqlQuery query = JpqlQueryBuilder.selectFrom(Order.class) -// .entity() -// .orderBy() -// .where(context -> "1 = 1"); -// -// } - - @Test + @Test // GH-3588 void joins() { Entity entity = JpqlQueryBuilder.entity(LineItem.class); @@ -175,14 +150,14 @@ void joins() { PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name"); PathAndOrigin personName = JpqlQueryBuilder.path(li_pr2, "name"); - String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30")) - .and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("ex40"))).render(ctx(entity)); + String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30")) + .and(JpqlQueryBuilder.where(personName).eq(literal("ex40"))).render(ctx(entity)); assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'ex40'"); } - @Test - void x2() { + @Test // GH-3588 + void joinOnPaths() { Entity entity = JpqlQueryBuilder.entity(LineItem.class); Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); @@ -191,36 +166,17 @@ void x2() { PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name"); PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name"); - String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30")) - .and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity)); - - assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'"); - } - - @Test - void x3() { - - Entity entity = JpqlQueryBuilder.entity(LineItem.class); - Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); - Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person"); - - PathAndOrigin productName = JpqlQueryBuilder.path(li_pr, "name"); - PathAndOrigin personName = JpqlQueryBuilder.path(li_pe, "name"); - - // JpqlQueryBuilder.and("x = y", "a = b"); -> x = y AND a = b - - // JpqlQueryBuilder.nested(JpqlQueryBuilder.and("x = y", "a = b")) (x = y AND a = b) - - String fragment = JpqlQueryBuilder.where(productName).eq(JpqlQueryBuilder.stringLiteral("ex30")) - .and(JpqlQueryBuilder.where(personName).eq(JpqlQueryBuilder.stringLiteral("cstrobl"))).render(ctx(entity)); + String fragment = JpqlQueryBuilder.where(productName).eq(literal("ex30")) + .and(JpqlQueryBuilder.where(personName).eq(literal("cstrobl"))).render(ctx(entity)); assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'"); } static RenderContext ctx(Entity... entities) { + Map aliases = new LinkedHashMap<>(entities.length); for (Entity entity : entities) { - aliases.put(entity, entity.alias()); + aliases.put(entity, entity.getAlias()); } return new RenderContext(aliases); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java index 355a34aff3..beb8e68a76 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java @@ -48,24 +48,26 @@ class ParameterMetadataProviderIntegrationTests { @PersistenceContext EntityManager em; - /* TODO + @Test // DATAJPA-758 - void forwardsParameterNameIfTransparentlyNamed() throws Exception { + void usesIndexedParametersForExplicityNamedParameters() throws Exception { ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByFirstname", String.class)); - ParameterMetadata metadata = provider.next(new Part("firstname", User.class)); + ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("firstname", User.class)); - assertThat(metadata.getName()).isEqualTo("name"); + assertThat(metadata.getName()).isNull(); + assertThat(metadata.getPosition()).isEqualTo(1); } @Test // DATAJPA-758 - void forwardsParameterNameIfExplicitlyAnnotated() throws Exception { + void usesIndexedParameters() throws Exception { ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByLastname", String.class)); - ParameterMetadata metadata = provider.next(new Part("lastname", User.class)); + ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("lastname", User.class)); - assertThat(metadata.getExpression().getName()).isNull(); - } */ + assertThat(metadata.getName()).isNull(); + assertThat(metadata.getPosition()).isEqualTo(1); + } @Test // DATAJPA-772 void doesNotApplyLikeExpansionOnNonStringProperties() throws Exception { From 57b190fb4d348a0e2550c257c56897ceb1f9c577 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 21 Nov 2024 10:44:50 +0100 Subject: [PATCH 07/94] Revise PartTree query caching. See #3588 Original pull request: #3653 --- .../repository/query/JpqlQueryBuilder.java | 2 +- .../repository/query/PartTreeJpaQuery.java | 21 +++++++------------ .../repository/query/PartTreeQueryCache.java | 5 +++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index db6697a9d5..287b397384 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -450,7 +450,7 @@ default Select instantiate(Class resultType, Collection cache = new LinkedHashMap<>() { + private final Map cache = Collections.synchronizedMap(new LinkedHashMap<>() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 256; } - }; + }); @Nullable JpqlQueryCreator get(Sort sort, JpaParametersParameterAccessor accessor) { From 507dce68905c5a72474e46aaccfc5511237b4885 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:10:28 +0100 Subject: [PATCH 08/94] Upgrade to JPA 3.2. Closes: #3673 Original Pull Request: #3695 --- pom.xml | 118 +++++++++++++++++- .../data/jpa/util/TestMetaModel.java | 13 +- 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 887a96a4db..49d8da0e21 100755 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ 7.0.0-SNAPSHOT 2.7.4

2.3.232

- 3.1.0 + 3.2.0 5.0 9.1.0 42.7.4 @@ -112,6 +112,44 @@ + + all-dbs + + + + org.apache.maven.plugins + maven-surefire-plugin + + + mysql-test + test + + test + + + + **/MySql*IntegrationTests.java + + + + + postgres-test + test + + test + + + + **/Postgres*IntegrationTests.java + + + + + + + + + eclipselink-next @@ -152,6 +190,84 @@ + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.springframework + spring-instrument + ${spring} + runtime + + + + + + default-test + + + **/* + + + + + unit-test + + test + + test + + + **/*UnitTests.java + + + + + integration-test + + test + + test + + + **/*IntegrationTests.java + **/*Tests.java + + + **/*UnitTests.java + **/EclipseLink* + **/MySql* + **/Postgres* + + + -javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring}/spring-instrument-${spring}.jar + + + + + eclipselink-test + + test + + test + + + **/EclipseLink*Tests.java + + + -javaagent:${settings.localRepository}/org/eclipse/persistence/org.eclipse.persistence.jpa/${eclipselink}/org.eclipse.persistence.jpa-${eclipselink}.jar + -javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring}/spring-instrument-${spring}.jar + + + + + + + + diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java index a755ba222b..822365b65a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java @@ -43,13 +43,13 @@ public class TestMetaModel implements Metamodel { private final Set> managedTypes; private final Lazy entityManagerFactory = Lazy.of(this::init); private final Lazy metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel()); - private Lazy enityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); + private final Lazy entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); - TestMetaModel(Set> managedTypes) { + private TestMetaModel(Set> managedTypes) { this("dynamic-tests", managedTypes); } - TestMetaModel(String persistenceUnit, Set> managedTypes) { + private TestMetaModel(String persistenceUnit, Set> managedTypes) { this.persistenceUnit = persistenceUnit; this.managedTypes = managedTypes; } @@ -66,6 +66,11 @@ public EntityType entity(Class cls) { return metamodel.get().entity(cls); } + @Override + public EntityType entity(String s) { + return metamodel.get().entity(s); + } + public ManagedType managedType(Class cls) { return metamodel.get().managedType(cls); } @@ -87,7 +92,7 @@ public Set> getEmbeddables() { } public EntityManager entityManager() { - return enityManager.get(); + return entityManager.get(); } EntityManagerFactory init() { From 66afbfd65e45517017434a9f8ba6b937b84dc63d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:10:53 +0100 Subject: [PATCH 09/94] Upgrade to Hibernate 7.0 Beta1. Closes: #3671 Original Pull Request: #3695 --- Jenkinsfile | 44 ----------------- pom.xml | 48 ++----------------- .../query/QueryUtilsIntegrationTests.java | 2 +- 3 files changed, 4 insertions(+), 90 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index f87caaa0aa..a088fae34e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -58,50 +58,6 @@ pipeline { } parallel { - stage("test: hibernate 6.2 (LTS)") { - agent { - label 'data' - } - options { timeout(time: 30, unit: 'MINUTES')} - environment { - ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") - TESTCONTAINERS_IMAGE_SUBSTITUTOR = 'org.springframework.data.jpa.support.ProxyImageNameSubstitutor' - } - steps { - script { - docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { - docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) { - sh "PROFILE=all-dbs,hibernate-62 " + - "JENKINS_USER_NAME=${p['jenkins.user.name']} " + - "ci/test.sh" - } - } - } - } - } - stage("test: baseline (hibernate 6.6 snapshots)") { - agent { - label 'data' - } - options { timeout(time: 30, unit: 'MINUTES')} - environment { - ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") - TESTCONTAINERS_IMAGE_SUBSTITUTOR = 'org.springframework.data.jpa.support.ProxyImageNameSubstitutor' - } - steps { - script { - docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { - docker.image(p['docker.java.next.image']).inside(p['docker.java.inside.docker']) { - sh "PROFILE=all-dbs,hibernate-66-snapshots " + - "JENKINS_USER_NAME=${p['jenkins.user.name']} " + - "ci/test.sh" - } - } - } - } - } stage("test: java.next (next)") { agent { label 'data' diff --git a/pom.xml b/pom.xml index 49d8da0e21..42b4054cef 100755 --- a/pom.xml +++ b/pom.xml @@ -28,12 +28,9 @@ 4.13.0 - 4.0.5 - 4.0.6-SNAPSHOT - 6.6.11.Final - 6.2.33.Final - 6.6.12-SNAPSHOT - 7.0.0.Beta5 + 4.0.4 + 4.0.5-SNAPSHOT + 7.0.0.Beta1 7.0.0-SNAPSHOT 2.7.4

2.3.232

@@ -47,7 +44,6 @@ org.hibernate reuseReports -
@@ -58,44 +54,6 @@ - - hibernate-62 - - ${hibernate-62} - - - - hibernate-66-snapshots - - ${hibernate-66-snapshots} - - - - sonatype-oss - https://oss.sonatype.org/content/repositories/snapshots - - false - - - - - - hibernate-70 - - ${hibernate-70} - 3.2.0 - 4.13.2 - - - - sonatype-oss - https://oss.sonatype.org/content/repositories/snapshots - - false - - - - hibernate-70-snapshots diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java index 1d4f917a5d..8f86882049 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java @@ -32,6 +32,7 @@ import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; import jakarta.persistence.spi.PersistenceProvider; @@ -127,7 +128,6 @@ void prefersFetchOverJoin() { assertThat(expr.getParentPath()).hasFieldOrPropertyWithValue("fetched", true); assertThat(from.getFetches()).hasSize(1); - assertThat(from.getJoins()).hasSize(1); } @Test // DATAJPA-401, DATAJPA-1238 From ab44631d01eee3fb8248ad08b071790a53d012d1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:17:58 +0100 Subject: [PATCH 10/94] Upgrade to Eclipselink 5.0.0-B05. Closes: #3672 Original Pull Request: #3695 --- pom.xml | 4 ++-- .../data/jpa/repository/sample/UserRepository.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 42b4054cef..87f0b99aa7 100755 --- a/pom.xml +++ b/pom.xml @@ -28,8 +28,8 @@ 4.13.0 - 4.0.4 - 4.0.5-SNAPSHOT + 5.0.0-B05 + 5.0.0-SNAPSHOT 7.0.0.Beta1 7.0.0-SNAPSHOT 2.7.4 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 c4ebaf9f43..419e757bc0 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 @@ -546,7 +546,7 @@ List findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity List findRolesAndFirstnameBy(); - @Query(value = "FROM User u") + @Query(value = "SELECT u FROM User u") List findIdOnly(); // DATAJPA-1172 @@ -642,13 +642,13 @@ Page findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, List findAllInterfaceProjectedBy(); // GH-2045, GH-425 - @Query("select concat(?1,u.id,?2) as id from #{#entityName} u") + @Query("select concat(?1,u.id,?2) as identifier from #{#entityName} u") List findAllAndSortByFunctionResultPositionalParameter( @Param("positionalParameter1") String positionalParameter1, @Param("positionalParameter2") String positionalParameter2, Sort sort); // GH-2045, GH-425 - @Query("select concat(:namedParameter1,u.id,:namedParameter2) as id from #{#entityName} u") + @Query("select concat(:namedParameter1,u.id,:namedParameter2) as identifier from #{#entityName} u") List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter1") String namedParameter1, @Param("namedParameter2") String namedParameter2, Sort sort); From c3efdcf6d5e21ff155aa1d25213a4f46193d09fc Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Dec 2024 08:46:59 +0100 Subject: [PATCH 11/94] Switch XML persistence & orm files to version 3.2 See: #3673 Original Pull Request: #3695 --- spring-data-jpa/src/test/resources/META-INF/orm.xml | 8 ++++---- .../src/test/resources/META-INF/persistence-jmh.xml | 7 ++++--- .../src/test/resources/META-INF/persistence.xml | 5 ++++- .../src/test/resources/META-INF/persistence2.xml | 7 ++++--- .../org/springframework/data/jpa/support/mapping.xml | 5 ++++- .../data/jpa/support/module1/module1-orm.xml | 6 ++++-- .../data/jpa/support/module2/module2-orm.xml | 6 ++++-- .../org/springframework/data/jpa/support/persistence.xml | 5 ++++- .../org/springframework/data/jpa/support/persistence2.xml | 5 ++++- .../resources/simple-persistence/simple-persistence.xml | 5 ++++- 10 files changed, 40 insertions(+), 19 deletions(-) diff --git a/spring-data-jpa/src/test/resources/META-INF/orm.xml b/spring-data-jpa/src/test/resources/META-INF/orm.xml index 820a9cced2..65f0ef28fe 100644 --- a/spring-data-jpa/src/test/resources/META-INF/orm.xml +++ b/spring-data-jpa/src/test/resources/META-INF/orm.xml @@ -1,8 +1,8 @@ - + diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml b/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml index 60c6b5c97a..a78eb59468 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence-jmh.xml @@ -14,9 +14,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + org.hibernate.jpa.HibernatePersistenceProvider org.springframework.data.jpa.domain.AbstractPersistable diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence.xml b/spring-data-jpa/src/test/resources/META-INF/persistence.xml index 4f904373c3..35a8715991 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence.xml @@ -1,5 +1,8 @@ - + org.springframework.data.jpa.domain.AbstractPersistable org.springframework.data.jpa.domain.AbstractAuditable diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence2.xml b/spring-data-jpa/src/test/resources/META-INF/persistence2.xml index f4f7adb6b2..a93617de58 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence2.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence2.xml @@ -1,7 +1,8 @@ - + org.springframework.data.jpa.domain.sample.AnnotatedAuditableUser org.springframework.data.jpa.domain.sample.AuditableRole diff --git a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/mapping.xml b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/mapping.xml index 87f3460858..634c42b966 100644 --- a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/mapping.xml +++ b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/mapping.xml @@ -1,2 +1,5 @@ - + diff --git a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module1/module1-orm.xml b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module1/module1-orm.xml index da1ce9a7d4..634c42b966 100644 --- a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module1/module1-orm.xml +++ b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module1/module1-orm.xml @@ -1,3 +1,5 @@ - - + diff --git a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module2/module2-orm.xml b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module2/module2-orm.xml index da1ce9a7d4..634c42b966 100644 --- a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module2/module2-orm.xml +++ b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/module2/module2-orm.xml @@ -1,3 +1,5 @@ - - + diff --git a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence.xml b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence.xml index ad1460bad7..f75fea5ba3 100644 --- a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence.xml +++ b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence.xml @@ -1,5 +1,8 @@ - + foo.xml org.springframework.data.jpa.domain.sample.User diff --git a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence2.xml b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence2.xml index 962748440b..1666022d07 100644 --- a/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence2.xml +++ b/spring-data-jpa/src/test/resources/org/springframework/data/jpa/support/persistence2.xml @@ -1,5 +1,8 @@ - + bar.xml org.springframework.data.jpa.domain.sample.Role diff --git a/spring-data-jpa/src/test/resources/simple-persistence/simple-persistence.xml b/spring-data-jpa/src/test/resources/simple-persistence/simple-persistence.xml index 9caa71259a..706d5fb919 100644 --- a/spring-data-jpa/src/test/resources/simple-persistence/simple-persistence.xml +++ b/spring-data-jpa/src/test/resources/simple-persistence/simple-persistence.xml @@ -1,5 +1,8 @@ - + true From 53243d7f99a3bc7488d47b66303d7aa2673e4666 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:26:55 +0100 Subject: [PATCH 12/94] Consider `NULLS` precedence using `Sort` for Criteria Queries. Closes: #3587 Original Pull Request: #3695 --- .../data/jpa/repository/query/QueryUtils.java | 18 +++++++++++++----- .../query/QueryUtilsIntegrationTests.java | 11 +++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) 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 71919e5ffa..e51d305e0b 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 @@ -29,6 +29,7 @@ import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Path; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Attribute.PersistentAttributeType; @@ -727,18 +728,25 @@ private static jakarta.persistence.criteria.Order toJpaOrder(Order order, From expression = toExpressionRecursively(from, property); - if (order.getNullHandling() != Sort.NullHandling.NATIVE) { - throw new UnsupportedOperationException("Applying Null Precedence using Criteria Queries is not yet supported."); - } + Nulls nulls = toNulls(order.getNullHandling()); if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) { Expression upper = cb.lower((Expression) expression); - return order.isAscending() ? cb.asc(upper) : cb.desc(upper); + return order.isAscending() ? cb.asc(upper, nulls) : cb.desc(upper, nulls); } else { - return order.isAscending() ? cb.asc(expression) : cb.desc(expression); + return order.isAscending() ? cb.asc(expression, nulls) : cb.desc(expression, nulls); } } + private static Nulls toNulls(Sort.NullHandling nullHandling) { + + return switch (nullHandling) { + case NULLS_LAST -> Nulls.LAST; + case NULLS_FIRST -> Nulls.FIRST; + case NATIVE -> Nulls.NONE; + }; + } + static Expression toExpressionRecursively(From from, PropertyPath property) { return toExpressionRecursively(from, property, false); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java index 8f86882049..a7aecc36a7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java @@ -34,6 +34,7 @@ import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Root; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceProviderResolver; @@ -353,8 +354,8 @@ void toOrdersCanSortByJoinColumn() { assertThat(orders).hasSize(1); } - @Test // GH-3529 - void nullPrecedenceThroughCriteriaApiNotYetSupported() { + @Test // GH-3529, GH-3587 + void queryUtilsConsidersNullPrecedence() { CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(User.class); @@ -363,8 +364,10 @@ void nullPrecedenceThroughCriteriaApiNotYetSupported() { Sort sort = Sort.by(Sort.Order.desc("manager").nullsFirst()); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> QueryUtils.toOrders(sort, join, builder)); + List orders = QueryUtils.toOrders(sort, join, builder); + for (jakarta.persistence.criteria.Order order : orders) { + assertThat(order.getNullPrecedence()).isEqualTo(Nulls.FIRST); + } } /** From 4b30241ea313685988ff934af7eef7fb783cba34 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Fri, 1 Sep 2023 13:48:34 -0500 Subject: [PATCH 13/94] Add support for JPA 3.2 additions to JPQL. See: #3136 Original Pull Request: #3695 --- .../data/jpa/repository/query/Jpql.g4 | 12 ++- .../repository/query/JpqlQueryRenderer.java | 47 ++++++++++ .../query/HqlQueryRendererTests.java | 90 ++++++++++++------- .../query/JpqlQueryRendererTests.java | 53 +++++++++++ 4 files changed, 171 insertions(+), 31 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 index 98a0df214f..e014375adc 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 @@ -43,7 +43,13 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator_with_select_statement)* + ; + +setOperator_with_select_statement + : INTERSECT select_statement + | UNION select_statement + | EXCEPT select_statement ; update_statement @@ -439,6 +445,7 @@ string_expression | aggregate_expression | case_expression | function_invocation + | string_expression op='||' string_expression | '(' subquery ')' ; @@ -887,6 +894,7 @@ ELSE : E L S E; EMPTY : E M P T Y; ENTRY : E N T R Y; ESCAPE : E S C A P E; +EXCEPT : E X C E P T; EXISTS : E X I S T S; EXP : E X P; EXTRACT : E X T R A C T; @@ -901,6 +909,7 @@ HAVING : H A V I N G; IN : I N; INDEX : I N D E X; INNER : I N N E R; +INTERSECT : I N T E R S E C T; IS : I S; JOIN : J O I N; KEY : K E Y; @@ -945,6 +954,7 @@ TREAT : T R E A T; TRIM : T R I M; TRUE : T R U E; TYPE : T Y P E; +UNION : U N I O N; UPDATE : U P D A T E; UPPER : U P P E R; VALUE : V A L U E; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index fad4187df7..04512d611e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -79,6 +79,29 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext builder.appendExpression(visit(ctx.orderby_clause())); } + ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> { + tokens.addAll(visit(setOperatorWithSelectStatementContext)); + }); + + return tokens; + } + + @Override + public List visitSetOperator_with_select_statement( + JpqlParser.SetOperator_with_select_statementContext ctx) { + + List tokens = new ArrayList<>(); + + if (ctx.INTERSECT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.INTERSECT())); + } else if (ctx.UNION() != null) { + tokens.add(new JpaQueryParsingToken(ctx.UNION())); + } else if (ctx.EXCEPT() != null) { + tokens.add(new JpaQueryParsingToken(ctx.EXCEPT())); + } + + tokens.addAll(visit(ctx.select_statement())); + return builder; } @@ -799,6 +822,25 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { if (ctx.nullsPrecedence() != null) { builder.append(visit(ctx.nullsPrecedence())); } + if (ctx.nullsPrecedence() != null) { + tokens.addAll(visit(ctx.nullsPrecedence())); + } + + return tokens; + } + + @Override + public List visitNullsPrecedence(JpqlParser.NullsPrecedenceContext ctx) { + + List tokens = new ArrayList<>(); + + tokens.add(new JpaQueryParsingToken(ctx.NULLS())); + + if (ctx.FIRST() != null) { + tokens.add(new JpaQueryParsingToken(ctx.FIRST())); + } else if (ctx.LAST() != null) { + tokens.add(new JpaQueryParsingToken(ctx.LAST())); + } return builder; } @@ -1451,6 +1493,11 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(visit(ctx.case_expression())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); + } else if (ctx.op != null) { + + tokens.addAll(visit(ctx.string_expression(0))); + tokens.add(new JpaQueryParsingToken(ctx.op)); + tokens.addAll(visit(ctx.string_expression(1))); } else if (ctx.subquery() != null) { builder.append(TOKEN_OPEN_PAREN); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index cdb81af4d4..09c77e7a2d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1639,42 +1639,36 @@ void hqlQueries() { @Test // GH-2962 void orderByWithNullsFirstOrLastShouldWork() { - assertThatNoException().isThrownBy(() -> { - parseWithoutChanges(""" - select a, - case - when a.geaendertAm is null then a.erstelltAm - else a.geaendertAm end as mutationAm - from Element a - where a.erstelltDurch = :variable - order by mutationAm desc nulls first - """); - }); - - assertThatNoException().isThrownBy(() -> { - parseWithoutChanges(""" - select a, - case - when a.geaendertAm is null then a.erstelltAm - else a.geaendertAm end as mutationAm - from Element a - where a.erstelltDurch = :variable - order by mutationAm desc nulls last + assertQuery(""" + select a, + case + when a.geaendertAm is null then a.erstelltAm + else a.geaendertAm end as mutationAm + from Element a + where a.erstelltDurch = :variable + order by mutationAm desc nulls first + """); + + assertQuery(""" + select a, + case + when a.geaendertAm is null then a.erstelltAm + else a.geaendertAm end as mutationAm + from Element a + where a.erstelltDurch = :variable + order by mutationAm desc nulls last """); - }); } @Test // GH-2964 void roundFunctionShouldWorkLikeAnyOtherFunction() { - assertThatNoException().isThrownBy(() -> { - parseWithoutChanges(""" - select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc - from StockOrderItem oi - right join StockReceiptItem ri - on ri.article = oi.article - """); - }); + assertQuery(""" + select round(count(ri)*100/max(ri.receipt.positions), 0) as perc + from StockOrderItem oi + right join StockReceiptItem ri + on ri.article = oi.article + """); } @Test // GH-3711 @@ -1854,6 +1848,42 @@ void powerShouldBeLegalInAQuery() { assertQuery("select e.power.id from MyEntity e"); } + @Test // GH-3136 + void doublePipeShouldBeValidAsAStringConcatOperator() { + + assertQuery(""" + select e.name || ' ' || e.title + from Employee e + """); + } + + @Test // GH-3136 + void additionalStringOperationsShouldWork() { + + assertQuery(""" + select + replace(e.name, 'Baggins', 'Proudfeet'), + left(e.role, 4), + right(e.home, 5), + cast(e.distance_from_home, int) + from Employee e + """); + } + + @Test // GH-3136 + void combinedSelectStatementsShouldWork() { + + assertQuery(""" + select e from Employee e where e.last_name = 'Baggins' + intersect + select e from Employee e where e.first_name = 'Samwise' + union + select e from Employee e where e.home = 'The Shire' + except + select e from Employee e where e.home = 'Isengard' + """); + } + @Test // GH-3219 void extractFunctionShouldSupportAdditionalExtensions() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java index a16a5a8802..4681161836 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java @@ -1017,6 +1017,59 @@ void powerShouldBeLegalInAQuery() { assertQuery("select e.power.id from MyEntity e"); } + @Test // GH-3136 + void doublePipeShouldBeValidAsAStringConcatOperator() { + + assertQuery(""" + select e.name || ' ' || e.title + from Employee e + """); + } + + @Test // GH-3136 + void combinedSelectStatementsShouldWork() { + + assertQuery(""" + select e from Employee e where e.last_name = 'Baggins' + intersect + select e from Employee e where e.first_name = 'Samwise' + union + select e from Employee e where e.home = 'The Shire' + except + select e from Employee e where e.home = 'Isengard' + """); + } + + @Disabled + @Test // GH-3136 + void additionalStringOperationsShouldWork() { + + assertQuery(""" + select + replace(e.name, 'Baggins', 'Proudfeet'), + left(e.role, 4), + right(e.home, 5), + cast(e.distance_from_home, int) + from Employee e + """); + } + + @Test // GH-3136 + void orderByWithNullsFirstOrLastShouldWork() { + + assertQuery(""" + select a + from Element a + order by mutationAm desc nulls first + """); + + assertQuery(""" + select a + from Element a + order by mutationAm desc nulls last + """); + } + @ParameterizedTest // GH-3342 @ValueSource(strings = { "select 1 as value from User u", "select -1 as value from User u", "select +1 as value from User u", "select +1 * -100 as value from User u", From ec75aae3166496505d19ad42be78d33f44d9cbad Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 21 Jun 2024 18:14:07 +0200 Subject: [PATCH 14/94] Add support for JPA 3.2 additions to EQL. See: #3136 Original Pull Request: #3695 --- .../data/jpa/repository/query/Eql.g4 | 28 ++++- .../data/jpa/repository/query/Jpql.g4 | 50 ++++++++- .../repository/query/EqlQueryRenderer.java | 52 ++++++++- .../query/JpqlCountQueryTransformer.java | 12 +- .../repository/query/JpqlQueryRenderer.java | 103 ++++++++++++++++-- .../query/JpqlSortedQueryTransformer.java | 12 +- .../repository/query/EqlComplianceTests.java | 51 +++++++++ .../query/HqlQueryRendererTests.java | 12 ++ .../repository/query/JpqlComplianceTests.java | 52 +++++++++ 9 files changed, 352 insertions(+), 20 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index 9a1136ddd1..0b3f6ecc72 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 @@ -309,6 +309,7 @@ scalar_expression | datetime_expression | boolean_expression | case_expression + | cast_function | entity_type_expression ; @@ -455,6 +456,7 @@ string_expression | case_expression | function_invocation | '(' subquery ')' + | string_expression '||' string_expression ; datetime_expression @@ -539,6 +541,9 @@ functions_returning_strings | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' | LOWER '(' string_expression ')' | UPPER '(' string_expression ')' + | REPLACE '(' string_expression ',' string_expression ',' string_expression ')' + | LEFT '(' string_expression ',' arithmetic_expression ')' + | RIGHT '(' string_expression ',' arithmetic_expression ')' ; trim_specification @@ -548,7 +553,7 @@ trim_specification ; cast_function - : CAST '(' single_valued_path_expression identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' + : CAST '(' single_valued_path_expression (identification_variable)? identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' ; function_invocation @@ -614,6 +619,14 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; +type_literal + : STRING + | INTEGER + | LONG + | FLOAT + | DOUBLE + ; + /******************* Gaps in the spec. *******************/ @@ -626,6 +639,7 @@ trim_character identification_variable : IDENTIFICATION_VARIABLE | f=(COUNT + | AS | DATE | FROM | INNER @@ -635,11 +649,13 @@ identification_variable | ORDER | OUTER | POWER + | RIGHT | FLOOR | SIGN | TIME | TYPE | VALUE) + | type_literal ; constructor_name @@ -820,6 +836,8 @@ reserved_word |OR |ORDER |OUTER + |REPLACE + |RIGHT |POWER |ROUND |SELECT @@ -903,6 +921,7 @@ DATETIME : D A T E T I M E ; DELETE : D E L E T E; DESC : D E S C; DISTINCT : D I S T I N C T; +DOUBLE : D O U B L E; END : E N D; ELSE : E L S E; EMPTY : E M P T Y; @@ -915,6 +934,7 @@ EXTRACT : E X T R A C T; FALSE : F A L S E; FETCH : F E T C H; FIRST : F I R S T; +FLOAT : F L O A T; FLOOR : F L O O R; FROM : F R O M; FUNCTION : F U N C T I O N; @@ -923,6 +943,7 @@ HAVING : H A V I N G; IN : I N; INDEX : I N D E X; INNER : I N N E R; +INTEGER : I N T E G E R; INTERSECT : I N T E R S E C T; IS : I S; JOIN : J O I N; @@ -935,6 +956,7 @@ LIKE : L I K E; LN : L N; LOCAL : L O C A L; LOCATE : L O C A T E; +LONG : L O N G; LOWER : L O W E R; MAX : M A X; MEMBER : M E M B E R; @@ -953,6 +975,8 @@ ORDER : O R D E R; OUTER : O U T E R; POWER : P O W E R; REGEXP : R E G E X P; +REPLACE : R E P L A C E; +RIGHT : R I G H T; ROUND : R O U N D; SELECT : S E L E C T; SET : S E T; @@ -960,6 +984,7 @@ SIGN : S I G N; SIZE : S I Z E; SOME : S O M E; SQRT : S Q R T; +STRING : S T R I N G; SUBSTRING : S U B S T R I N G; SUM : S U M; THEN : T H E N; @@ -979,7 +1004,6 @@ WHERE : W H E R E; EQUAL : '=' ; NOT_EQUAL : '<>' | '!=' ; - CHARACTER : '\'' (~ ('\'' | '\\')) '\'' ; IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ; STRINGLITERAL : '\'' (~ ('\'' | '\\')|'\\')* '\'' ; diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 index e014375adc..db828fe124 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 @@ -43,13 +43,25 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator_with_select_statement)* + : select_query ; -setOperator_with_select_statement - : INTERSECT select_statement - | UNION select_statement - | EXCEPT select_statement +select_query + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? + ; + +setOperator + : UNION ALL? + | INTERSECT ALL? + | EXCEPT ALL? + ; + +set_fuction + : setOperator set_function_select + ; + +set_function_select + : select_query ; update_statement @@ -303,6 +315,7 @@ scalar_expression | datetime_expression | boolean_expression | case_expression + | cast_expression | entity_type_expression ; @@ -447,6 +460,7 @@ string_expression | function_invocation | string_expression op='||' string_expression | '(' subquery ')' + | string_expression '||' string_expression ; datetime_expression @@ -530,7 +544,10 @@ functions_returning_strings | SUBSTRING '(' string_expression ',' arithmetic_expression (',' arithmetic_expression)? ')' | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' | LOWER '(' string_expression ')' + | REPLACE '(' string_expression ',' string_expression ',' string_expression ')' | UPPER '(' string_expression ')' + | LEFT '(' string_expression ',' arithmetic_expression ')' + | RIGHT '(' string_expression ',' arithmetic_expression ')' ; trim_specification @@ -603,6 +620,10 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; +cast_expression + : CAST '(' string_expression AS type_literal ')' + ; + /******************* Gaps in the spec. *******************/ @@ -624,6 +645,7 @@ identification_variable | ORDER | OUTER | POWER + | RIGHT | FLOOR | SIGN | TIME @@ -673,6 +695,14 @@ numeric_literal | LONGLITERAL ; +type_literal + : STRING + | INTEGER + | LONG + | FLOAT + | DOUBLE + ; + boolean_literal : TRUE | FALSE @@ -808,6 +838,8 @@ reserved_word |ORDER |OUTER |POWER + |REPLACE + |RIGHT |ROUND |SELECT |SET @@ -877,6 +909,7 @@ BETWEEN : B E T W E E N; BOTH : B O T H; BY : B Y; CASE : C A S E; +CAST : C A S T; CEILING : C E I L I N G; COALESCE : C O A L E S C E; CONCAT : C O N C A T; @@ -889,6 +922,7 @@ DATETIME : D A T E T I M E ; DELETE : D E L E T E; DESC : D E S C; DISTINCT : D I S T I N C T; +DOUBLE : D O U B L E; END : E N D; ELSE : E L S E; EMPTY : E M P T Y; @@ -901,6 +935,7 @@ EXTRACT : E X T R A C T; FALSE : F A L S E; FETCH : F E T C H; FIRST : F I R S T; +FLOAT : F L O A T; FLOOR : F L O O R; FROM : F R O M; FUNCTION : F U N C T I O N; @@ -909,6 +944,7 @@ HAVING : H A V I N G; IN : I N; INDEX : I N D E X; INNER : I N N E R; +INTEGER : I N T E G E R; INTERSECT : I N T E R S E C T; IS : I S; JOIN : J O I N; @@ -921,6 +957,7 @@ LIKE : L I K E; LN : L N; LOCAL : L O C A L; LOCATE : L O C A T E; +LONG : L O N G; LOWER : L O W E R; MAX : M A X; MEMBER : M E M B E R; @@ -937,6 +974,8 @@ ON : O N; OR : O R; ORDER : O R D E R; OUTER : O U T E R; +REPLACE : R E P L A C E; +RIGHT : R I G H T; POWER : P O W E R; ROUND : R O U N D; SELECT : S E L E C T; @@ -945,6 +984,7 @@ SIGN : S I G N; SIZE : S I Z E; SOME : S O M E; SQRT : S Q R T; +STRING : S T R I N G; SUBSTRING : S U B S T R I N G; SUM : S U M; THEN : T H E N; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 8225545c83..f93e59f6a8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -23,6 +23,7 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. @@ -1008,6 +1009,8 @@ public QueryTokenStream visitScalar_expression(EqlParser.Scalar_expressionContex builder.append(visit(ctx.case_expression())); } else if (ctx.entity_type_expression() != null) { builder.append(visit(ctx.entity_type_expression())); + } else if (ctx.cast_function() != null) { + return (visit(ctx.cast_function())); } return builder; @@ -1603,6 +1606,11 @@ public QueryTokenStream visitString_expression(EqlParser.String_expressionContex builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.subquery())); builder.append(TOKEN_CLOSE_PAREN); + } else if (!ObjectUtils.isEmpty(ctx.string_expression())) { + + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_DOUBLE_PIPE); + builder.appendExpression(visit(ctx.string_expression(1))); } return builder; @@ -1934,6 +1942,32 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.LEFT() != null) { + + builder.append(QueryTokens.token(ctx.LEFT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.RIGHT() != null) { + + builder.append(QueryTokens.token(ctx.RIGHT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.REPLACE() != null) { + + builder.append(QueryTokens.token(ctx.REPLACE())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(1))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(2))); + builder.append(TOKEN_CLOSE_PAREN); } return builder; @@ -1960,9 +1994,9 @@ public QueryTokenStream visitCast_function(EqlParser.Cast_functionContext ctx) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.single_valued_path_expression())); builder.append(TOKEN_SPACE); - builder.appendInline(visit(ctx.identification_variable())); + builder.appendInline(QueryTokenStream.concat(ctx.identification_variable(), this::visit, TOKEN_SPACE)); - if (ctx.numeric_literal() != null) { + if (!ObjectUtils.isEmpty(ctx.numeric_literal())) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); @@ -2063,6 +2097,14 @@ public QueryTokenStream visitCase_expression(EqlParser.Case_expressionContext ct } } + @Override + public QueryRendererBuilder visitType_literal(EqlParser.Type_literalContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + ctx.children.forEach(it -> builder.append(QueryTokens.expression(it.getText()))); + return builder; + } + @Override public QueryTokenStream visitGeneral_case_expression(EqlParser.General_case_expressionContext ctx) { @@ -2183,9 +2225,11 @@ public QueryTokenStream visitIdentification_variable(EqlParser.Identification_va return QueryRendererBuilder.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); } else if (ctx.f != null) { return QueryRendererBuilder.from(QueryTokens.expression(ctx.f)); - } else { - return QueryRenderer.builder(); + } else if (ctx.type_literal() != null) { + return visit(ctx.type_literal()); } + + return QueryRenderer.builder(); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index 89e4d54070..fe8b2e0bdd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -42,7 +42,17 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer { } @Override - public QueryRenderer.QueryRendererBuilder visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + + if(ctx.select_query() != null) { + return visitSelect_query(ctx.select_query()); + } + + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index 04512d611e..aeec4497d7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -15,16 +15,33 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.*; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COLON; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOT; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_EQUALS; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_QUESTION_MARK; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOUBLE_PIPE; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_SPACE; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; import java.util.ArrayList; import java.util.List; import org.antlr.v4.runtime.tree.ParseTree; +import org.springframework.data.jpa.repository.query.JpqlParser.Except_clauseContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Intersect_clauseContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Relation_fuctions_selectContext; import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Cast_expressionContext; import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Set_fuctionContext; +import org.springframework.data.jpa.repository.query.JpqlParser.Type_literalContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. @@ -55,8 +72,17 @@ public QueryTokenStream visitQl_statement(JpqlParser.Ql_statementContext ctx) { } } - @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + @Override + public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + + if(ctx.select_query() != null) { + return visitSelect_query(ctx.select_query()); + } + + return QueryTokenStream.empty(); + } + + public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -79,11 +105,11 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext builder.appendExpression(visit(ctx.orderby_clause())); } - ctx.setOperator_with_select_statement().forEach(setOperatorWithSelectStatementContext -> { - tokens.addAll(visit(setOperatorWithSelectStatementContext)); - }); + if(ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } - return tokens; + return builder; } @Override @@ -799,6 +825,19 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx return builder; } + @Override + public QueryTokenStream visitSet_fuction(Set_fuctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.setOperator().getStart())); + if(ctx.setOperator().ALL() != null) { + builder.append(QueryTokens.expression(ctx.setOperator().ALL())); + } + builder.appendExpression(visit(ctx.set_function_select().select_query())); + return builder; + } + @Override public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { @@ -973,6 +1012,8 @@ public QueryTokenStream visitScalar_expression(JpqlParser.Scalar_expressionConte return visit(ctx.case_expression()); } else if (ctx.entity_type_expression() != null) { return visit(ctx.entity_type_expression()); + } else if (ctx.cast_expression() != null) { + return (visit(ctx.cast_expression())); } return QueryTokenStream.empty(); @@ -1503,6 +1544,11 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.subquery())); builder.append(TOKEN_CLOSE_PAREN); + } else if (!ObjectUtils.isEmpty(ctx.string_expression())) { + + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_DOUBLE_PIPE); + builder.appendExpression(visit(ctx.string_expression(1))); } return builder; @@ -1823,6 +1869,29 @@ public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_re builder.append(TOKEN_OPEN_PAREN); builder.append(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.LEFT() != null) { + builder.append(QueryTokens.token(ctx.LEFT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.RIGHT() != null) { + builder.append(QueryTokens.token(ctx.RIGHT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.REPLACE() != null) { + builder.append(QueryTokens.token(ctx.REPLACE())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(1))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.string_expression(2))); + builder.append(TOKEN_CLOSE_PAREN); } return builder; @@ -1925,6 +1994,26 @@ public QueryTokenStream visitCase_expression(JpqlParser.Case_expressionContext c } } + @Override + public QueryRendererBuilder visitCast_expression(Cast_expressionContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.token(ctx.CAST())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.string_expression())); + builder.append(QueryTokens.expression(ctx.AS())); + builder.appendInline(visit(ctx.type_literal())); + builder.append(TOKEN_CLOSE_PAREN); + return builder; + } + + @Override + public QueryRendererBuilder visitType_literal(Type_literalContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + ctx.children.forEach(it -> builder.append(QueryTokens.expression(it.getText()))); + return builder; + } + @Override public QueryTokenStream visitGeneral_case_expression(JpqlParser.General_case_expressionContext ctx) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index 41d0661d2c..2a63b7250d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -54,6 +54,16 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { @Override public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + if(ctx.select_query() != null) { + return visitSelect_query(ctx.select_query()); + } + + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(visit(ctx.select_clause())); @@ -96,7 +106,7 @@ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) return builder.append(dtoDelegate.transformSelectionList(tokenStream)); } - private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_queryContext ctx) { if (ctx.orderby_clause() != null) { QueryTokenStream existingOrder = visit(ctx.orderby_clause()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java index 2ec5f229a1..de9a81944a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; @@ -412,4 +414,53 @@ void isNullAndIsNotNull() { assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT null OR e.active = true)"); assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)"); } + + + @Test // GH-3496 + void lateralShouldBeAValidParameter() { + + assertQuery("select e from Employee e where e.lateral = :_lateral"); + assertQuery("select te from TestEntity te where te.lateral = :lateral"); + } + + @Test // GH-3136 + void intersect() { + + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + } + + @Test // GH-3136 + void except() { + + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = {"STRING", "INTEGER", "FLOAT", "DOUBLE"}) + void jpqlCast(String targetType) { + assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = {"LEFT", "RIGHT"}) + void leftRightStringFunctions(String keyword) { + assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); + } + + @Test // GH-3136 + void replaceStringFunctions() { + assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e"); + assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); + } + + @Test // GH-3136 + void stringConcatWithPipes() { + assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 09c77e7a2d..7c158b3242 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1954,6 +1954,18 @@ void entityNameWithPackageContainingReservedWord(String reservedWord) { assertQuery(source); } + @ParameterizedTest // GH-3136 + @ValueSource(strings = {"LEFT", "RIGHT"}) + void leftRightStringFunctions(String keyword) { + assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); + } + + @Test // GH-3136 + void replaceStringFunctions() { + assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e"); + assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); + } + @Test void reservedWordsShouldWork() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index 81722f9b90..f7bc8f76c9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Test to verify compliance of {@link JpqlParser} with standard SQL. Other than {@link JpqlSpecificationTests} tests in @@ -63,4 +65,54 @@ void newWithStrings() { assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); } + @Test // GH-3136 + void union() { + + assertQuery(""" + SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1 + UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 + """); + } + + @Test // GH-3136 + void intersect() { + + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + } + + @Test // GH-3136 + void except() { + + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = {"STRING", "INTEGER", "FLOAT", "DOUBLE"}) + void cast(String targetType) { + assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = {"LEFT", "RIGHT"}) + void leftRightStringFunctions(String keyword) { + assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); + } + + @Test // GH-3136 + void replaceStringFunctions() { + assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e"); + assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); + } + + @Test // GH-3136 + void stringConcatWithPipes() { + assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); + } + } From 8d8d901e7c77396e99342146d99334bde03b1385 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 28 Jun 2024 11:46:42 +0200 Subject: [PATCH 15/94] Make sure sorting is rendered correctly for JPQL query using set operator. Original Pull Request: #3695 --- .../repository/query/JpqlCountQueryTransformer.java | 3 +++ .../repository/query/JpqlSortedQueryTransformer.java | 6 +++++- .../data/jpa/repository/query/EqlComplianceTests.java | 11 +++++------ .../repository/query/JpqlQueryTransformerTests.java | 9 +++++++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index fe8b2e0bdd..923c2d48c6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -68,6 +68,9 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { if (ctx.having_clause() != null) { builder.appendExpression(visit(ctx.having_clause())); } + if(ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } return builder; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index 2a63b7250d..a3e9fddbfd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -81,7 +81,11 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { builder.appendExpression(visit(ctx.having_clause())); } - doVisitOrderBy(builder, ctx); + if(ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx); + } return builder; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java index de9a81944a..c0819dc928 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -26,9 +26,9 @@ /** * Tests built around examples of EQL found in the EclipseLink's docs at * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL
- * With the exception of {@literal MOD} which is defined as {@literal MOD(arithmetic_expression , arithmetic_expression)}, - * but shown in tests as {@literal MOD(arithmetic_expression ? arithmetic_expression)}. - *
+ * With the exception of {@literal MOD} which is defined as + * {@literal MOD(arithmetic_expression , arithmetic_expression)}, but shown in tests as + * {@literal MOD(arithmetic_expression ? arithmetic_expression)}.
* IMPORTANT: Purely verifies the parser without any transformations. * * @author Greg Turnquist @@ -415,7 +415,6 @@ void isNullAndIsNotNull() { assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)"); } - @Test // GH-3496 void lateralShouldBeAValidParameter() { @@ -442,13 +441,13 @@ void except() { } @ParameterizedTest // GH-3136 - @ValueSource(strings = {"STRING", "INTEGER", "FLOAT", "DOUBLE"}) + @ValueSource(strings = { "STRING", "INTEGER", "FLOAT", "DOUBLE" }) void jpqlCast(String targetType) { assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); } @ParameterizedTest // GH-3136 - @ValueSource(strings = {"LEFT", "RIGHT"}) + @ValueSource(strings = { "LEFT", "RIGHT" }) void leftRightStringFunctions(String keyword) { assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index 147477fc2f..660f3c9a7d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -784,6 +784,15 @@ void sortingRecognizesJoinAliases() { """); } + @Test // GH-3427 + void sortShouldBeAppendedToFullSelectOnlyInCaseOfSetOperator() { + + String source = "SELECT tb FROM Test tb WHERE (tb.type='A') UNION SELECT tb FROM Test tb WHERE (tb.type='B')"; + String target = createQueryFor(source, Sort.by("Type").ascending()); + + assertThat(target).isEqualTo("SELECT tb FROM Test tb WHERE (tb.type = 'A') UNION SELECT tb FROM Test tb WHERE (tb.type = 'B') order by tb.Type asc"); + } + static Stream queriesWithReservedWordsAsIdentifiers() { return Stream.of( // From e7fde065009d5f5db9e77e90c30946daae9edb13 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Nov 2024 08:25:32 +0100 Subject: [PATCH 16/94] Polishing. Inline select_query into select_statement, simplify set_function resolution. Align JPQL and EQL grammars. Adopt Hibernate version guards in tests. Original Pull Request: #3695 --- .../data/jpa/repository/query/Eql.g4 | 10 +- .../data/jpa/repository/query/Jpql.g4 | 39 +- .../query/EqlCountQueryTransformer.java | 4 +- .../repository/query/EqlQueryRenderer.java | 448 +++++++++--------- .../query/EqlSortedQueryTransformer.java | 15 +- .../query/JpqlCountQueryTransformer.java | 12 +- .../repository/query/JpqlQueryRenderer.java | 268 ++++++----- .../query/JpqlSortedQueryTransformer.java | 13 +- ...stgresStoredProcedureIntegrationTests.java | 3 +- .../repository/query/EqlComplianceTests.java | 14 +- .../query/EqlQueryRendererTests.java | 54 +++ .../query/EqlSpecificationTests.java | 52 +- .../query/HqlQueryRendererTests.java | 4 +- .../repository/query/JpqlComplianceTests.java | 207 +++++++- .../query/JpqlSpecificationTests.java | 32 ++ 15 files changed, 745 insertions(+), 430 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index 0b3f6ecc72..ff6be74296 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 @@ -43,7 +43,7 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (setOperator select_statement)* + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? ; setOperator @@ -52,6 +52,10 @@ setOperator | EXCEPT ALL? ; +set_fuction + : setOperator select_statement + ; + update_statement : update_clause (where_clause)? ; @@ -664,6 +668,7 @@ constructor_name literal : STRINGLITERAL + | JAVASTRINGLITERAL | INTLITERAL | FLOATLITERAL | LONGLITERAL @@ -836,9 +841,9 @@ reserved_word |OR |ORDER |OUTER + |POWER |REPLACE |RIGHT - |POWER |ROUND |SELECT |SET @@ -1006,6 +1011,7 @@ NOT_EQUAL : '<>' | '!=' ; CHARACTER : '\'' (~ ('\'' | '\\')) '\'' ; IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ; +JAVASTRINGLITERAL : '"' ( ('\\' [btnfr"']) | ~('"'))* '"'; STRINGLITERAL : '\'' (~ ('\'' | '\\')|'\\')* '\'' ; FLOATLITERAL : ('0' .. '9')* '.' ('0' .. '9')+ (E ('0' .. '9')+)* (F|D)?; INTLITERAL : ('0' .. '9')+ ; diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 index db828fe124..d477ea2467 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 @@ -43,10 +43,6 @@ ql_statement ; select_statement - : select_query - ; - -select_query : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? ; @@ -57,11 +53,7 @@ setOperator ; set_fuction - : setOperator set_function_select - ; - -set_function_select - : select_query + : setOperator select_statement ; update_statement @@ -95,7 +87,7 @@ join ; fetch_join - : join_spec FETCH join_association_path_expression + : join_spec FETCH join_association_path_expression AS? identification_variable? join_condition? ; join_spec @@ -315,7 +307,7 @@ scalar_expression | datetime_expression | boolean_expression | case_expression - | cast_expression + | cast_function | entity_type_expression ; @@ -446,6 +438,7 @@ arithmetic_primary | functions_returning_numerics | aggregate_expression | case_expression + | cast_function | function_invocation | '(' subquery ')' ; @@ -458,7 +451,6 @@ string_expression | aggregate_expression | case_expression | function_invocation - | string_expression op='||' string_expression | '(' subquery ')' | string_expression '||' string_expression ; @@ -544,8 +536,8 @@ functions_returning_strings | SUBSTRING '(' string_expression ',' arithmetic_expression (',' arithmetic_expression)? ')' | TRIM '(' ((trim_specification)? (trim_character)? FROM)? string_expression ')' | LOWER '(' string_expression ')' - | REPLACE '(' string_expression ',' string_expression ',' string_expression ')' | UPPER '(' string_expression ')' + | REPLACE '(' string_expression ',' string_expression ',' string_expression ')' | LEFT '(' string_expression ',' arithmetic_expression ')' | RIGHT '(' string_expression ',' arithmetic_expression ')' ; @@ -556,6 +548,9 @@ trim_specification | BOTH ; +cast_function + : CAST '(' single_valued_path_expression (identification_variable)? identification_variable ('(' numeric_literal (',' numeric_literal)* ')')? ')' + ; function_invocation : FUNCTION '(' function_name (',' function_arg)* ')' @@ -620,9 +615,6 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; -cast_expression - : CAST '(' string_expression AS type_literal ')' - ; /******************* Gaps in the spec. @@ -636,6 +628,7 @@ trim_character identification_variable : IDENTIFICATION_VARIABLE | f=(COUNT + | AS | DATE | FROM | INNER @@ -651,6 +644,7 @@ identification_variable | TIME | TYPE | VALUE) + | type_literal ; constructor_name @@ -678,6 +672,9 @@ pattern_value date_time_timestamp_literal : STRINGLITERAL + | DATELITERAL + | TIMELITERAL + | TIMESTAMPLITERAL ; entity_type_literal @@ -974,9 +971,10 @@ ON : O N; OR : O R; ORDER : O R D E R; OUTER : O U T E R; +POWER : P O W E R; +REGEXP : R E G E X P; REPLACE : R E P L A C E; RIGHT : R I G H T; -POWER : P O W E R; ROUND : R O U N D; SELECT : S E L E C T; SET : S E T; @@ -1006,8 +1004,11 @@ NOT_EQUAL : '<>' | '!=' ; CHARACTER : '\'' (~ ('\'' | '\\')) '\'' ; IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ; -STRINGLITERAL : '\'' (~ ('\'' | '\\'))* '\'' ; +STRINGLITERAL : '\'' (~ ('\'' | '\\')|'\\')* '\'' ; JAVASTRINGLITERAL : '"' ( ('\\' [btnfr"']) | ~('"'))* '"'; FLOATLITERAL : ('0' .. '9')* '.' ('0' .. '9')+ (E ('0' .. '9')+)* (F|D)?; INTLITERAL : ('0' .. '9')+ ; -LONGLITERAL : ('0' .. '9')+L ; +LONGLITERAL : ('0' .. '9')+ L; +DATELITERAL : '{' D STRINGLITERAL '}'; +TIMELITERAL : '{' T STRINGLITERAL '}'; +TIMESTAMPLITERAL : '{' T S STRINGLITERAL '}'; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java index 0221aff83a..81b5e9a8f6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java @@ -42,7 +42,7 @@ class EqlCountQueryTransformer extends EqlQueryRenderer { } @Override - public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -92,7 +92,7 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { return builder; } - private QueryRendererBuilder getDistinctCountSelection(QueryTokenStream selectionListbuilder) { + private QueryTokenStream getDistinctCountSelection(QueryTokenStream selectionListbuilder) { QueryRendererBuilder nested = new QueryRendererBuilder(); CountSelectionTokenStream countSelection = CountSelectionTokenStream.create(selectionListbuilder); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index f93e59f6a8..e7e768df62 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -78,30 +78,8 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext builder.appendExpression(visit(ctx.orderby_clause())); } - for (int i = 0; i < ctx.setOperator().size(); i++) { - - builder.appendExpression(visit(ctx.setOperator(i))); - builder.appendExpression(visit(ctx.select_statement(i))); - } - - return builder; - } - - @Override - public QueryTokenStream visitSetOperator(EqlParser.SetOperatorContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.UNION() != null) { - builder.append(QueryTokens.expression(ctx.UNION())); - } else if (ctx.INTERSECT() != null) { - builder.append(QueryTokens.expression(ctx.INTERSECT())); - } else if (ctx.EXCEPT() != null) { - builder.append(QueryTokens.expression(ctx.EXCEPT())); - } - - if (ctx.ALL() != null) { - builder.append(QueryTokens.expression(ctx.ALL())); + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); } return builder; @@ -227,9 +205,11 @@ public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } + if (ctx.identification_variable() != null) { builder.appendExpression(visit(ctx.identification_variable())); } + if (ctx.join_condition() != null) { builder.appendExpression(visit(ctx.join_condition())); } @@ -292,8 +272,7 @@ public QueryTokenStream visitJoin_condition(EqlParser.Join_conditionContext ctx) } @Override - public QueryTokenStream visitJoin_association_path_expression( - EqlParser.Join_association_path_expressionContext ctx) { + public QueryTokenStream visitJoin_association_path_expression(EqlParser.Join_association_path_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -305,31 +284,25 @@ public QueryTokenStream visitJoin_association_path_expression( builder.appendExpression(visit(ctx.join_single_valued_path_expression())); } } else { - if (ctx.join_collection_valued_path_expression() != null) { + QueryRendererBuilder nested = QueryRenderer.builder(); - QueryRendererBuilder nested = QueryRenderer.builder(); + if (ctx.join_collection_valued_path_expression() != null) { - nested.append(QueryTokens.token(ctx.TREAT())); - nested.append(TOKEN_OPEN_PAREN); - nested.appendInline(visit(ctx.join_collection_valued_path_expression())); + nested.appendExpression(visit(ctx.join_collection_valued_path_expression())); nested.append(QueryTokens.expression(ctx.AS())); - nested.appendInline(visit(ctx.subtype())); - nested.append(TOKEN_CLOSE_PAREN); + nested.appendExpression(visit(ctx.subtype())); - builder.appendExpression(nested); } else if (ctx.join_single_valued_path_expression() != null) { - QueryRendererBuilder nested = QueryRenderer.builder(); - - nested.append(QueryTokens.token(ctx.TREAT())); - nested.append(TOKEN_OPEN_PAREN); - nested.appendInline(visit(ctx.join_single_valued_path_expression())); + nested.appendExpression(visit(ctx.join_single_valued_path_expression())); nested.append(QueryTokens.expression(ctx.AS())); - nested.appendInline(visit(ctx.subtype())); - nested.append(TOKEN_CLOSE_PAREN); - - builder.appendExpression(nested); + nested.appendExpression(visit(ctx.subtype())); } + + builder.append(QueryTokens.token(ctx.TREAT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); } return builder; @@ -451,8 +424,7 @@ public QueryTokenStream visitSingle_valued_path_expression(EqlParser.Single_valu } @Override - public QueryTokenStream visitGeneral_identification_variable( - EqlParser.General_identification_variableContext ctx) { + public QueryTokenStream visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -495,12 +467,15 @@ public QueryTokenStream visitSimple_subpath(EqlParser.Simple_subpathContext ctx) public QueryTokenStream visitTreated_subpath(EqlParser.Treated_subpathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.general_subpath())); + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.subtype())); builder.append(QueryTokens.token(ctx.TREAT())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.general_subpath())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.subtype())); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -853,15 +828,15 @@ public QueryTokenStream visitOrderby_item(EqlParser.Orderby_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.state_field_path_expression() != null) { - builder.append(visit(ctx.state_field_path_expression())); + builder.appendExpression(visit(ctx.state_field_path_expression())); } else if (ctx.general_identification_variable() != null) { - builder.append(visit(ctx.general_identification_variable())); + builder.appendExpression(visit(ctx.general_identification_variable())); } else if (ctx.result_variable() != null) { - builder.append(visit(ctx.result_variable())); + builder.appendExpression(visit(ctx.result_variable())); } else if (ctx.string_expression() != null) { - builder.append(visit(ctx.string_expression())); + builder.appendExpression(visit(ctx.string_expression())); } else if (ctx.scalar_expression() != null) { - builder.append(visit(ctx.scalar_expression())); + builder.appendExpression(visit(ctx.scalar_expression())); } if (ctx.ASC() != null) { @@ -883,12 +858,44 @@ public QueryTokenStream visitNullsPrecedence(EqlParser.NullsPrecedenceContext ct QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_NULLS); + builder.append(QueryTokens.expression(ctx.NULLS())); if (ctx.FIRST() != null) { - builder.append(TOKEN_FIRST); + builder.append(QueryTokens.expression(ctx.FIRST())); } else if (ctx.LAST() != null) { - builder.append(TOKEN_LAST); + builder.append(QueryTokens.expression(ctx.LAST())); + } + + return builder; + } + + @Override + public QueryTokenStream visitSet_fuction(EqlParser.Set_fuctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.setOperator() != null) { + builder.append(visit(ctx.setOperator())); + } + + builder.appendExpression(visit(ctx.select_statement())); + + return builder; + } + + @Override + public QueryTokenStream visitSetOperator(EqlParser.SetOperatorContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.INTERSECT() != null) { + builder.append(QueryTokens.expression(ctx.INTERSECT())); + } else if (ctx.UNION() != null) { + builder.append(QueryTokens.expression(ctx.UNION())); + } else if (ctx.EXCEPT() != null) { + builder.append(QueryTokens.expression(ctx.EXCEPT())); + } else if (ctx.ALL() != null) { + builder.append(QueryTokens.expression(ctx.ALL())); } return builder; @@ -993,83 +1000,82 @@ public QueryTokenStream visitSimple_select_expression(EqlParser.Simple_select_ex @Override public QueryTokenStream visitScalar_expression(EqlParser.Scalar_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.arithmetic_expression() != null) { - builder.append(visit(ctx.arithmetic_expression())); + return visit(ctx.arithmetic_expression()); } else if (ctx.string_expression() != null) { - builder.append(visit(ctx.string_expression())); + return visit(ctx.string_expression()); } else if (ctx.enum_expression() != null) { - builder.append(visit(ctx.enum_expression())); + return visit(ctx.enum_expression()); } else if (ctx.datetime_expression() != null) { - builder.append(visit(ctx.datetime_expression())); + return visit(ctx.datetime_expression()); } else if (ctx.boolean_expression() != null) { - builder.append(visit(ctx.boolean_expression())); + return visit(ctx.boolean_expression()); } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); + return visit(ctx.case_expression()); } else if (ctx.entity_type_expression() != null) { - builder.append(visit(ctx.entity_type_expression())); + return visit(ctx.entity_type_expression()); } else if (ctx.cast_function() != null) { return (visit(ctx.cast_function())); } - return builder; + return QueryTokenStream.empty(); } @Override public QueryTokenStream visitConditional_expression(EqlParser.Conditional_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.conditional_expression() != null) { - builder.append(visit(ctx.conditional_expression())); + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.conditional_expression())); builder.append(QueryTokens.expression(ctx.OR())); - builder.append(visit(ctx.conditional_term())); + builder.appendExpression(visit(ctx.conditional_term())); + + return builder; } else { - builder.append(visit(ctx.conditional_term())); + return visit(ctx.conditional_term()); } - - return builder; } @Override public QueryTokenStream visitConditional_term(EqlParser.Conditional_termContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.conditional_term() != null) { - builder.append(visit(ctx.conditional_term())); + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.conditional_term())); builder.append(QueryTokens.expression(ctx.AND())); - builder.append(visit(ctx.conditional_factor())); + builder.appendExpression(visit(ctx.conditional_factor())); + + return builder; } else { - builder.append(visit(ctx.conditional_factor())); + return visit(ctx.conditional_factor()); } - - return builder; } @Override public QueryTokenStream visitConditional_factor(EqlParser.Conditional_factorContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.NOT() != null) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.NOT())); + builder.appendExpression(visit(ctx.conditional_primary())); + return builder; } - builder.append(visit(ctx.conditional_primary())); - - return builder; + return visit(ctx.conditional_primary()); } @Override public QueryTokenStream visitConditional_primary(EqlParser.Conditional_primaryContext ctx) { + if (ctx.simple_cond_expression() != null) { + return visit(ctx.simple_cond_expression()); + } + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.simple_cond_expression() != null) { - builder.append(visit(ctx.simple_cond_expression())); - } else if (ctx.conditional_expression() != null) { + if (ctx.conditional_expression() != null) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.conditional_expression())); @@ -1082,27 +1088,25 @@ public QueryTokenStream visitConditional_primary(EqlParser.Conditional_primaryCo @Override public QueryTokenStream visitSimple_cond_expression(EqlParser.Simple_cond_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.comparison_expression() != null) { - builder.append(visit(ctx.comparison_expression())); + return visit(ctx.comparison_expression()); } else if (ctx.between_expression() != null) { - builder.append(visit(ctx.between_expression())); + return visit(ctx.between_expression()); } else if (ctx.in_expression() != null) { - builder.append(visit(ctx.in_expression())); + return visit(ctx.in_expression()); } else if (ctx.like_expression() != null) { - builder.append(visit(ctx.like_expression())); + return visit(ctx.like_expression()); } else if (ctx.null_comparison_expression() != null) { - builder.append(visit(ctx.null_comparison_expression())); + return visit(ctx.null_comparison_expression()); } else if (ctx.empty_collection_comparison_expression() != null) { - builder.append(visit(ctx.empty_collection_comparison_expression())); + return visit(ctx.empty_collection_comparison_expression()); } else if (ctx.collection_member_expression() != null) { - builder.append(visit(ctx.collection_member_expression())); + return visit(ctx.collection_member_expression()); } else if (ctx.exists_expression() != null) { - builder.append(visit(ctx.exists_expression())); + return visit(ctx.exists_expression()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -1112,7 +1116,7 @@ public QueryTokenStream visitBetween_expression(EqlParser.Between_expressionCont if (ctx.arithmetic_expression(0) != null) { - builder.append(visit(ctx.arithmetic_expression(0))); + builder.appendExpression(visit(ctx.arithmetic_expression(0))); if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); @@ -1138,7 +1142,7 @@ public QueryTokenStream visitBetween_expression(EqlParser.Between_expressionCont } else if (ctx.datetime_expression(0) != null) { - builder.append(visit(ctx.datetime_expression(0))); + builder.appendExpression(visit(ctx.datetime_expression(0))); if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); @@ -1159,10 +1163,10 @@ public QueryTokenStream visitIn_expression(EqlParser.In_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.string_expression() != null) { - builder.append(visit(ctx.string_expression())); + builder.appendExpression(visit(ctx.string_expression())); } if (ctx.type_discriminator() != null) { - builder.append(visit(ctx.type_discriminator())); + builder.appendExpression(visit(ctx.type_discriminator())); } if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); @@ -1175,7 +1179,6 @@ public QueryTokenStream visitIn_expression(EqlParser.In_expressionContext ctx) { builder.append(TOKEN_OPEN_PAREN); builder.appendInline(QueryTokenStream.concat(ctx.in_item(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.subquery() != null) { @@ -1216,7 +1219,8 @@ public QueryTokenStream visitLike_expression(EqlParser.Like_expressionContext ct QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.string_expression())); + builder.appendExpression(visit(ctx.string_expression())); + if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); } @@ -1238,11 +1242,11 @@ public QueryTokenStream visitNull_comparison_expression(EqlParser.Null_compariso QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.single_valued_path_expression() != null) { - builder.append(visit(ctx.single_valued_path_expression())); + builder.appendExpression(visit(ctx.single_valued_path_expression())); } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); + builder.appendExpression(visit(ctx.input_parameter())); } else if (ctx.nullif_expression() != null) { - builder.append(visit(ctx.nullif_expression())); + builder.appendExpression(visit(ctx.nullif_expression())); } if (ctx.op != null) { @@ -1264,7 +1268,7 @@ public QueryTokenStream visitEmpty_collection_comparison_expression( QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.collection_valued_path_expression())); + builder.appendExpression(visit(ctx.collection_valued_path_expression())); builder.append(QueryTokens.expression(ctx.IS())); if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); @@ -1279,7 +1283,7 @@ public QueryTokenStream visitCollection_member_expression(EqlParser.Collection_m QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.entity_or_value_expression())); + builder.appendExpression(visit(ctx.entity_or_value_expression())); if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); } @@ -1295,34 +1299,30 @@ public QueryTokenStream visitCollection_member_expression(EqlParser.Collection_m @Override public QueryTokenStream visitEntity_or_value_expression(EqlParser.Entity_or_value_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.single_valued_object_path_expression() != null) { - builder.append(visit(ctx.single_valued_object_path_expression())); + return visit(ctx.single_valued_object_path_expression()); } else if (ctx.state_field_path_expression() != null) { - builder.append(visit(ctx.state_field_path_expression())); + return visit(ctx.state_field_path_expression()); } else if (ctx.simple_entity_or_value_expression() != null) { - builder.append(visit(ctx.simple_entity_or_value_expression())); + return visit(ctx.simple_entity_or_value_expression()); } - return builder; + return QueryTokenStream.empty(); } @Override public QueryTokenStream visitSimple_entity_or_value_expression( EqlParser.Simple_entity_or_value_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.identification_variable() != null) { - builder.append(visit(ctx.identification_variable())); + return visit(ctx.identification_variable()); } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); + return visit(ctx.input_parameter()); } else if (ctx.literal() != null) { - builder.append(visit(ctx.literal())); + return visit(ctx.literal()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -1367,13 +1367,13 @@ public QueryTokenStream visitStringComparison(EqlParser.StringComparisonContext QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendInline(visit(ctx.string_expression(0))); - builder.append(visit(ctx.comparison_operator())); + builder.appendExpression(visit(ctx.string_expression(0))); + builder.appendExpression(visit(ctx.comparison_operator())); if (ctx.string_expression(1) != null) { - builder.append(visit(ctx.string_expression(1))); + builder.appendExpression(visit(ctx.string_expression(1))); } else { - builder.append(visit(ctx.all_or_any_expression())); + builder.appendExpression(visit(ctx.all_or_any_expression())); } return builder; @@ -1388,9 +1388,9 @@ public QueryTokenStream visitBooleanComparison(EqlParser.BooleanComparisonContex builder.append(QueryTokens.ventilated(ctx.op)); if (ctx.boolean_expression(1) != null) { - builder.append(visit(ctx.boolean_expression(1))); + builder.appendExpression(visit(ctx.boolean_expression(1))); } else { - builder.append(visit(ctx.all_or_any_expression())); + builder.appendExpression(visit(ctx.all_or_any_expression())); } return builder; @@ -1410,9 +1410,9 @@ public QueryTokenStream visitEnumComparison(EqlParser.EnumComparisonContext ctx) builder.append(QueryTokens.ventilated(ctx.op)); if (ctx.enum_expression(1) != null) { - builder.append(visit(ctx.enum_expression(1))); + builder.appendExpression(visit(ctx.enum_expression(1))); } else { - builder.append(visit(ctx.all_or_any_expression())); + builder.appendExpression(visit(ctx.all_or_any_expression())); } return builder; @@ -1427,9 +1427,9 @@ public QueryTokenStream visitDatetimeComparison(EqlParser.DatetimeComparisonCont builder.append(QueryTokens.ventilated(ctx.comparison_operator().op)); if (ctx.datetime_expression(1) != null) { - builder.append(visit(ctx.datetime_expression(1))); + builder.appendExpression(visit(ctx.datetime_expression(1))); } else { - builder.append(visit(ctx.all_or_any_expression())); + builder.appendExpression(visit(ctx.all_or_any_expression())); } return builder; @@ -1444,9 +1444,9 @@ public QueryTokenStream visitEntityComparison(EqlParser.EntityComparisonContext builder.append(QueryTokens.expression(ctx.op)); if (ctx.entity_expression(1) != null) { - builder.append(visit(ctx.entity_expression(1))); + builder.appendExpression(visit(ctx.entity_expression(1))); } else { - builder.append(visit(ctx.all_or_any_expression())); + builder.appendExpression(visit(ctx.all_or_any_expression())); } return builder; @@ -1457,13 +1457,13 @@ public QueryTokenStream visitArithmeticComparison(EqlParser.ArithmeticComparison QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.arithmetic_expression(0))); - builder.append(visit(ctx.comparison_operator())); + builder.appendExpression(visit(ctx.arithmetic_expression(0))); + builder.appendExpression(visit(ctx.comparison_operator())); if (ctx.arithmetic_expression(1) != null) { - builder.append(visit(ctx.arithmetic_expression(1))); + builder.appendExpression(visit(ctx.arithmetic_expression(1))); } else { - builder.append(visit(ctx.all_or_any_expression())); + builder.appendExpression(visit(ctx.all_or_any_expression())); } return builder; @@ -1476,7 +1476,7 @@ public QueryTokenStream visitEntityTypeComparison(EqlParser.EntityTypeComparison builder.appendInline(visit(ctx.entity_type_expression(0))); builder.append(QueryTokens.ventilated(ctx.op)); - builder.append(visit(ctx.entity_type_expression(1))); + builder.appendExpression(visit(ctx.entity_type_expression(1))); return builder; } @@ -1495,42 +1495,37 @@ public QueryTokenStream visitRegexpComparison(EqlParser.RegexpComparisonContext @Override public QueryTokenStream visitComparison_operator(EqlParser.Comparison_operatorContext ctx) { - return QueryRendererBuilder.from(QueryTokens.ventilated(ctx.op)); + return QueryRenderer.from(QueryTokens.token(ctx.op)); } @Override public QueryTokenStream visitArithmetic_expression(EqlParser.Arithmetic_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.arithmetic_expression() != null) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(visit(ctx.arithmetic_expression())); - builder.append(QueryTokens.expression(ctx.op)); + builder.append(QueryTokens.ventilated(ctx.op)); builder.append(visit(ctx.arithmetic_term())); + return builder; } else { - builder.append(visit(ctx.arithmetic_term())); + return visit(ctx.arithmetic_term()); } - - return builder; } @Override public QueryTokenStream visitArithmetic_term(EqlParser.Arithmetic_termContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.arithmetic_term() != null) { - + QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendInline(visit(ctx.arithmetic_term())); builder.append(QueryTokens.ventilated(ctx.op)); builder.append(visit(ctx.arithmetic_factor())); + return builder; } else { - builder.append(visit(ctx.arithmetic_factor())); + return visit(ctx.arithmetic_factor()); } - - return builder; } @Override @@ -1541,7 +1536,8 @@ public QueryTokenStream visitArithmetic_factor(EqlParser.Arithmetic_factorContex if (ctx.op != null) { builder.append(QueryTokens.token(ctx.op)); } - builder.appendInline(visit(ctx.arithmetic_primary())); + + builder.append(visit(ctx.arithmetic_primary())); return builder; } @@ -1698,45 +1694,39 @@ public QueryTokenStream visitEnum_expression(EqlParser.Enum_expressionContext ct @Override public QueryTokenStream visitEntity_expression(EqlParser.Entity_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.single_valued_object_path_expression() != null) { - builder.append(visit(ctx.single_valued_object_path_expression())); + return visit(ctx.single_valued_object_path_expression()); } else if (ctx.simple_entity_expression() != null) { - builder.append(visit(ctx.simple_entity_expression())); + return visit(ctx.simple_entity_expression()); } - return builder; + return QueryTokenStream.empty(); } @Override public QueryTokenStream visitSimple_entity_expression(EqlParser.Simple_entity_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.identification_variable() != null) { - builder.append(visit(ctx.identification_variable())); + return visit(ctx.identification_variable()); } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); + return visit(ctx.input_parameter()); } - return builder; + return QueryTokenStream.empty(); } @Override public QueryTokenStream visitEntity_type_expression(EqlParser.Entity_type_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.type_discriminator() != null) { - builder.append(visit(ctx.type_discriminator())); + return visit(ctx.type_discriminator()); } else if (ctx.entity_type_literal() != null) { - builder.append(visit(ctx.entity_type_literal())); + return visit(ctx.entity_type_literal()); } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); + return visit(ctx.input_parameter()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -1911,7 +1901,7 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.append(QueryTokens.token(ctx.SUBSTRING())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression(0))); + builder.append(visit(ctx.string_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(QueryTokenStream.concat(ctx.arithmetic_expression(), this::visit, TOKEN_COMMA)); builder.append(TOKEN_CLOSE_PAREN); @@ -1928,7 +1918,7 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret if (ctx.FROM() != null) { builder.append(QueryTokens.expression(ctx.FROM())); } - builder.appendInline(visit(ctx.string_expression(0))); + builder.append(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.LOWER() != null) { @@ -1940,10 +1930,9 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.append(QueryTokens.token(ctx.UPPER())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression(0))); + builder.append(visit(ctx.string_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.LEFT() != null) { - builder.append(QueryTokens.token(ctx.LEFT())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1951,7 +1940,6 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.RIGHT() != null) { - builder.append(QueryTokens.token(ctx.RIGHT())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1959,7 +1947,6 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.REPLACE() != null) { - builder.append(QueryTokens.token(ctx.REPLACE())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1977,11 +1964,11 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret public QueryTokenStream visitTrim_specification(EqlParser.Trim_specificationContext ctx) { if (ctx.LEADING() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.LEADING())); + return QueryRenderer.from(QueryTokens.expression(ctx.LEADING())); } else if (ctx.TRAILING() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.TRAILING())); + return QueryRenderer.from(QueryTokens.expression(ctx.TRAILING())); } else { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.BOTH())); + return QueryRenderer.from(QueryTokens.expression(ctx.BOTH())); } } @@ -2033,12 +2020,15 @@ public QueryTokenStream visitFunction_invocation(EqlParser.Function_invocationCo public QueryTokenStream visitExtract_datetime_field(EqlParser.Extract_datetime_fieldContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.datetime_field())); + nested.append(QueryTokens.expression(ctx.FROM())); + nested.appendExpression(visit(ctx.datetime_expression())); builder.append(QueryTokens.token(ctx.EXTRACT())); builder.append(TOKEN_OPEN_PAREN); - builder.appendExpression(visit(ctx.datetime_field())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendInline(visit(ctx.datetime_expression())); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -2053,12 +2043,15 @@ public QueryTokenStream visitDatetime_field(EqlParser.Datetime_fieldContext ctx) public QueryTokenStream visitExtract_datetime_part(EqlParser.Extract_datetime_partContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.datetime_part())); + nested.append(QueryTokens.expression(ctx.FROM())); + nested.appendExpression(visit(ctx.datetime_expression())); builder.append(QueryTokens.token(ctx.EXTRACT())); builder.append(TOKEN_OPEN_PAREN); - builder.appendExpression(visit(ctx.datetime_part())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendInline(visit(ctx.datetime_expression())); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -2111,10 +2104,7 @@ public QueryTokenStream visitGeneral_case_expression(EqlParser.General_case_expr QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.CASE())); - - ctx.when_clause().forEach(whenClauseContext -> { - builder.appendExpression(visit(whenClauseContext)); - }); + builder.appendExpression(QueryTokenStream.concat(ctx.when_clause(), this::visit, TOKEN_SPACE)); builder.append(QueryTokens.expression(ctx.ELSE())); builder.appendExpression(visit(ctx.scalar_expression())); @@ -2129,9 +2119,9 @@ public QueryTokenStream visitWhen_clause(EqlParser.When_clauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.WHEN())); - builder.append(visit(ctx.conditional_expression())); + builder.appendExpression(visit(ctx.conditional_expression())); builder.append(QueryTokens.expression(ctx.THEN())); - builder.append(visit(ctx.scalar_expression())); + builder.appendExpression(visit(ctx.scalar_expression())); return builder; } @@ -2142,14 +2132,11 @@ public QueryTokenStream visitSimple_case_expression(EqlParser.Simple_case_expres QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.CASE())); - builder.append(visit(ctx.case_operand())); - - ctx.simple_when_clause().forEach(simpleWhenClauseContext -> { - builder.append(visit(simpleWhenClauseContext)); - }); + builder.appendExpression(visit(ctx.case_operand())); + builder.appendExpression(QueryTokenStream.concat(ctx.simple_when_clause(), this::visit, TOKEN_SPACE)); builder.append(QueryTokens.expression(ctx.ELSE())); - builder.append(visit(ctx.scalar_expression())); + builder.appendExpression(visit(ctx.scalar_expression())); builder.append(QueryTokens.expression(ctx.END())); return builder; @@ -2210,11 +2197,11 @@ public QueryTokenStream visitNullif_expression(EqlParser.Nullif_expressionContex public QueryTokenStream visitTrim_character(EqlParser.Trim_characterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); } else if (ctx.character_valued_input_parameter() != null) { return visit(ctx.character_valued_input_parameter()); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -2222,14 +2209,14 @@ public QueryTokenStream visitTrim_character(EqlParser.Trim_characterContext ctx) public QueryTokenStream visitIdentification_variable(EqlParser.Identification_variableContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); - } else if (ctx.f != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.f)); + return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); } else if (ctx.type_literal() != null) { return visit(ctx.type_literal()); + } else if (ctx.f != null) { + return QueryRenderer.from(QueryTokens.token(ctx.f)); + } else { + return QueryTokenStream.empty(); } - - return QueryRenderer.builder(); } @Override @@ -2240,23 +2227,23 @@ public QueryTokenStream visitConstructor_name(EqlParser.Constructor_nameContext @Override public QueryTokenStream visitLiteral(EqlParser.LiteralContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.STRINGLITERAL() != null) { - builder.append(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); + } else if (ctx.JAVASTRINGLITERAL() != null) { + return QueryRenderer.from(QueryTokens.expression(ctx.JAVASTRINGLITERAL())); } else if (ctx.INTLITERAL() != null) { - builder.append(QueryTokens.expression(ctx.INTLITERAL())); + return QueryRenderer.from(QueryTokens.expression(ctx.INTLITERAL())); } else if (ctx.FLOATLITERAL() != null) { - builder.append(QueryTokens.expression(ctx.FLOATLITERAL())); + return QueryRenderer.from(QueryTokens.expression(ctx.FLOATLITERAL())); } else if (ctx.LONGLITERAL() != null) { - builder.append(QueryTokens.expression(ctx.LONGLITERAL())); + return QueryRenderer.from(QueryTokens.expression(ctx.LONGLITERAL())); } else if (ctx.boolean_literal() != null) { - builder.append(visit(ctx.boolean_literal())); + return visit(ctx.boolean_literal()); } else if (ctx.entity_type_literal() != null) { - builder.append(visit(ctx.entity_type_literal())); + return visit(ctx.entity_type_literal()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -2310,20 +2297,20 @@ public QueryTokenStream visitEntity_type_literal(EqlParser.Entity_type_literalCo @Override public QueryTokenStream visitEscape_character(EqlParser.Escape_characterContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.CHARACTER())); + return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); } @Override public QueryTokenStream visitNumeric_literal(EqlParser.Numeric_literalContext ctx) { if (ctx.INTLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTLITERAL())); + return QueryRenderer.from(QueryTokens.token(ctx.INTLITERAL())); } else if (ctx.FLOATLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.FLOATLITERAL())); + return QueryRenderer.from(QueryTokens.token(ctx.FLOATLITERAL())); } else if (ctx.LONGLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.LONGLITERAL())); + return QueryRenderer.from(QueryTokens.token(ctx.LONGLITERAL())); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -2331,11 +2318,11 @@ public QueryTokenStream visitNumeric_literal(EqlParser.Numeric_literalContext ct public QueryTokenStream visitBoolean_literal(EqlParser.Boolean_literalContext ctx) { if (ctx.TRUE() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.TRUE())); + return QueryRenderer.from(QueryTokens.expression(ctx.TRUE())); } else if (ctx.FALSE() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.FALSE())); + return QueryRenderer.from(QueryTokens.expression(ctx.FALSE())); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -2348,11 +2335,11 @@ public QueryTokenStream visitEnum_literal(EqlParser.Enum_literalContext ctx) { public QueryTokenStream visitString_literal(EqlParser.String_literalContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); } else if (ctx.STRINGLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -2409,7 +2396,7 @@ public QueryTokenStream visitCollection_value_field(EqlParser.Collection_value_f @Override public QueryTokenStream visitEntity_name(EqlParser.Entity_nameContext ctx) { - return QueryTokenStream.concat(ctx.reserved_word(), this::visit, QueryRenderer::inline, TOKEN_DOT); + return QueryTokenStream.concat(ctx.reserved_word(), this::visit, TOKEN_DOT); } @Override @@ -2440,26 +2427,25 @@ public QueryTokenStream visitFunction_name(EqlParser.Function_nameContext ctx) { } @Override - public QueryTokenStream visitCharacter_valued_input_parameter( - EqlParser.Character_valued_input_parameterContext ctx) { + public QueryTokenStream visitCharacter_valued_input_parameter(EqlParser.Character_valued_input_parameterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); } else if (ctx.input_parameter() != null) { return visit(ctx.input_parameter()); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @Override public QueryTokenStream visitReserved_word(EqlParser.Reserved_wordContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); + return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); } else if (ctx.f != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.f)); + return QueryRenderer.from(QueryTokens.token(ctx.f)); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java index e544024750..04fdc9e7ca 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java @@ -24,7 +24,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query by applying @@ -54,7 +53,7 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { } @Override - public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -73,12 +72,10 @@ public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementCont builder.appendExpression(visit(ctx.having_clause())); } - doVisitOrderBy(builder, ctx, ObjectUtils.isEmpty(ctx.setOperator()) ? this.sort : Sort.unsorted()); - - for (int i = 0; i < ctx.setOperator().size(); i++) { - - builder.appendExpression(visit(ctx.setOperator(i))); - builder.appendExpression(visit(ctx.select_statement(i))); + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx); } return builder; @@ -104,7 +101,7 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { return builder.append(dtoDelegate.transformSelectionList(tokenStream)); } - private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) { + private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) { if (ctx.orderby_clause() != null) { QueryTokenStream existingOrder = visit(ctx.orderby_clause()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index 923c2d48c6..289e6a5b64 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -44,16 +44,6 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer { @Override public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { - if(ctx.select_query() != null) { - return visitSelect_query(ctx.select_query()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(visit(ctx.select_clause())); @@ -68,7 +58,7 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { if (ctx.having_clause() != null) { builder.appendExpression(visit(ctx.having_clause())); } - if(ctx.set_fuction() != null) { + if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index aeec4497d7..892f37bff9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -15,28 +15,14 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COLON; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOT; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_EQUALS; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_QUESTION_MARK; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_DOUBLE_PIPE; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_SPACE; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_CLOSE_PAREN; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_OPEN_PAREN; +import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.ArrayList; import java.util.List; import org.antlr.v4.runtime.tree.ParseTree; -import org.springframework.data.jpa.repository.query.JpqlParser.Except_clauseContext; -import org.springframework.data.jpa.repository.query.JpqlParser.Intersect_clauseContext; -import org.springframework.data.jpa.repository.query.JpqlParser.Relation_fuctions_selectContext; import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext; -import org.springframework.data.jpa.repository.query.JpqlParser.Cast_expressionContext; import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext; import org.springframework.data.jpa.repository.query.JpqlParser.Set_fuctionContext; import org.springframework.data.jpa.repository.query.JpqlParser.Type_literalContext; @@ -72,17 +58,8 @@ public QueryTokenStream visitQl_statement(JpqlParser.Ql_statementContext ctx) { } } - @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { - - if(ctx.select_query() != null) { - return visitSelect_query(ctx.select_query()); - } - - return QueryTokenStream.empty(); - } - - public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { + @Override + public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -105,32 +82,13 @@ public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { builder.appendExpression(visit(ctx.orderby_clause())); } - if(ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); } return builder; } - @Override - public List visitSetOperator_with_select_statement( - JpqlParser.SetOperator_with_select_statementContext ctx) { - - List tokens = new ArrayList<>(); - - if (ctx.INTERSECT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.INTERSECT())); - } else if (ctx.UNION() != null) { - tokens.add(new JpaQueryParsingToken(ctx.UNION())); - } else if (ctx.EXCEPT() != null) { - tokens.add(new JpaQueryParsingToken(ctx.EXCEPT())); - } - - tokens.addAll(visit(ctx.select_statement())); - - return builder; - } - @Override public QueryTokenStream visitUpdate_statement(JpqlParser.Update_statementContext ctx) { @@ -229,14 +187,19 @@ public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.join_spec())); - builder.append(visit(ctx.join_association_path_expression())); + builder.appendExpression(visit(ctx.join_spec())); + builder.appendExpression(visit(ctx.join_association_path_expression())); + if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } - builder.append(visit(ctx.identification_variable())); + + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } + if (ctx.join_condition() != null) { - builder.append(visit(ctx.join_condition())); + builder.appendExpression(visit(ctx.join_condition())); } return builder; @@ -247,9 +210,19 @@ public QueryTokenStream visitFetch_join(JpqlParser.Fetch_joinContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.join_spec())); + builder.appendExpression(visit(ctx.join_spec())); builder.append(QueryTokens.expression(ctx.FETCH())); - builder.append(visit(ctx.join_association_path_expression())); + builder.appendExpression(visit(ctx.join_association_path_expression())); + + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } + if (ctx.join_condition() != null) { + builder.appendExpression(visit(ctx.join_condition())); + } return builder; } @@ -300,23 +273,25 @@ public QueryTokenStream visitJoin_association_path_expression( builder.appendExpression(visit(ctx.join_single_valued_path_expression())); } } else { + QueryRendererBuilder nested = QueryRenderer.builder(); + if (ctx.join_collection_valued_path_expression() != null) { - builder.append(QueryTokens.token(ctx.TREAT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.join_collection_valued_path_expression())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.subtype())); - builder.append(TOKEN_CLOSE_PAREN); + nested.appendExpression(visit(ctx.join_collection_valued_path_expression())); + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.subtype())); + } else if (ctx.join_single_valued_path_expression() != null) { - builder.append(QueryTokens.token(ctx.TREAT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.join_single_valued_path_expression())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.subtype())); - builder.append(TOKEN_CLOSE_PAREN); + nested.appendExpression(visit(ctx.join_single_valued_path_expression())); + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.subtype())); } + + builder.append(QueryTokens.token(ctx.TREAT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); } return builder; @@ -476,12 +451,15 @@ public QueryTokenStream visitSimple_subpath(JpqlParser.Simple_subpathContext ctx public QueryTokenStream visitTreated_subpath(JpqlParser.Treated_subpathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.general_subpath())); + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.subtype())); builder.append(QueryTokens.token(ctx.TREAT())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.general_subpath())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.subtype())); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -825,19 +803,6 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx return builder; } - @Override - public QueryTokenStream visitSet_fuction(Set_fuctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.setOperator().getStart())); - if(ctx.setOperator().ALL() != null) { - builder.append(QueryTokens.expression(ctx.setOperator().ALL())); - } - builder.appendExpression(visit(ctx.set_function_select().select_query())); - return builder; - } - @Override public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { @@ -853,48 +818,60 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { if (ctx.ASC() != null) { builder.append(QueryTokens.expression(ctx.ASC())); - } - if (ctx.DESC() != null) { + } else if (ctx.DESC() != null) { builder.append(QueryTokens.expression(ctx.DESC())); } if (ctx.nullsPrecedence() != null) { builder.append(visit(ctx.nullsPrecedence())); } - if (ctx.nullsPrecedence() != null) { - tokens.addAll(visit(ctx.nullsPrecedence())); - } - return tokens; + return builder; } @Override - public List visitNullsPrecedence(JpqlParser.NullsPrecedenceContext ctx) { + public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) { - List tokens = new ArrayList<>(); + QueryRendererBuilder builder = QueryRenderer.builder(); - tokens.add(new JpaQueryParsingToken(ctx.NULLS())); + builder.append(QueryTokens.expression(ctx.NULLS())); if (ctx.FIRST() != null) { - tokens.add(new JpaQueryParsingToken(ctx.FIRST())); + builder.append(QueryTokens.expression(ctx.FIRST())); } else if (ctx.LAST() != null) { - tokens.add(new JpaQueryParsingToken(ctx.LAST())); + builder.append(QueryTokens.expression(ctx.LAST())); } return builder; } @Override - public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) { + public QueryTokenStream visitSet_fuction(Set_fuctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_NULLS); + if (ctx.setOperator() != null) { + builder.append(visit(ctx.setOperator())); + } - if (ctx.FIRST() != null) { - builder.append(TOKEN_FIRST); - } else if (ctx.LAST() != null) { - builder.append(TOKEN_LAST); + builder.appendExpression(visit(ctx.select_statement())); + + return builder; + } + + @Override + public QueryTokenStream visitSetOperator(JpqlParser.SetOperatorContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.INTERSECT() != null) { + builder.append(QueryTokens.expression(ctx.INTERSECT())); + } else if (ctx.UNION() != null) { + builder.append(QueryTokens.expression(ctx.UNION())); + } else if (ctx.EXCEPT() != null) { + builder.append(QueryTokens.expression(ctx.EXCEPT())); + } else if (ctx.ALL() != null) { + builder.append(QueryTokens.expression(ctx.ALL())); } return builder; @@ -1012,8 +989,8 @@ public QueryTokenStream visitScalar_expression(JpqlParser.Scalar_expressionConte return visit(ctx.case_expression()); } else if (ctx.entity_type_expression() != null) { return visit(ctx.entity_type_expression()); - } else if (ctx.cast_expression() != null) { - return (visit(ctx.cast_expression())); + } else if (ctx.cast_function() != null) { + return (visit(ctx.cast_function())); } return QueryTokenStream.empty(); @@ -1247,9 +1224,11 @@ public QueryTokenStream visitNull_comparison_expression(JpqlParser.Null_comparis } builder.append(QueryTokens.expression(ctx.IS())); + if (ctx.NOT() != null) { builder.append(QueryTokens.expression(ctx.NOT())); } + builder.append(QueryTokens.expression(ctx.NULL())); return builder; @@ -1503,6 +1482,8 @@ public QueryTokenStream visitArithmetic_primary(JpqlParser.Arithmetic_primaryCon builder.append(visit(ctx.aggregate_expression())); } else if (ctx.case_expression() != null) { builder.append(visit(ctx.case_expression())); + } else if (ctx.cast_function() != null) { + builder.append(visit(ctx.cast_function())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); } else if (ctx.subquery() != null) { @@ -1534,11 +1515,6 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(visit(ctx.case_expression())); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); - } else if (ctx.op != null) { - - tokens.addAll(visit(ctx.string_expression(0))); - tokens.add(new JpaQueryParsingToken(ctx.op)); - tokens.addAll(visit(ctx.string_expression(1))); } else if (ctx.subquery() != null) { builder.append(TOKEN_OPEN_PAREN); @@ -1792,6 +1768,8 @@ public QueryTokenStream visitFunctions_returning_numerics(JpqlParser.Functions_r builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.identification_variable())); builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.extract_datetime_field() != null) { + builder.append(visit(ctx.extract_datetime_field())); } return builder; @@ -1819,6 +1797,8 @@ public QueryTokenStream visitFunctions_returning_datetime(JpqlParser.Functions_r } else if (ctx.DATETIME() != null) { builder.append(QueryTokens.expression(ctx.DATETIME())); } + } else if (ctx.extract_datetime_part() != null) { + builder.append(visit(ctx.extract_datetime_part())); } return builder; @@ -1840,6 +1820,7 @@ public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_re builder.append(QueryTokens.token(ctx.SUBSTRING())); builder.append(TOKEN_OPEN_PAREN); builder.append(visit(ctx.string_expression(0))); + builder.append(TOKEN_COMMA); builder.appendInline(QueryTokenStream.concat(ctx.arithmetic_expression(), this::visit, TOKEN_COMMA)); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.TRIM() != null) { @@ -1909,6 +1890,28 @@ public QueryTokenStream visitTrim_specification(JpqlParser.Trim_specificationCon } } + @Override + public QueryTokenStream visitCast_function(JpqlParser.Cast_functionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.token(ctx.CAST())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.single_valued_path_expression())); + builder.append(TOKEN_SPACE); + builder.appendInline(QueryTokenStream.concat(ctx.identification_variable(), this::visit, TOKEN_SPACE)); + + if (!ObjectUtils.isEmpty(ctx.numeric_literal())) { + + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); + builder.append(TOKEN_CLOSE_PAREN); + } + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + @Override public QueryTokenStream visitFunction_invocation(JpqlParser.Function_invocationContext ctx) { @@ -1930,12 +1933,15 @@ public QueryTokenStream visitFunction_invocation(JpqlParser.Function_invocationC public QueryTokenStream visitExtract_datetime_field(JpqlParser.Extract_datetime_fieldContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.EXTRACT())); + nested.appendExpression(visit(ctx.datetime_field())); + nested.append(QueryTokens.expression(ctx.FROM())); + nested.appendExpression(visit(ctx.datetime_expression())); + + builder.append(QueryTokens.token(ctx.EXTRACT())); builder.append(TOKEN_OPEN_PAREN); - builder.appendExpression(visit(ctx.datetime_field())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendInline(visit(ctx.datetime_expression())); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -1950,12 +1956,15 @@ public QueryTokenStream visitDatetime_field(JpqlParser.Datetime_fieldContext ctx public QueryTokenStream visitExtract_datetime_part(JpqlParser.Extract_datetime_partContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.datetime_part())); + nested.append(QueryTokens.expression(ctx.FROM())); + nested.appendExpression(visit(ctx.datetime_expression())); - builder.append(QueryTokens.expression(ctx.EXTRACT())); + builder.append(QueryTokens.token(ctx.EXTRACT())); builder.append(TOKEN_OPEN_PAREN); - builder.appendExpression(visit(ctx.datetime_part())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.append(visit(ctx.datetime_expression())); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -1994,18 +2003,6 @@ public QueryTokenStream visitCase_expression(JpqlParser.Case_expressionContext c } } - @Override - public QueryRendererBuilder visitCast_expression(Cast_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.type_literal())); - builder.append(TOKEN_CLOSE_PAREN); - return builder; - } - @Override public QueryRendererBuilder visitType_literal(Type_literalContext ctx) { @@ -2099,7 +2096,7 @@ public QueryTokenStream visitNullif_expression(JpqlParser.Nullif_expressionConte QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.NULLIF())); + builder.append(QueryTokens.token(ctx.NULLIF())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.scalar_expression(0))); builder.append(TOKEN_COMMA); @@ -2125,7 +2122,9 @@ public QueryTokenStream visitTrim_character(JpqlParser.Trim_characterContext ctx public QueryTokenStream visitIdentification_variable(JpqlParser.Identification_variableContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); + return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); + } else if (ctx.type_literal() != null) { + return visit(ctx.type_literal()); } else if (ctx.f != null) { return QueryRenderer.from(QueryTokens.token(ctx.f)); } else { @@ -2185,7 +2184,18 @@ public QueryTokenStream visitPattern_value(JpqlParser.Pattern_valueContext ctx) @Override public QueryTokenStream visitDate_time_timestamp_literal(JpqlParser.Date_time_timestamp_literalContext ctx) { - return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); + + if (ctx.STRINGLITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRINGLITERAL())); + } else if (ctx.DATELITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.DATELITERAL())); + } else if (ctx.TIMELITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.TIMELITERAL())); + } else if (ctx.TIMESTAMPLITERAL() != null) { + return QueryRendererBuilder.from(QueryTokens.expression(ctx.TIMESTAMPLITERAL())); + } else { + return QueryRenderer.builder(); + } } @Override @@ -2289,7 +2299,7 @@ public QueryTokenStream visitCollection_value_field(JpqlParser.Collection_value_ @Override public QueryTokenStream visitEntity_name(JpqlParser.Entity_nameContext ctx) { - return QueryTokenStream.concat(ctx.reserved_word(), this::visitReserved_word, TOKEN_DOT); + return QueryTokenStream.concat(ctx.reserved_word(), this::visit, TOKEN_DOT); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index a3e9fddbfd..3cef78794b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -54,16 +54,6 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { @Override public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { - if(ctx.select_query() != null) { - return visitSelect_query(ctx.select_query()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSelect_query(JpqlParser.Select_queryContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(visit(ctx.select_clause())); @@ -110,7 +100,7 @@ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) return builder.append(dtoDelegate.transformSelectionList(tokenStream)); } - private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_queryContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { if (ctx.orderby_clause() != null) { QueryTokenStream existingOrder = visit(ctx.orderby_clause()); @@ -160,4 +150,5 @@ public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { return tokens; } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java index af07eb0013..a88e23f9a6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java @@ -106,7 +106,8 @@ void testNamedOutputParameter() { new Employee(4, "Gabriel")); } - @DisabledOnHibernate("6") + @DisabledOnHibernate(value = "7", + disabledReason = "class org.hibernate.metamodel.model.domain.internal.EntityTypeImpl cannot be cast to class org.hibernate.query.OutputableType (org.hibernate.metamodel.model.domain.internal.EntityTypeImpl and org.hibernate.query.OutputableType are in unnamed module of loader 'app')") @Test // 2256 void testSingleEntityFromResultSet() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java index c0819dc928..8ae77e5553 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -98,6 +98,7 @@ void joinFetch() { assertQuery("SELECT e FROM Employee e JOIN FETCH e.address"); assertQuery("SELECT e FROM Employee e JOIN FETCH e.address a ORDER BY a.city"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address AS a ORDER BY a.city"); } @Test @@ -118,6 +119,16 @@ void subselectsInFromClause() { "SELECT e, c.city FROM Employee e, (SELECT DISTINCT a.city FROM Address a) c WHERE e.address.city = c.city"); } + @Test // GH-3277 + void numericLiterals() { + + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + } + @Test void orderByClause() { @@ -442,7 +453,7 @@ void except() { @ParameterizedTest // GH-3136 @ValueSource(strings = { "STRING", "INTEGER", "FLOAT", "DOUBLE" }) - void jpqlCast(String targetType) { + void cast(String targetType) { assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); } @@ -462,4 +473,5 @@ void replaceStringFunctions() { void stringConcatWithPipes() { assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java index 72bdfc3b1b..1f346a4970 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java @@ -1024,6 +1024,59 @@ void powerShouldBeLegalInAQuery() { assertQuery("select e.power.id from MyEntity e"); } + @Test // GH-3136 + void doublePipeShouldBeValidAsAStringConcatOperator() { + + assertQuery(""" + select e.name || ' ' || e.title + from Employee e + """); + } + + @Test // GH-3136 + void combinedSelectStatementsShouldWork() { + + assertQuery(""" + select e from Employee e where e.last_name = 'Baggins' + intersect + select e from Employee e where e.first_name = 'Samwise' + union + select e from Employee e where e.home = 'The Shire' + except + select e from Employee e where e.home = 'Isengard' + """); + } + + @Disabled + @Test // GH-3136 + void additionalStringOperationsShouldWork() { + + assertQuery(""" + select + replace(e.name, 'Baggins', 'Proudfeet'), + left(e.role, 4), + right(e.home, 5), + cast(e.distance_from_home, int) + from Employee e + """); + } + + @Test // GH-3136 + void orderByWithNullsFirstOrLastShouldWork() { + + assertQuery(""" + select a + from Element a + order by mutationAm desc nulls first + """); + + assertQuery(""" + select a + from Element a + order by mutationAm desc nulls last + """); + } + @ParameterizedTest // GH-3342 @ValueSource(strings = { "select 1 from User u", "select -1 from User u", "select +1 from User u", "select +1 * -100 from User u", "select count(u) * -0.7f from User u", @@ -1064,4 +1117,5 @@ void reservedWordsShouldWork() { assertQuery("select f from FooEntity f where upper(f.name) IN :names"); assertQuery("select f from FooEntity f where f.size IN :sizes"); } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java index bff45ec75d..df67e51b7d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java @@ -275,7 +275,7 @@ void fromClauseDowncastingExample1() { assertQuery(""" SELECT b.name, b.ISBN FROM Order o JOIN TREAT(o.product AS Book) b - """); + """); } @Test @@ -284,7 +284,7 @@ void fromClauseDowncastingExample2() { assertQuery(""" SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp WHERE lp.budget > 1000 - """); + """); } /** @@ -299,7 +299,7 @@ void fromClauseDowncastingExample3_SPEC_BUG() { WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" - """); + """); } @Test @@ -310,7 +310,7 @@ void fromClauseDowncastingExample3fixed() { WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE 'cost overrun' - """); + """); } @Test @@ -320,7 +320,39 @@ void fromClauseDowncastingExample4() { SELECT e FROM Employee e WHERE TREAT(e AS Exempt).vacationDays > 10 OR TREAT(e AS Contractor).hours > 100 - """); + """); + } + + @Test // GH-3136 + void substring() { + + assertQuery("select substring(c.number, 1, 2) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1) " + // + "from Call c"); + } + + @Test // GH-3136 + void currentDateFunctions() { + + assertQuery("select CURRENT_DATE " + // + "from Call c "); + + assertQuery("select CURRENT_TIME " + // + "from Call c "); + + assertQuery("select CURRENT_TIMESTAMP " + // + "from Call c "); + + assertQuery("select LOCAL_DATE " + // + "from Call c "); + + assertQuery("select LOCAL_TIME " + // + "from Call c "); + + assertQuery("select LOCAL_DATETIME " + // + "from Call c "); } @Test @@ -384,7 +416,7 @@ void allExample() { WHERE emp.salary > ALL (SELECT m.salary FROM Manager m WHERE m.department = emp.department) - """); + """); } @Test @@ -396,7 +428,7 @@ void existsSubSelectExample2() { WHERE EXISTS (SELECT spouseEmp FROM Employee spouseEmp WHERE spouseEmp = emp.spouse) - """); + """); } @Test @@ -464,7 +496,7 @@ void updateCaseExample1() { WHEN e.rating = 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -477,7 +509,7 @@ void updateCaseExample2() { WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END - """); + """); } @Test @@ -517,7 +549,7 @@ void theRest() { SELECT e FROM Employee e WHERE TYPE(e) IN (Exempt, Contractor) - """); + """); } @Test diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 7c158b3242..2025f116ad 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1657,14 +1657,14 @@ void orderByWithNullsFirstOrLastShouldWork() { from Element a where a.erstelltDurch = :variable order by mutationAm desc nulls last - """); + """); } @Test // GH-2964 void roundFunctionShouldWorkLikeAnyOtherFunction() { assertQuery(""" - select round(count(ri)*100/max(ri.receipt.positions), 0) as perc + select round(count(ri) * 100 / max(ri.receipt.positions), 0) as perc from StockOrderItem oi right join StockReceiptItem ri on ri.article = oi.article diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java index f7bc8f76c9..a346c8c39e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -27,6 +27,7 @@ * suffix. * * @author Christoph Strobl + * @author Mark Paluch */ class JpqlComplianceTests { @@ -50,6 +51,52 @@ private String reduceWhitespace(String original) { .trim(); } + @Test + void selectQueries() { + + assertQuery("Select e FROM Employee e WHERE e.salary > 100000"); + assertQuery("Select e FROM Employee e WHERE e.id = :id"); + assertQuery("Select MAX(e.salary) FROM Employee e"); + assertQuery("Select e.firstName FROM Employee e"); + assertQuery("Select e.firstName, e.lastName FROM Employee e"); + } + + @Test + void selectClause() { + + assertQuery("SELECT COUNT(e) FROM Employee e"); + assertQuery("SELECT MAX(e.salary) FROM Employee e"); + assertQuery("SELECT NEW com.acme.reports.EmpReport(e.firstName, e.lastName, e.salary) FROM Employee e"); + } + + @Test + void fromClause() { + + assertQuery("SELECT e FROM Employee e"); + assertQuery("SELECT e, a FROM Employee e, MailingAddress a WHERE e.address = a.address"); + assertQuery("SELECT e FROM com.acme.Employee e"); + } + + @Test + void join() { + + assertQuery("SELECT e FROM Employee e JOIN e.address a WHERE a.city = :city"); + assertQuery("SELECT e FROM Employee e JOIN e.projects p JOIN e.projects p2 WHERE p.name = :p1 AND p2.name = :p2"); + } + + @Test + void joinFetch() { + + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address a ORDER BY a.city"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address AS a ORDER BY a.city"); + } + + @Test + void leftJoin() { + assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); + } + @Test // GH-3277 void numericLiterals() { @@ -65,6 +112,27 @@ void newWithStrings() { assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); } + @Test + void orderByClause() { + + assertQuery("SELECT e FROM Employee e ORDER BY e.lastName ASC, e.firstName ASC"); // Typo in EQL document + assertQuery("SELECT e FROM Employee e LEFT JOIN e.manager m ORDER BY m.lastName NULLS FIRST"); + assertQuery("SELECT e FROM Employee e ORDER BY e.address"); + } + + @Test + void groupByClause() { + + assertQuery("SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city"); + assertQuery("SELECT e, COUNT(p) FROM Employee e LEFT JOIN e.projects p GROUP BY e"); + } + + @Test + void havingClause() { + assertQuery( + "SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city HAVING AVG(e.salary) > 100000"); + } + @Test // GH-3136 void union() { @@ -72,6 +140,141 @@ void union() { SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1 UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 """); + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @Test + void whereClause() { + // TBD + } + + @Test + void updateQueries() { + assertQuery("UPDATE Employee e SET e.salary = 60000 WHERE e.salary = 50000"); + } + + @Test + void deleteQueries() { + assertQuery("DELETE FROM Employee e WHERE e.department IS NULL"); + } + + @Test + void literals() { + + assertQuery("SELECT e FROM Employee e WHERE e.name = 'Bob'"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + assertQuery("SELECT e FROM Employee e WHERE e.active = TRUE"); + assertQuery("SELECT e FROM Employee e WHERE e.startDate = {d'2012-01-03'}"); + assertQuery("SELECT e FROM Employee e WHERE e.startTime = {t'09:00:00'}"); + assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}"); + assertQuery("SELECT e FROM Employee e WHERE e.gender = org.acme.Gender.MALE"); + assertQuery("UPDATE Employee e SET e.manager = NULL WHERE e.manager = :manager"); + } + + @Test + void functionsInSelect() { + + assertQuery("SELECT e.salary - 1000 FROM Employee e"); + assertQuery("SELECT e.salary + 1000 FROM Employee e"); + assertQuery("SELECT e.salary * 2 FROM Employee e"); + assertQuery("SELECT e.salary * 2.0 FROM Employee e"); + assertQuery("SELECT e.salary / 2 FROM Employee e"); + assertQuery("SELECT e.salary / 2.0 FROM Employee e"); + assertQuery("SELECT ABS(e.salary - e.manager.salary) FROM Employee e"); + assertQuery( + "select e from Employee e where case e.firstName when 'Bob' then 'Robert' when 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery( + "select case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end from Employee e where e.firstName = 'Bob' or e.firstName = 'Jill'"); + assertQuery( + "select e from Employee e where case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery("SELECT COALESCE(e.salary, 0) FROM Employee e"); + assertQuery("SELECT CONCAT(e.firstName, ' ', e.lastName) FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_DATE FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIME FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIMESTAMP FROM Employee e"); + assertQuery("SELECT LENGTH(e.lastName) FROM Employee e"); + assertQuery("SELECT LOWER(e.lastName) FROM Employee e"); + assertQuery("SELECT MOD(e.hoursWorked, 8) FROM Employee e"); + assertQuery("SELECT NULLIF(e.salary, 0) FROM Employee e"); + assertQuery("SELECT SQRT(o.RESULT) FROM Output o"); + assertQuery("SELECT SUBSTRING(e.lastName, 0, 2) FROM Employee e"); + assertQuery( + "SELECT TRIM(TRAILING FROM e.lastName), TRIM(e.lastName), TRIM(LEADING '-' FROM e.lastName) FROM Employee e"); + assertQuery("SELECT UPPER(e.lastName) FROM Employee e"); + assertQuery("SELECT CAST(e.salary NUMERIC(10, 2)) FROM Employee e"); + assertQuery("SELECT EXTRACT(YEAR FROM e.startDate) FROM Employee e"); + } + + @Test + void functionsInWhere() { + + assertQuery("SELECT e FROM Employee e WHERE e.salary - 1000 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary + 1000 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary * 2 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary * 2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary / 2 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary / 2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE ABS(e.salary - e.manager.salary) > 0"); + assertQuery("SELECT e FROM Employee e WHERE COALESCE(e.salary, 0) > 0"); + assertQuery("SELECT e FROM Employee e WHERE CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_DATE > CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_TIME > CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e WHERE LENGTH(e.lastName) > 0"); + assertQuery("SELECT e FROM Employee e WHERE LOWER(e.lastName) = 'bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked, 8) > 0"); + assertQuery("SELECT e FROM Employee e WHERE SQRT(o.RESULT) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(TRAILING FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(LEADING '-' FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE UPPER(e.lastName) = 'BILBO'"); + assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE EXTRACT(YEAR FROM e.startDate) = '2023'"); + } + + @Test + void specialOperators() { + + assertQuery("SELECT toDo FROM Employee e JOIN e.toDoList toDo WHERE INDEX(toDo) = 1"); + assertQuery("SELECT p FROM Employee e JOIN e.priorities p WHERE KEY(p) = 'high'"); + assertQuery("SELECT e FROM Employee e WHERE SIZE(e.managedEmployees) < 2"); + assertQuery("SELECT e FROM Employee e WHERE e.managedEmployees IS EMPTY"); + assertQuery("SELECT e FROM Employee e WHERE 'write code' MEMBER OF e.responsibilities"); + assertQuery("SELECT p FROM Project p WHERE TYPE(p) = LargeProject"); + + /** + * NOTE: The following query has been altered to properly align with EclipseLink test code despite NOT matching + * their ref docs. See https://github.com/eclipse-ee4j/eclipselink/issues/1949 for more details. + */ + assertQuery("SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) p WHERE p.budget > 1000000"); + + assertQuery("SELECT p FROM Phone p WHERE FUNCTION('TO_NUMBER', p.areaCode) > 613"); + } + + @Test // GH-3314 + void isNullAndIsNotNull() { + + assertQuery("SELECT e FROM Employee e WHERE (e.active IS null OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NULL OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT null OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)"); + } + + @Test // GH-3496 + void lateralShouldBeAValidParameter() { + + assertQuery("select e from Employee e where e.lateral = :_lateral"); + assertQuery("select te from TestEntity te where te.lateral = :lateral"); } @Test // GH-3136 @@ -93,13 +296,13 @@ void except() { } @ParameterizedTest // GH-3136 - @ValueSource(strings = {"STRING", "INTEGER", "FLOAT", "DOUBLE"}) + @ValueSource(strings = { "STRING", "INTEGER", "FLOAT", "DOUBLE" }) void cast(String targetType) { assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); } @ParameterizedTest // GH-3136 - @ValueSource(strings = {"LEFT", "RIGHT"}) + @ValueSource(strings = { "LEFT", "RIGHT" }) void leftRightStringFunctions(String keyword) { assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java index 289e522455..566bfb8801 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java @@ -327,6 +327,38 @@ OR TREAT(e AS Contractor).hours > 100 """); } + @Test // GH-3136 + void substring() { + + assertQuery("select substring(c.number, 1, 2) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1) " + // + "from Call c"); + } + + @Test // GH-3136 + void currentDateFunctions() { + + assertQuery("select CURRENT_DATE " + // + "from Call c "); + + assertQuery("select CURRENT_TIME " + // + "from Call c "); + + assertQuery("select CURRENT_TIMESTAMP " + // + "from Call c "); + + assertQuery("select LOCAL_DATE " + // + "from Call c "); + + assertQuery("select LOCAL_TIME " + // + "from Call c "); + + assertQuery("select LOCAL_DATETIME " + // + "from Call c "); + } + @Test void pathExpressionsNamedParametersExample() { From 2eaf93e52c040ecc058297b26006d10b1c883cb8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Nov 2024 11:12:06 +0100 Subject: [PATCH 17/94] Fix EQL grammar to accept literals in constructor expressions. Original Pull Request: #3695 --- .../data/jpa/repository/query/Eql.g4 | 1 + .../repository/query/EqlQueryRenderer.java | 14 ++++++------- .../repository/query/HqlQueryRenderer.java | 20 +++++++++---------- .../repository/query/EqlComplianceTests.java | 5 +++++ .../EqlDtoQueryTransformerUnitTests.java | 4 ++-- .../JpqlDtoQueryTransformerUnitTests.java | 4 ++-- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index ff6be74296..6cfbe1db26 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 @@ -215,6 +215,7 @@ constructor_item | scalar_expression | aggregate_expression | identification_variable + | literal ; aggregate_expression diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index e7e768df62..2822ddff76 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -695,19 +695,19 @@ public QueryTokenStream visitConstructor_expression(EqlParser.Constructor_expres @Override public QueryTokenStream visitConstructor_item(EqlParser.Constructor_itemContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.single_valued_path_expression() != null) { - builder.append(visit(ctx.single_valued_path_expression())); + return visit(ctx.single_valued_path_expression()); } else if (ctx.scalar_expression() != null) { - builder.append(visit(ctx.scalar_expression())); + return visit(ctx.scalar_expression()); } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); + return visit(ctx.aggregate_expression()); } else if (ctx.identification_variable() != null) { - builder.append(visit(ctx.identification_variable())); + return visit(ctx.identification_variable()); + } else if (ctx.literal() != null) { + return visit(ctx.literal()); } - return builder; + return QueryTokenStream.empty(); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 36f8a2f62f..2191e2ee94 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -1309,7 +1309,7 @@ public QueryTokenStream visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ct @Override public QueryTokenStream visitGenericTemporalLiteralText(HqlParser.GenericTemporalLiteralTextContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override @@ -1338,12 +1338,12 @@ public QueryTokenStream visitGeneralizedLiteral(HqlParser.GeneralizedLiteralCont @Override public QueryTokenStream visitGeneralizedLiteralType(HqlParser.GeneralizedLiteralTypeContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override public QueryTokenStream visitGeneralizedLiteralText(HqlParser.GeneralizedLiteralTextContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override @@ -1414,37 +1414,37 @@ public QueryTokenStream visitOffsetWithMinutes(HqlParser.OffsetWithMinutesContex @Override public QueryTokenStream visitYear(HqlParser.YearContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } @Override public QueryTokenStream visitMonth(HqlParser.MonthContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } @Override public QueryTokenStream visitDay(HqlParser.DayContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } @Override public QueryTokenStream visitHour(HqlParser.HourContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } @Override public QueryTokenStream visitMinute(HqlParser.MinuteContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } @Override public QueryTokenStream visitSecond(HqlParser.SecondContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } @Override public QueryTokenStream visitZoneId(HqlParser.ZoneIdContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java index 8ae77e5553..9b092c7924 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -129,6 +129,11 @@ void numericLiterals() { assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); } + @Test // GH-3308 + void newWithStrings() { + assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); + } + @Test void orderByClause() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java index a2f1a125fb..b8ac4f35a3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java @@ -63,11 +63,11 @@ void shouldRewriteQueriesWithSubselect() { void shouldNotTranslateConstructorExpressionQuery() { JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("SELECT NEW String(p) from Person p"); + .parseQuery("SELECT NEW Foo(p) from Person p"); QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p"); } @Test diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java index 38c416271f..d0c8fa1305 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java @@ -63,11 +63,11 @@ void shouldRewriteQueriesWithSubselect() { void shouldNotTranslateConstructorExpressionQuery() { JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("SELECT NEW String(p) from Person p"); + .parseQuery("SELECT NEW Foo(p) from Person p"); QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p"); } @Test From 9c8fc91caf285e7d6e393c955497518d055064c0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Nov 2024 14:55:47 +0100 Subject: [PATCH 18/94] Cleanup QueryTokenStream methods. Remove unused methods. Introduce QueryTokenStream.from and QueryTokenStream.ofToken() factory methods. Migrate JPQL visitors to consistently return token streams instead of mixing expression streams when obtaining values from nodes/terminal nodes. Remove also unused concat methods for consistency. We now instead decide on the composition (calling) site whether a token (stream) should be inlined, an expression or used as-is. Original Pull Request: #3695 --- .../repository/query/EqlQueryRenderer.java | 76 +++++----- .../query/EqlSortedQueryTransformer.java | 4 +- .../repository/query/HqlQueryRenderer.java | 130 +++++++++--------- .../query/HqlSortedQueryTransformer.java | 6 +- .../repository/query/JpqlQueryRenderer.java | 78 +++++------ .../query/JpqlSortedQueryTransformer.java | 4 +- .../jpa/repository/query/QueryRenderer.java | 119 +++------------- .../repository/query/QueryTokenStream.java | 63 +++++++-- .../jpa/repository/query/QueryTokens.java | 25 ---- 9 files changed, 219 insertions(+), 286 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 2822ddff76..04626dbb12 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -163,13 +163,8 @@ public QueryTokenStream visitIdentification_variable_declaration( QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(visit(ctx.range_variable_declaration())); - - ctx.join().forEach(joinContext -> { - builder.append(visit(joinContext)); - }); - ctx.fetch_join().forEach(fetchJoinContext -> { - builder.append(visit(fetchJoinContext)); - }); + builder.appendExpression(QueryTokenStream.concat(ctx.join(), this::visit, TOKEN_SPACE)); + builder.appendExpression(QueryTokenStream.concat(ctx.fetch_join(), this::visit, TOKEN_SPACE)); return builder; } @@ -588,7 +583,7 @@ public QueryTokenStream visitNew_value(EqlParser.New_valueContext ctx) { } else if (ctx.simple_entity_expression() != null) { return visit(ctx.simple_entity_expression()); } else if (ctx.NULL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.NULL())); + return QueryTokenStream.ofToken(ctx.NULL()); } else { return QueryRenderer.builder(); } @@ -1495,7 +1490,7 @@ public QueryTokenStream visitRegexpComparison(EqlParser.RegexpComparisonContext @Override public QueryTokenStream visitComparison_operator(EqlParser.Comparison_operatorContext ctx) { - return QueryRenderer.from(QueryTokens.token(ctx.op)); + return QueryTokenStream.ofToken(ctx.op); } @Override @@ -1909,16 +1904,19 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret builder.append(QueryTokens.token(ctx.TRIM())); builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); if (ctx.trim_specification() != null) { - builder.appendExpression(visit(ctx.trim_specification())); + nested.appendExpression(visit(ctx.trim_specification())); } if (ctx.trim_character() != null) { - builder.appendExpression(visit(ctx.trim_character())); + nested.appendExpression(visit(ctx.trim_character())); } if (ctx.FROM() != null) { - builder.append(QueryTokens.expression(ctx.FROM())); + nested.append(QueryTokens.expression(ctx.FROM())); } - builder.append(visit(ctx.string_expression(0))); + nested.append(visit(ctx.string_expression(0))); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.LOWER() != null) { @@ -1964,11 +1962,11 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret public QueryTokenStream visitTrim_specification(EqlParser.Trim_specificationContext ctx) { if (ctx.LEADING() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.LEADING())); + return QueryTokenStream.ofToken(ctx.LEADING()); } else if (ctx.TRAILING() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.TRAILING())); + return QueryTokenStream.ofToken(ctx.TRAILING()); } else { - return QueryRenderer.from(QueryTokens.expression(ctx.BOTH())); + return QueryTokenStream.ofToken(ctx.BOTH()); } } @@ -2197,7 +2195,7 @@ public QueryTokenStream visitNullif_expression(EqlParser.Nullif_expressionContex public QueryTokenStream visitTrim_character(EqlParser.Trim_characterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } else if (ctx.character_valued_input_parameter() != null) { return visit(ctx.character_valued_input_parameter()); } else { @@ -2209,11 +2207,11 @@ public QueryTokenStream visitTrim_character(EqlParser.Trim_characterContext ctx) public QueryTokenStream visitIdentification_variable(EqlParser.Identification_variableContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); + return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); } else if (ctx.type_literal() != null) { return visit(ctx.type_literal()); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.token(ctx.f)); + return QueryTokenStream.ofToken(ctx.f); } else { return QueryTokenStream.empty(); } @@ -2228,15 +2226,15 @@ public QueryTokenStream visitConstructor_name(EqlParser.Constructor_nameContext public QueryTokenStream visitLiteral(EqlParser.LiteralContext ctx) { if (ctx.STRINGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); } else if (ctx.JAVASTRINGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.JAVASTRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.JAVASTRINGLITERAL()); } else if (ctx.INTLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.INTLITERAL())); + return QueryTokenStream.ofToken(ctx.INTLITERAL()); } else if (ctx.FLOATLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.FLOATLITERAL())); + return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); } else if (ctx.LONGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.LONGLITERAL())); + return QueryTokenStream.ofToken(ctx.LONGLITERAL()); } else if (ctx.boolean_literal() != null) { return visit(ctx.boolean_literal()); } else if (ctx.entity_type_literal() != null) { @@ -2278,13 +2276,13 @@ public QueryTokenStream visitPattern_value(EqlParser.Pattern_valueContext ctx) { public QueryTokenStream visitDate_time_timestamp_literal(EqlParser.Date_time_timestamp_literalContext ctx) { if (ctx.STRINGLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); } else if (ctx.DATELITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.DATELITERAL())); + return QueryTokenStream.ofToken(ctx.DATELITERAL()); } else if (ctx.TIMELITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.TIMELITERAL())); + return QueryTokenStream.ofToken(ctx.TIMELITERAL()); } else if (ctx.TIMESTAMPLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.TIMESTAMPLITERAL())); + return QueryTokenStream.ofToken(ctx.TIMESTAMPLITERAL()); } else { return QueryRenderer.builder(); } @@ -2297,18 +2295,18 @@ public QueryTokenStream visitEntity_type_literal(EqlParser.Entity_type_literalCo @Override public QueryTokenStream visitEscape_character(EqlParser.Escape_characterContext ctx) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } @Override public QueryTokenStream visitNumeric_literal(EqlParser.Numeric_literalContext ctx) { if (ctx.INTLITERAL() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.INTLITERAL())); + return QueryTokenStream.ofToken(ctx.INTLITERAL()); } else if (ctx.FLOATLITERAL() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.FLOATLITERAL())); + return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); } else if (ctx.LONGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.LONGLITERAL())); + return QueryTokenStream.ofToken(ctx.LONGLITERAL()); } else { return QueryTokenStream.empty(); } @@ -2318,9 +2316,9 @@ public QueryTokenStream visitNumeric_literal(EqlParser.Numeric_literalContext ct public QueryTokenStream visitBoolean_literal(EqlParser.Boolean_literalContext ctx) { if (ctx.TRUE() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.TRUE())); + return QueryTokenStream.ofToken(ctx.TRUE()); } else if (ctx.FALSE() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.FALSE())); + return QueryTokenStream.ofToken(ctx.FALSE()); } else { return QueryTokenStream.empty(); } @@ -2335,9 +2333,9 @@ public QueryTokenStream visitEnum_literal(EqlParser.Enum_literalContext ctx) { public QueryTokenStream visitString_literal(EqlParser.String_literalContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } else if (ctx.STRINGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); } else { return QueryTokenStream.empty(); } @@ -2430,7 +2428,7 @@ public QueryTokenStream visitFunction_name(EqlParser.Function_nameContext ctx) { public QueryTokenStream visitCharacter_valued_input_parameter(EqlParser.Character_valued_input_parameterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } else if (ctx.input_parameter() != null) { return visit(ctx.input_parameter()); } else { @@ -2441,9 +2439,9 @@ public QueryTokenStream visitCharacter_valued_input_parameter(EqlParser.Characte @Override public QueryTokenStream visitReserved_word(EqlParser.Reserved_wordContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); + return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.token(ctx.f)); + return QueryTokenStream.ofToken(ctx.f); } else { return QueryTokenStream.empty(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java index 04fdc9e7ca..50a3019acc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java @@ -134,7 +134,7 @@ public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) { QueryTokenStream tokens = super.visitSelect_item(ctx); if (ctx.result_variable() != null && !tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; @@ -146,7 +146,7 @@ public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { QueryTokenStream tokens = super.visitJoin(ctx); if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 2191e2ee94..b4cc3f5386 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -107,7 +107,7 @@ public QueryTokenStream visitQueryExpression(HqlParser.QueryExpressionContext ct @Override public QueryTokenStream visitWithClause(HqlParser.WithClauseContext ctx) { - QueryRendererBuilder builder = QueryRendererBuilder.from(TOKEN_WITH); + QueryRendererBuilder builder = QueryRendererBuilder.builder(TOKEN_WITH); builder.append(QueryTokenStream.concatExpressions(ctx.cte(), this::visit, TOKEN_COMMA)); return builder; @@ -668,7 +668,7 @@ public QueryTokenStream visitGroupedItem(HqlParser.GroupedItemContext ctx) { if (ctx.identifier() != null) { return visit(ctx.identifier()); } else if (ctx.INTEGER_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } else if (ctx.expression() != null) { return visit(ctx.expression()); } else { @@ -700,7 +700,7 @@ public QueryTokenStream visitSortExpression(HqlParser.SortExpressionContext ctx) if (ctx.identifier() != null) { return visit(ctx.identifier()); } else if (ctx.INTEGER_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } else if (ctx.expression() != null) { return visit(ctx.expression()); } else { @@ -712,9 +712,9 @@ public QueryTokenStream visitSortExpression(HqlParser.SortExpressionContext ctx) public QueryTokenStream visitSortDirection(HqlParser.SortDirectionContext ctx) { if (ctx.ASC() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.ASC())); + return QueryTokenStream.ofToken(ctx.ASC()); } else if (ctx.DESC() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.DESC())); + return QueryTokenStream.ofToken(ctx.DESC()); } else { return QueryTokenStream.empty(); } @@ -778,9 +778,9 @@ public QueryTokenStream visitFetchClause(HqlParser.FetchClauseContext ctx) { } if (ctx.parameterOrIntegerLiteral() != null) { - builder.append(visit(ctx.parameterOrIntegerLiteral())); + builder.appendExpression(visit(ctx.parameterOrIntegerLiteral())); } else if (ctx.parameterOrNumberLiteral() != null) { - builder.append(visit(ctx.parameterOrNumberLiteral())); + builder.appendExpression(visit(ctx.parameterOrNumberLiteral())); } if (ctx.ROW() != null) { @@ -1029,13 +1029,13 @@ public QueryTokenStream visitSetOperator(HqlParser.SetOperatorContext ctx) { public QueryTokenStream visitLiteral(HqlParser.LiteralContext ctx) { if (ctx.NULL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.NULL())); + return QueryTokenStream.ofToken(ctx.NULL()); } else if (ctx.booleanLiteral() != null) { return visit(ctx.booleanLiteral()); } else if (ctx.JAVA_STRING_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.JAVA_STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.JAVA_STRING_LITERAL()); } else if (ctx.STRING_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } else if (ctx.numericLiteral() != null) { return visit(ctx.numericLiteral()); } else if (ctx.temporalLiteral() != null) { @@ -1055,9 +1055,9 @@ public QueryTokenStream visitLiteral(HqlParser.LiteralContext ctx) { public QueryTokenStream visitBooleanLiteral(HqlParser.BooleanLiteralContext ctx) { if (ctx.TRUE() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.TRUE())); + return QueryTokenStream.ofToken(ctx.TRUE()); } else if (ctx.FALSE() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.FALSE())); + return QueryTokenStream.ofToken(ctx.FALSE()); } else { return QueryTokenStream.empty(); } @@ -1067,19 +1067,19 @@ public QueryTokenStream visitBooleanLiteral(HqlParser.BooleanLiteralContext ctx) public QueryTokenStream visitNumericLiteral(HqlParser.NumericLiteralContext ctx) { if (ctx.INTEGER_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } else if (ctx.LONG_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.LONG_LITERAL())); + return QueryTokenStream.ofToken(ctx.LONG_LITERAL()); } else if (ctx.BIG_INTEGER_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.BIG_INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.BIG_INTEGER_LITERAL()); } else if (ctx.FLOAT_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.FLOAT_LITERAL())); + return QueryTokenStream.ofToken(ctx.FLOAT_LITERAL()); } else if (ctx.DOUBLE_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.DOUBLE_LITERAL())); + return QueryTokenStream.ofToken(ctx.DOUBLE_LITERAL()); } else if (ctx.BIG_DECIMAL_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.BIG_DECIMAL_LITERAL())); + return QueryTokenStream.ofToken(ctx.BIG_DECIMAL_LITERAL()); } else if (ctx.HEX_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.HEX_LITERAL())); + return QueryTokenStream.ofToken(ctx.HEX_LITERAL()); } else { return QueryTokenStream.empty(); } @@ -1451,25 +1451,25 @@ public QueryTokenStream visitZoneId(HqlParser.ZoneIdContext ctx) { public QueryTokenStream visitDatetimeField(HqlParser.DatetimeFieldContext ctx) { if (ctx.YEAR() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.YEAR())); + return QueryTokenStream.ofToken(ctx.YEAR()); } else if (ctx.MONTH() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.MONTH())); + return QueryTokenStream.ofToken(ctx.MONTH()); } else if (ctx.DAY() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.DAY())); + return QueryTokenStream.ofToken(ctx.DAY()); } else if (ctx.WEEK() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.WEEK())); + return QueryTokenStream.ofToken(ctx.WEEK()); } else if (ctx.QUARTER() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.QUARTER())); + return QueryTokenStream.ofToken(ctx.QUARTER()); } else if (ctx.HOUR() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.HOUR())); + return QueryTokenStream.ofToken(ctx.HOUR()); } else if (ctx.MINUTE() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.MINUTE())); + return QueryTokenStream.ofToken(ctx.MINUTE()); } else if (ctx.SECOND() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.SECOND())); + return QueryTokenStream.ofToken(ctx.SECOND()); } else if (ctx.NANOSECOND() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.NANOSECOND())); + return QueryTokenStream.ofToken(ctx.NANOSECOND()); } else if (ctx.EPOCH() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.EPOCH())); + return QueryTokenStream.ofToken(ctx.EPOCH()); } else { return QueryTokenStream.empty(); } @@ -1547,7 +1547,7 @@ public QueryTokenStream visitTimeZoneField(HqlParser.TimeZoneFieldContext ctx) { @Override public QueryTokenStream visitDateOrTimeField(HqlParser.DateOrTimeFieldContext ctx) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.DATE() != null ? ctx.DATE() : ctx.TIME())); + return QueryTokenStream.ofToken(ctx.DATE() != null ? ctx.DATE() : ctx.TIME()); } @Override @@ -1560,11 +1560,7 @@ public QueryTokenStream visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) { } else if (ctx.HEX_LITERAL() != null) { builder.append(TOKEN_OPEN_BRACE); - - builder.append(QueryTokenStream.concat(ctx.HEX_LITERAL(), it -> { - return QueryRendererBuilder.from(QueryTokens.token(it)); - }, TOKEN_COMMA)); - + builder.append(QueryTokenStream.concat(ctx.HEX_LITERAL(), QueryTokenStream::ofToken, TOKEN_COMMA)); builder.append(TOKEN_CLOSE_BRACE); } @@ -1741,7 +1737,7 @@ public QueryTokenStream visitToDurationExpression(HqlParser.ToDurationExpression QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.expression())); + builder.appendExpression(visit(ctx.expression())); builder.appendExpression(visit(ctx.datetimeField())); return builder; @@ -2163,12 +2159,12 @@ public QueryTokenStream visitPadFunction(HqlParser.PadFunctionContext ctx) { @Override public QueryTokenStream visitPadSpecification(HqlParser.PadSpecificationContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.LEADING() != null ? ctx.LEADING() : ctx.TRAILING())); + return QueryTokenStream.ofToken(ctx.LEADING() != null ? ctx.LEADING() : ctx.TRAILING()); } @Override public QueryTokenStream visitPadCharacter(HqlParser.PadCharacterContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override @@ -2180,12 +2176,16 @@ public QueryTokenStream visitPadLength(HqlParser.PadLengthContext ctx) { public QueryTokenStream visitPositionFunction(HqlParser.PositionFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); builder.append(QueryTokens.token(ctx.POSITION())); builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.positionFunctionPatternArgument())); - builder.append(QueryTokens.expression(ctx.IN())); - builder.appendInline(visit(ctx.positionFunctionStringArgument())); + + nested.appendExpression(visit(ctx.positionFunctionPatternArgument())); + nested.append(QueryTokens.expression(ctx.IN())); + nested.append(visit(ctx.positionFunctionStringArgument())); + + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); return builder; @@ -2208,7 +2208,7 @@ public QueryTokenStream visitOverlayFunction(HqlParser.OverlayFunctionContext ct builder.append(QueryTokens.token(ctx.OVERLAY())); builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.overlayFunctionStringArgument())); + builder.appendExpression(visit(ctx.overlayFunctionStringArgument())); builder.append(QueryTokens.expression(ctx.PLACING())); builder.append(visit(ctx.overlayFunctionReplacementArgument())); builder.append(QueryTokens.expression(ctx.FROM())); @@ -2442,7 +2442,7 @@ public QueryTokenStream visitRollup(HqlParser.RollupContext ctx) { @Override public QueryTokenStream visitFormat(HqlParser.FormatContext ctx) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override @@ -2504,7 +2504,7 @@ public QueryTokenStream visitJpaNonstandardFunctionName(HqlParser.JpaNonstandard return visit(ctx.identifier()); } - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } @Override @@ -3003,10 +3003,7 @@ public QueryTokenStream visitSimpleCaseExpression(HqlParser.SimpleCaseExpression builder.append(QueryTokens.expression(ctx.CASE())); builder.append(visit(ctx.expressionOrPredicate(0))); - - ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> { - builder.append(visit(caseWhenExpressionClauseContext)); - }); + builder.appendExpression(QueryTokenStream.concat(ctx.caseWhenExpressionClause(), this::visit, TOKEN_SPACE)); if (ctx.ELSE() != null) { @@ -3249,7 +3246,7 @@ public QueryTokenStream visitCastTarget(HqlParser.CastTargetContext ctx) { @Override public QueryTokenStream visitCastTargetType(HqlParser.CastTargetTypeContext ctx) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.fullTargetName)); + return QueryTokenStream.from(QueryTokens.token(ctx.fullTargetName)); } @Override @@ -3266,7 +3263,7 @@ public QueryTokenStream visitExtractFunction(HqlParser.ExtractFunctionContext ct nested.appendExpression(visit(ctx.extractField())); nested.append(QueryTokens.expression(ctx.FROM())); - nested.append(visit(ctx.expression())); + nested.appendExpression(visit(ctx.expression())); builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); @@ -3356,7 +3353,7 @@ public QueryTokenStream visitTrimSpecification(HqlParser.TrimSpecificationContex public QueryTokenStream visitTrimCharacter(HqlParser.TrimCharacterContext ctx) { if (ctx.STRING_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.STRING_LITERAL())); + return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); } return visit(ctx.parameter()); @@ -3506,11 +3503,11 @@ public QueryTokenStream visitToOneFkReference(HqlParser.ToOneFkReferenceContext public QueryTokenStream visitElementValueQuantifier(HqlParser.ElementValueQuantifierContext ctx) { if (ctx.ELEMENT() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.ELEMENT())); + return QueryTokenStream.ofToken(ctx.ELEMENT()); } if (ctx.VALUE() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.VALUE())); + return QueryTokenStream.ofToken(ctx.VALUE()); } return QueryTokenStream.empty(); @@ -3520,11 +3517,11 @@ public QueryTokenStream visitElementValueQuantifier(HqlParser.ElementValueQuanti public QueryTokenStream visitIndexKeyQuantifier(HqlParser.IndexKeyQuantifierContext ctx) { if (ctx.INDEX() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INDEX())); + return QueryTokenStream.ofToken(ctx.INDEX()); } if (ctx.KEY() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.KEY())); + return QueryTokenStream.ofToken(ctx.KEY()); } return QueryTokenStream.empty(); @@ -3868,11 +3865,10 @@ public QueryTokenStream visitExistsExpression(HqlParser.ExistsExpressionContext public QueryTokenStream visitInstantiationTarget(HqlParser.InstantiationTargetContext ctx) { if (ctx.LIST() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.LIST())); + return QueryTokenStream.ofToken(ctx.LIST()); } else if (ctx.MAP() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.MAP())); + return QueryTokenStream.ofToken(ctx.MAP()); } else if (ctx.simplePath() != null) { - return visit(ctx.simplePath()); } else { return QueryTokenStream.empty(); @@ -3908,7 +3904,7 @@ public QueryTokenStream visitParameterOrIntegerLiteral(HqlParser.ParameterOrInte if (ctx.parameter() != null) { return visit(ctx.parameter()); } else if (ctx.INTEGER_LITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INTEGER_LITERAL())); + return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); } else { return QueryTokenStream.empty(); } @@ -3974,15 +3970,15 @@ public QueryTokenStream visitIdentifier(HqlParser.IdentifierContext ctx) { if (ctx.nakedIdentifier() != null) { return visit(ctx.nakedIdentifier()); } else if (ctx.FULL() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.FULL())); + return QueryTokenStream.ofToken(ctx.FULL()); } else if (ctx.LEFT() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.LEFT())); + return QueryTokenStream.ofToken(ctx.LEFT()); } else if (ctx.INNER() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.INNER())); + return QueryTokenStream.ofToken(ctx.INNER()); } else if (ctx.OUTER() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.OUTER())); + return QueryTokenStream.ofToken(ctx.OUTER()); } else if (ctx.RIGHT() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.RIGHT())); + return QueryTokenStream.ofToken(ctx.RIGHT()); } return QueryTokenStream.empty(); @@ -3992,11 +3988,11 @@ public QueryTokenStream visitIdentifier(HqlParser.IdentifierContext ctx) { public QueryTokenStream visitNakedIdentifier(HqlParser.NakedIdentifierContext ctx) { if (ctx.IDENTIFIER() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.IDENTIFIER())); + return QueryTokenStream.ofToken(ctx.IDENTIFIER()); } else if (ctx.QUOTED_IDENTIFIER() != null) { - return QueryRendererBuilder.from(QueryTokens.token(ctx.QUOTED_IDENTIFIER())); + return QueryTokenStream.ofToken(ctx.QUOTED_IDENTIFIER()); } else { - return QueryRendererBuilder.from(QueryTokens.token(ctx.f)); + return QueryTokenStream.ofToken(ctx.f); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java index b5784b31dc..202a2107b4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java @@ -104,7 +104,7 @@ public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { QueryTokenStream tokens = super.visitJoinPath(ctx); if (ctx.variable() != null && !isSubquery(ctx)) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; @@ -116,7 +116,7 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { QueryTokenStream tokens = super.visitJoinSubquery(ctx); if (ctx.variable() != null && !tokens.isEmpty() && !isSubquery(ctx)) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; @@ -128,7 +128,7 @@ public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) { QueryTokenStream tokens = super.visitVariable(ctx); if (ctx.identifier() != null && !tokens.isEmpty() && !isSubquery(ctx)) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index 892f37bff9..762bfab02a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -153,15 +153,10 @@ public QueryTokenStream visitIdentification_variable_declaration( JpqlParser.Identification_variable_declarationContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.range_variable_declaration())); - ctx.join().forEach(joinContext -> { - builder.append(visit(joinContext)); - }); - - ctx.fetch_join().forEach(fetchJoinContext -> { - builder.append(visit(fetchJoinContext)); - }); + builder.append(visit(ctx.range_variable_declaration())); + builder.appendExpression(QueryTokenStream.concat(ctx.join(), this::visit, TOKEN_SPACE)); + builder.appendExpression(QueryTokenStream.concat(ctx.fetch_join(), this::visit, TOKEN_SPACE)); return builder; } @@ -571,7 +566,7 @@ public QueryTokenStream visitNew_value(JpqlParser.New_valueContext ctx) { } else if (ctx.simple_entity_expression() != null) { return visit(ctx.simple_entity_expression()); } else if (ctx.NULL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.NULL())); + return QueryTokenStream.ofToken(ctx.NULL()); } else { return QueryTokenStream.empty(); } @@ -1411,7 +1406,7 @@ public QueryTokenStream visitComparison_expression(JpqlParser.Comparison_express @Override public QueryTokenStream visitComparison_operator(JpqlParser.Comparison_operatorContext ctx) { - return QueryRenderer.from(QueryTokens.token(ctx.op)); + return QueryTokenStream.ofToken(ctx.op); } @Override @@ -1827,16 +1822,19 @@ public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_re builder.append(QueryTokens.token(ctx.TRIM())); builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); if (ctx.trim_specification() != null) { - builder.appendExpression(visit(ctx.trim_specification())); + nested.appendExpression(visit(ctx.trim_specification())); } if (ctx.trim_character() != null) { - builder.appendExpression(visit(ctx.trim_character())); + nested.appendExpression(visit(ctx.trim_character())); } if (ctx.FROM() != null) { - builder.append(QueryTokens.expression(ctx.FROM())); + nested.append(QueryTokens.expression(ctx.FROM())); } - builder.append(visit(ctx.string_expression(0))); + nested.append(visit(ctx.string_expression(0))); + builder.appendInline(nested); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.LOWER() != null) { @@ -1882,11 +1880,11 @@ public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_re public QueryTokenStream visitTrim_specification(JpqlParser.Trim_specificationContext ctx) { if (ctx.LEADING() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.LEADING())); + return QueryTokenStream.ofToken(ctx.LEADING()); } else if (ctx.TRAILING() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.TRAILING())); + return QueryTokenStream.ofToken(ctx.TRAILING()); } else { - return QueryRenderer.from(QueryTokens.expression(ctx.BOTH())); + return QueryTokenStream.ofToken(ctx.BOTH()); } } @@ -2110,7 +2108,7 @@ public QueryTokenStream visitNullif_expression(JpqlParser.Nullif_expressionConte public QueryTokenStream visitTrim_character(JpqlParser.Trim_characterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } else if (ctx.character_valued_input_parameter() != null) { return visit(ctx.character_valued_input_parameter()); } else { @@ -2122,11 +2120,11 @@ public QueryTokenStream visitTrim_character(JpqlParser.Trim_characterContext ctx public QueryTokenStream visitIdentification_variable(JpqlParser.Identification_variableContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); + return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); } else if (ctx.type_literal() != null) { return visit(ctx.type_literal()); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.token(ctx.f)); + return QueryTokenStream.ofToken(ctx.f); } else { return QueryTokenStream.empty(); } @@ -2141,15 +2139,15 @@ public QueryTokenStream visitConstructor_name(JpqlParser.Constructor_nameContext public QueryTokenStream visitLiteral(JpqlParser.LiteralContext ctx) { if (ctx.STRINGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); } else if (ctx.JAVASTRINGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.JAVASTRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.JAVASTRINGLITERAL()); } else if (ctx.INTLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.INTLITERAL())); + return QueryTokenStream.ofToken(ctx.INTLITERAL()); } else if (ctx.FLOATLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.FLOATLITERAL())); + return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); } else if (ctx.LONGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.LONGLITERAL())); + return QueryTokenStream.ofToken(ctx.LONGLITERAL()); } else if (ctx.boolean_literal() != null) { return visit(ctx.boolean_literal()); } else if (ctx.entity_type_literal() != null) { @@ -2186,13 +2184,13 @@ public QueryTokenStream visitPattern_value(JpqlParser.Pattern_valueContext ctx) public QueryTokenStream visitDate_time_timestamp_literal(JpqlParser.Date_time_timestamp_literalContext ctx) { if (ctx.STRINGLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); } else if (ctx.DATELITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.DATELITERAL())); + return QueryTokenStream.ofToken(ctx.DATELITERAL()); } else if (ctx.TIMELITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.TIMELITERAL())); + return QueryTokenStream.ofToken(ctx.TIMELITERAL()); } else if (ctx.TIMESTAMPLITERAL() != null) { - return QueryRendererBuilder.from(QueryTokens.expression(ctx.TIMESTAMPLITERAL())); + return QueryTokenStream.ofToken(ctx.TIMESTAMPLITERAL()); } else { return QueryRenderer.builder(); } @@ -2205,18 +2203,18 @@ public QueryTokenStream visitEntity_type_literal(JpqlParser.Entity_type_literalC @Override public QueryTokenStream visitEscape_character(JpqlParser.Escape_characterContext ctx) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } @Override public QueryTokenStream visitNumeric_literal(JpqlParser.Numeric_literalContext ctx) { if (ctx.INTLITERAL() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.INTLITERAL())); + return QueryTokenStream.ofToken(ctx.INTLITERAL()); } else if (ctx.FLOATLITERAL() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.FLOATLITERAL())); + return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); } else if (ctx.LONGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.LONGLITERAL())); + return QueryTokenStream.ofToken(ctx.LONGLITERAL()); } else { return QueryTokenStream.empty(); } @@ -2226,9 +2224,9 @@ public QueryTokenStream visitNumeric_literal(JpqlParser.Numeric_literalContext c public QueryTokenStream visitBoolean_literal(JpqlParser.Boolean_literalContext ctx) { if (ctx.TRUE() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.TRUE())); + return QueryTokenStream.ofToken(ctx.TRUE()); } else if (ctx.FALSE() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.FALSE())); + return QueryTokenStream.ofToken(ctx.FALSE()); } else { return QueryTokenStream.empty(); } @@ -2243,9 +2241,9 @@ public QueryTokenStream visitEnum_literal(JpqlParser.Enum_literalContext ctx) { public QueryTokenStream visitString_literal(JpqlParser.String_literalContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } else if (ctx.STRINGLITERAL() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.STRINGLITERAL())); + return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); } else { return QueryTokenStream.empty(); } @@ -2334,7 +2332,7 @@ public QueryTokenStream visitCharacter_valued_input_parameter( JpqlParser.Character_valued_input_parameterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.CHARACTER())); + return QueryTokenStream.ofToken(ctx.CHARACTER()); } else if (ctx.input_parameter() != null) { return visit(ctx.input_parameter()); } else { @@ -2345,9 +2343,9 @@ public QueryTokenStream visitCharacter_valued_input_parameter( @Override public QueryTokenStream visitReserved_word(Reserved_wordContext ctx) { if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.IDENTIFICATION_VARIABLE())); + return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.token(ctx.f)); + return QueryTokenStream.ofToken(ctx.f); } else { return QueryTokenStream.empty(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index 3cef78794b..0b6a610614 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -133,7 +133,7 @@ public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { QueryTokenStream tokens = super.visitSelect_item(ctx); if (ctx.result_variable() != null && !tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; @@ -145,7 +145,7 @@ public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { QueryTokenStream tokens = super.visitJoin(ctx); if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); + transformerSupport.registerAlias(tokens.getRequiredLast()); } return tokens; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java index 3039ef735a..b7f0b45123 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java @@ -20,9 +20,9 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.function.Function; import java.util.stream.Stream; +import org.springframework.lang.Nullable; import org.springframework.util.CompositeIterator; /** @@ -44,9 +44,6 @@ abstract class QueryRenderer implements QueryTokenStream { /** * Creates a QueryRenderer from a {@link QueryToken}. - * - * @param token - * @return */ static QueryRenderer from(QueryToken token) { return QueryRenderer.from(Collections.singletonList(token)); @@ -54,9 +51,6 @@ static QueryRenderer from(QueryToken token) { /** * Creates a QueryRenderer from a collection of {@link QueryToken}. - * - * @param tokens - * @return */ static QueryRenderer from(Collection tokens) { List tokensToUse = new ArrayList<>(Math.max(tokens.size(), 32)); @@ -66,9 +60,6 @@ static QueryRenderer from(Collection tokens) { /** * Creates a QueryRenderer from a {@link QueryTokenStream}. - * - * @param tokens - * @return */ static QueryRenderer from(QueryTokenStream tokens) { @@ -85,8 +76,6 @@ static QueryRenderer from(QueryTokenStream tokens) { /** * Creates a new empty {@link QueryRenderer}. - * - * @return */ public static QueryRenderer empty() { return EmptyQueryRenderer.INSTANCE; @@ -94,8 +83,6 @@ public static QueryRenderer empty() { /** * Creates a new {@link QueryRendererBuilder}. - * - * @return */ static QueryRendererBuilder builder() { return new QueryRendererBuilder(); @@ -144,14 +131,11 @@ static String render(Iterable tokenStream) { results.append(token.value()); } - return results.toString(); + return results != null ? results.toString() : ""; } /** * Append a {@link QueryRenderer} to create a composed renderer. - * - * @param tokens - * @return */ QueryRenderer append(QueryTokenStream tokens) { @@ -180,7 +164,7 @@ public String toString() { return render(); } - public static QueryRenderer expression(QueryTokenStream tokenStream) { + public static QueryRenderer ofExpression(QueryTokenStream tokenStream) { if (tokenStream instanceof QueryRendererBuilder builder) { tokenStream = builder.current; @@ -258,9 +242,6 @@ String render() { /** * Append a {@link QueryRenderer} to create a composed renderer. - * - * @param tokens - * @return */ QueryRenderer append(QueryTokenStream tokens) { @@ -290,6 +271,7 @@ QueryRenderer append(QueryTokenStream tokens) { } @Override + @Nullable public QueryToken getLast() { for (int i = nested.size() - 1; i > -1; i--) { @@ -386,11 +368,13 @@ public List toList() { } @Override + @Nullable public QueryToken getFirst() { return tokens.isEmpty() ? null : tokens.get(0); } @Override + @Nullable public QueryToken getLast() { return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); } @@ -407,7 +391,7 @@ public boolean isEmpty() { @Override public boolean isExpression() { - return !tokens.isEmpty() && getLast().isExpression(); + return !tokens.isEmpty() && getRequiredLast().isExpression(); } /** @@ -418,7 +402,7 @@ public boolean isExpression() { */ static String render(Object tokens) { - if (tokens instanceof Collection tpr) { + if (tokens instanceof Collection tpr) { return render(tpr); } @@ -454,11 +438,13 @@ public Iterator iterator() { } @Override + @Nullable public QueryToken getFirst() { return tokens.getFirst(); } @Override + @Nullable public QueryToken getLast() { return tokens.getLast(); } @@ -475,7 +461,7 @@ public boolean isEmpty() { @Override public boolean isExpression() { - return !tokens.isEmpty() && tokens.getLast().isExpression(); + return !tokens.isEmpty() && tokens.getRequiredLast().isExpression(); } } @@ -486,68 +472,13 @@ static class QueryRendererBuilder implements QueryTokenStream { protected QueryRenderer current = QueryRenderer.empty(); - /** - * Compose a {@link QueryRendererBuilder} from a collection of inline elements that can be mapped to - * {@link QueryRendererBuilder}. - * - * @param elements - * @param visitor - * @param separator - * @return - * @param - */ - public static QueryRendererBuilder concat(Collection elements, Function visitor, - QueryToken separator) { - return concat(elements, visitor, QueryRendererBuilder::toInline, separator); - } - - /** - * Compose a {@link QueryRendererBuilder} from a collection of expression elements that can be mapped to - * {@link QueryRendererBuilder}. - * - * @param elements - * @param visitor - * @param separator - * @return - * @param - */ - public static QueryRendererBuilder concatExpressions(Collection elements, - Function visitor, QueryToken separator) { - return concat(elements, visitor, QueryRendererBuilder::toExpression, separator); - } - - /** - * Compose a {@link QueryRendererBuilder} from a collection of elements that can be mapped to - * {@link QueryRendererBuilder}. - * - * @param elements - * @param visitor - * @param postProcess post-processing function to convert {@link QueryRendererBuilder} into {@link QueryRenderer}. - * @param separator - * @return - * @param - */ - public static QueryRendererBuilder concat(Collection elements, Function visitor, - Function postProcess, QueryToken separator) { - - QueryRendererBuilder builder = new QueryRendererBuilder(); - for (T element : elements) { - if (!builder.isEmpty()) { - builder.append(separator); - } - builder.append(postProcess.apply(visitor.apply(element))); - } - - return builder; - } - /** * Create and initialize a QueryRendererBuilder from a {@link QueryTokens.SimpleQueryToken}. * * @param token * @return */ - public static QueryRendererBuilder from(QueryToken token) { + public static QueryRendererBuilder builder(QueryToken token) { return new QueryRendererBuilder().append(token); } @@ -627,7 +558,7 @@ QueryRendererBuilder appendExpression(QueryTokenStream tokens) { return this; } - current = current.append(QueryRenderer.expression(tokens)); + current = current.append(QueryRenderer.ofExpression(tokens)); return this; } @@ -643,11 +574,13 @@ public Stream stream() { } @Override + @Nullable public QueryToken getFirst() { return current.getFirst(); } @Override + @Nullable public QueryToken getLast() { return current.getLast(); } @@ -657,11 +590,6 @@ public boolean isExpression() { return current.isExpression(); } - /** - * Return whet the builder is empty. - * - * @return - */ @Override public boolean isEmpty() { return current.isEmpty(); @@ -686,19 +614,6 @@ public QueryRenderer build() { return current; } - QueryRenderer toExpression() { - - if (current instanceof ExpressionRenderer) { - return current; - } - - return QueryRenderer.expression(current); - } - - public QueryRenderer toInline() { - return new InlineRenderer(current); - } - } private static class InlineRenderer extends QueryRenderer { @@ -730,11 +645,13 @@ public Iterator iterator() { } @Override + @Nullable public QueryToken getFirst() { return delegate.getFirst(); } @Override + @Nullable public QueryToken getLast() { return delegate.getLast(); } @@ -784,11 +701,13 @@ public Iterator iterator() { } @Override + @Nullable public QueryToken getFirst() { return delegate.getFirst(); } @Override + @Nullable public QueryToken getLast() { return delegate.getLast(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java index c91fddb0e4..979b336528 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java @@ -16,10 +16,14 @@ package org.springframework.data.jpa.repository.query; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.function.Function; -import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.TerminalNode; + import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -35,13 +39,32 @@ interface QueryTokenStream extends Streamable { /** * Creates an empty stream. - * - * @return */ static QueryTokenStream empty() { return EmptyQueryTokenStream.INSTANCE; } + /** + * Creates a QueryTokenStream from a {@link QueryToken}. + */ + static QueryTokenStream from(QueryToken token) { + return QueryRenderer.from(Collections.singletonList(token)); + } + + /** + * Creates an token QueryRenderer from an AST {@link TerminalNode}. + */ + static QueryTokenStream ofToken(TerminalNode node) { + return from(QueryTokens.token(node)); + } + + /** + * Creates an token QueryRenderer from an AST {@link Token}. + */ + static QueryTokenStream ofToken(Token node) { + return from(QueryTokens.token(node)); + } + /** * Compose a {@link QueryTokenStream} from a collection of inline elements. * @@ -55,10 +78,6 @@ static QueryTokenStream concat(Collection elements, Function QueryTokenStream justAs(Collection elements, Function converter) { - return concat(elements, it-> QueryRendererBuilder.from(converter.apply(it)), QueryRenderer::inline, QueryTokens.TOKEN_SPACE); - } - /** * Compose a {@link QueryTokenStream} from a collection of expression elements. * @@ -69,7 +88,7 @@ static QueryTokenStream justAs(Collection elements, Function QueryTokenStream concatExpressions(Collection elements, Function visitor, QueryToken separator) { - return concat(elements, visitor, QueryRenderer::expression, separator); + return concat(elements, visitor, QueryRenderer::ofExpression, separator); } /** @@ -127,6 +146,20 @@ default QueryToken getFirst() { return it.hasNext() ? it.next() : null; } + /** + * @return the required first query token or throw {@link java.util.NoSuchElementException} if empty. + */ + default QueryToken getRequiredFirst() { + + QueryToken first = getFirst(); + + if (first == null) { + throw new NoSuchElementException("No token in the stream"); + } + + return first; + } + /** * @return the last query token or {@code null} if empty. */ @@ -135,6 +168,20 @@ default QueryToken getLast() { return CollectionUtils.lastElement(toList()); } + /** + * @return the required last query token or throw {@link java.util.NoSuchElementException} if empty. + */ + default QueryToken getRequiredLast() { + + QueryToken last = getLast(); + + if (last == null) { + throw new NoSuchElementException("No token in the stream"); + } + + return last; + } + /** * @return {@code true} if this stream represents a query expression. */ diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java index ea95343d42..33ff1bc5ed 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java @@ -31,7 +31,6 @@ class QueryTokens { /** * Commonly use tokens. */ - static final QueryToken TOKEN_NONE = token(""); static final QueryToken TOKEN_COMMA = token(", "); static final QueryToken TOKEN_SPACE = token(" "); static final QueryToken TOKEN_DOT = token("."); @@ -58,15 +57,9 @@ class QueryTokens { static final QueryToken TOKEN_WITH = expression("WITH"); static final QueryToken TOKEN_NOT = expression("NOT"); static final QueryToken TOKEN_MATERIALIZED = expression("materialized"); - static final QueryToken TOKEN_NULLS = expression("NULLS"); - static final QueryToken TOKEN_FIRST = expression("FIRST"); - static final QueryToken TOKEN_LAST = expression("LAST"); /** * Creates a {@link QueryToken token} from an ANTLR {@link TerminalNode}. - * - * @param node - * @return */ static QueryToken token(TerminalNode node) { return token(node.getText()); @@ -74,9 +67,6 @@ static QueryToken token(TerminalNode node) { /** * Creates a {@link QueryToken token} from an ANTLR {@link Token}. - * - * @param token - * @return */ static QueryToken token(Token token) { return token(token.getText()); @@ -84,9 +74,6 @@ static QueryToken token(Token token) { /** * Creates a {@link QueryToken token} from a string {@code token}. - * - * @param token - * @return */ static QueryToken token(String token) { return new SimpleQueryToken(token); @@ -94,9 +81,6 @@ static QueryToken token(String token) { /** * Creates a ventilated token that is embedded in spaces. - * - * @param token - * @return */ static QueryToken ventilated(Token token) { return new SimpleQueryToken(" " + token.getText() + " "); @@ -104,9 +88,6 @@ static QueryToken ventilated(Token token) { /** * Creates a {@link QueryToken expression} from an ANTLR {@link TerminalNode}. - * - * @param node - * @return */ static QueryToken expression(TerminalNode node) { return expression(node.getText()); @@ -114,9 +95,6 @@ static QueryToken expression(TerminalNode node) { /** * Creates a {@link QueryToken expression} from an ANTLR {@link Token}. - * - * @param token - * @return */ static QueryToken expression(Token token) { return expression(token.getText()); @@ -124,9 +102,6 @@ static QueryToken expression(Token token) { /** * Creates a {@link QueryToken token} from a string {@code expression}. - * - * @param expression - * @return */ static QueryToken expression(String expression) { return new ExpressionToken(expression); From e442ed8b8ba0494607addc3f5e96b633b5879f51 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 9 Dec 2024 14:34:01 +0100 Subject: [PATCH 19/94] Switch to `Query.getSingleResultOrNull()`. We now use getSingleResultOrNull() to avoid NoResultException handling. Closes: #3701 Original Pull Request: #3695 --- .../repository/query/JpaQueryExecution.java | 11 ++--------- .../support/SimpleJpaRepository.java | 19 +++++-------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 35a680c8fe..1fca772ed7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java @@ -16,7 +16,6 @@ package org.springframework.data.jpa.repository.query; import jakarta.persistence.EntityManager; -import jakarta.persistence.NoResultException; import jakarta.persistence.Query; import jakarta.persistence.StoredProcedureQuery; @@ -87,13 +86,7 @@ public Object execute(AbstractJpaQuery query, JpaParametersParameterAccessor acc Assert.notNull(query, "AbstractJpaQuery must not be null"); Assert.notNull(accessor, "JpaParametersParameterAccessor must not be null"); - Object result; - - try { - result = doExecute(query, accessor); - } catch (NoResultException e) { - return null; - } + Object result = doExecute(query, accessor); if (result == null) { return null; @@ -221,7 +214,7 @@ static class SingleEntityExecution extends JpaQueryExecution { @Override protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { - return query.createQuery(accessor).getSingleResult(); + return query.createQuery(accessor).getSingleResultOrNull(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 9f649069c2..25ffed505b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -19,7 +19,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.LockModeType; -import jakarta.persistence.NoResultException; import jakarta.persistence.Parameter; import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; @@ -442,12 +441,7 @@ public Page findAll(Pageable pageable) { @Override public Optional findOne(Specification spec) { - - try { - return Optional.of(getQuery(spec, Sort.unsorted()).setMaxResults(2).getSingleResult()); - } catch (NoResultException e) { - return Optional.empty(); - } + return Optional.ofNullable(getQuery(spec, Sort.unsorted()).setMaxResults(2).getSingleResultOrNull()); } @Override @@ -564,13 +558,10 @@ private R doFindBy(Specification spec, Class domainClass, @Override public Optional findOne(Example example) { - try { - return Optional - .of(getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), Sort.unsorted()) - .setMaxResults(2).getSingleResult()); - } catch (NoResultException e) { - return Optional.empty(); - } + TypedQuery query = getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), + Sort.unsorted()).setMaxResults(2); + + return Optional.ofNullable(query.getSingleResultOrNull()); } @Override From 2f642256952bdb102bbce965e0ceeee8be074f89 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Dec 2024 09:39:33 +0100 Subject: [PATCH 20/94] Move off deprecated TemporalType.TIMESTAMP Favour java.time types for auditing by switching from Date to Instant. See: #3673 Original Pull Request: #3695 --- .../data/jpa/domain/AbstractAuditable.java | 18 +++++++----------- .../data/jpa/repository/Temporal.java | 2 ++ .../jpa/repository/query/JpaParameters.java | 2 ++ ...AuditingViaJavaConfigRepositoriesTests.java | 7 ++++--- .../data/jpa/util/FixedDate.java | 11 ++++++----- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java index c2653a2e89..21637a9be9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java @@ -17,13 +17,11 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.MappedSuperclass; -import jakarta.persistence.Temporal; -import jakarta.persistence.TemporalType; import java.io.Serializable; +import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.Date; import java.util.Optional; import org.springframework.data.domain.Auditable; @@ -45,14 +43,12 @@ public abstract class AbstractAuditable extends Abst @ManyToOne // private @Nullable U createdBy; - @Temporal(TemporalType.TIMESTAMP) // - private @Nullable Date createdDate; + private @Nullable Instant createdDate; @ManyToOne // private @Nullable U lastModifiedBy; - @Temporal(TemporalType.TIMESTAMP) // - private @Nullable Date lastModifiedDate; + private @Nullable Instant lastModifiedDate; @Override public Optional getCreatedBy() { @@ -67,12 +63,12 @@ public void setCreatedBy(U createdBy) { @Override public Optional getCreatedDate() { return null == createdDate ? Optional.empty() - : Optional.of(LocalDateTime.ofInstant(createdDate.toInstant(), ZoneId.systemDefault())); + : Optional.of(LocalDateTime.ofInstant(createdDate, ZoneId.systemDefault())); } @Override public void setCreatedDate(LocalDateTime createdDate) { - this.createdDate = Date.from(createdDate.atZone(ZoneId.systemDefault()).toInstant()); + this.createdDate = createdDate.atZone(ZoneId.systemDefault()).toInstant(); } @Override @@ -88,11 +84,11 @@ public void setLastModifiedBy(U lastModifiedBy) { @Override public Optional getLastModifiedDate() { return null == lastModifiedDate ? Optional.empty() - : Optional.of(LocalDateTime.ofInstant(lastModifiedDate.toInstant(), ZoneId.systemDefault())); + : Optional.of(LocalDateTime.ofInstant(lastModifiedDate, ZoneId.systemDefault())); } @Override public void setLastModifiedDate(LocalDateTime lastModifiedDate) { - this.lastModifiedDate = Date.from(lastModifiedDate.atZone(ZoneId.systemDefault()).toInstant()); + this.lastModifiedDate = lastModifiedDate.atZone(ZoneId.systemDefault()).toInstant(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Temporal.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Temporal.java index e7492ab305..1a5d941662 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Temporal.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Temporal.java @@ -30,10 +30,12 @@ * * @author Thomas Darimont * @author Oliver Gierke + * @deprecated since 4.0. Please use {@literal java.time} types instead. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @Documented +@Deprecated(since = "4.0", forRemoval = true) public @interface Temporal { /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java index 74f4d84a05..b7c49ffc64 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java @@ -88,6 +88,8 @@ public boolean hasLimitingParameters() { public static class JpaParameter extends Parameter { private final @Nullable Temporal annotation; + + @SuppressWarnings("deprecation") private @Nullable TemporalType temporalType; /** diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/AbstractAuditingViaJavaConfigRepositoriesTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/AbstractAuditingViaJavaConfigRepositoriesTests.java index 5285ed2e3e..3073cc420e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/AbstractAuditingViaJavaConfigRepositoriesTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/AbstractAuditingViaJavaConfigRepositoriesTests.java @@ -20,9 +20,9 @@ import jakarta.persistence.EntityManager; +import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -54,6 +54,7 @@ * @author Oliver Gierke * @author Jens Schauder * @author Krzysztof Krason + * @author Christoph Strobl */ @ExtendWith(SpringExtension.class) @Transactional @@ -111,13 +112,13 @@ void shouldAllowUseOfDynamicSpelParametersInUpdateQueries() { em.detach(thomas); em.detach(auditor); - FixedDate.INSTANCE.setDate(new Date()); + FixedDate.INSTANCE.setDate(Instant.now()); SampleSecurityContextHolder.getCurrent().setPrincipal(thomas); auditableUserRepository.updateAllNamesToUpperCase(); // DateTime now = new DateTime(FixedDate.INSTANCE.getDate()); - LocalDateTime now = LocalDateTime.ofInstant(FixedDate.INSTANCE.getDate().toInstant(), ZoneId.systemDefault()); + LocalDateTime now = LocalDateTime.ofInstant(FixedDate.INSTANCE.getDate(), ZoneId.systemDefault()); List users = auditableUserRepository.findAll(); for (AuditableUser user : users) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/FixedDate.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/FixedDate.java index a6e2800784..091c3b24f7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/FixedDate.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/FixedDate.java @@ -15,24 +15,25 @@ */ package org.springframework.data.jpa.util; -import java.util.Date; +import java.time.Instant; /** - * Holds a fixed {@link Date} value to use in components that have no direct connection. + * Holds a fixed {@link Instant} value to use in components that have no direct connection. * * @author Thomas Darimont + * @author Christoph Strobl */ public enum FixedDate { INSTANCE; - private Date fixedDate; + private Instant fixedDate; - public void setDate(Date date) { + public void setDate(Instant date) { this.fixedDate = date; } - public Date getDate() { + public Instant getDate() { return fixedDate; } } From b4199e3a86fb0247b50e79a779870067981dfbaa Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Dec 2024 14:07:53 +0100 Subject: [PATCH 21/94] Polishing. A round of minor code style improvements. Original Pull Request: #3695 --- .../mapping/JpaPersistentPropertyImpl.java | 10 +++---- .../data/jpa/projection/package-info.java | 5 ++++ .../query/AbstractStringBasedJpaQuery.java | 4 +-- .../query/JSqlParserQueryEnhancer.java | 5 ++-- .../query/JpaQueryLookupStrategy.java | 3 ++- .../repository/query/JpqlQueryBuilder.java | 4 +-- .../query/KeysetScrollDelegate.java | 2 +- .../repository/query/ParameterBinding.java | 2 +- .../query/ParameterMetadataProvider.java | 17 +++++------- .../query/QueryParameterSetterFactory.java | 2 -- .../repository/query/QueryTokenStream.java | 5 ++++ .../jpa/repository/query/SimpleJpaQuery.java | 26 ++++++------------- .../jpa/repository/query/StringQuery.java | 26 +++++++------------ ...hScanningPersistenceUnitPostProcessor.java | 2 +- 14 files changed, 46 insertions(+), 67 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java index da773247e1..a63252f8db 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java @@ -57,13 +57,9 @@ class JpaPersistentPropertyImpl extends AnnotationBasedPersistentProperty> annotations = new HashSet<>(); - annotations.add(OneToMany.class); - annotations.add(OneToOne.class); - annotations.add(ManyToMany.class); - annotations.add(ManyToOne.class); + Set> annotations; - ASSOCIATION_ANNOTATIONS = Collections.unmodifiableSet(annotations); + ASSOCIATION_ANNOTATIONS = Set.of(OneToMany.class, OneToOne.class, ManyToMany.class, ManyToOne.class); annotations = new HashSet<>(); annotations.add(Id.class); @@ -107,7 +103,7 @@ public JpaPersistentPropertyImpl(JpaMetamodel metamodel, Property property, this.associationTargetType = detectAssociationTargetType(); this.updateable = detectUpdatability(); - this.isIdProperty = Lazy.of(() -> ID_ANNOTATIONS.stream().anyMatch(it -> isAnnotationPresent(it)) // + this.isIdProperty = Lazy.of(() -> ID_ANNOTATIONS.stream().anyMatch(this::isAnnotationPresent) // || metamodel.isSingleIdAttribute(getOwner().getType(), getName(), getType())); this.isEntity = Lazy.of(() -> metamodel.isMappedType(getActualType())); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java new file mode 100644 index 0000000000..4f85f48a62 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java @@ -0,0 +1,5 @@ +/** + * JPA specific support projection support. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.jpa.projection; 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 35e6cce708..73ec879ddd 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 @@ -92,9 +92,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri return query.deriveCountQuery(method.getCountQueryProjection()); }); - this.countParameterBinder = Lazy.of(() -> { - return this.createBinder(this.countQuery.get()); - }); + this.countParameterBinder = Lazy.of(() -> this.createBinder(this.countQuery.get())); this.queryRewriter = queryRewriter; 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 1f4b3b8b1b..99aec3ddf8 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 @@ -98,7 +98,8 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) { /** * Parses a query string with JSqlParser. * - * @param query the query to parse + * @param sql the query to parse + * @param classOfT the query to parse * @return the parsed query */ static T parseStatement(String sql, Class classOfT) { @@ -510,7 +511,7 @@ private static boolean onlyASingleColumnProjection(List> projectio * */ enum ParsedType { - DELETE, UPDATE, SELECT, INSERT, MERGE, OTHER; + DELETE, UPDATE, SELECT, INSERT, MERGE, OTHER } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index e6ca9f256b..317a66df94 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -143,7 +143,8 @@ private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStra * * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. + * @param delegate must not be {@literal null}. + * @param queryRewriterProvider must not be {@literal null}. */ public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index 287b397384..6c1817946b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -848,9 +848,7 @@ public String getAlias(Origin source) { */ public String getAlias(Origin source) { - return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(), s -> { - return !aliases.containsValue(s); - }, () -> "join_" + (counter++))); + return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(), s -> !aliases.containsValue(s), () -> "join_" + (counter++))); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java index ef9a67b697..0ff9902525 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java @@ -158,7 +158,7 @@ public Sort createSort(Sort sort, JpaEntityInformation entity) { } /** - * Reverse scrolling variant applying {@link Direction#Backward}. In reverse scrolling, we need to flip directions for + * Reverse scrolling variant applying {@link Direction#BACKWARD}. In reverse scrolling, we need to flip directions for * the actual query so that we do not get everything from the top position and apply the limit but rather flip the * sort direction, apply the limit and then reverse the result to restore the actual sort order. */ diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index 922719633d..b68ac78c83 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -667,7 +667,7 @@ static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Int /** * Creates a {@link MethodInvocationArgument} object for {@code position}. * - * @param position the parameter position (1-based) from the method invocation. + * @param parameter the parameter from the method invocation. * @return {@link MethodInvocationArgument} object for {@code position}. */ static MethodInvocationArgument ofParameter(Parameter parameter) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java index 667bc9f809..65d3538d04 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java @@ -273,17 +273,12 @@ public Object prepare(@Nullable Object value) { if (String.class.equals(parameterType) && !noWildcards) { - switch (type) { - case STARTING_WITH: - return String.format("%s%%", escape.escape(value.toString())); - case ENDING_WITH: - return String.format("%%%s", escape.escape(value.toString())); - case CONTAINING: - case NOT_CONTAINING: - return String.format("%%%s%%", escape.escape(value.toString())); - default: - return value; - } + return switch (type) { + case STARTING_WITH -> String.format("%s%%", escape.escape(value.toString())); + case ENDING_WITH -> String.format("%%%s", escape.escape(value.toString())); + case CONTAINING, NOT_CONTAINING -> String.format("%%%s%%", escape.escape(value.toString())); + default -> value; + }; } return Collection.class.isAssignableFrom(parameterType) // diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index 9e5c378621..3944628cf4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -93,7 +93,6 @@ static QueryParameterSetterFactory forSynthetic() { * * @param parser must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. - * @param parameters must not be {@literal null}. * @return a {@link QueryParameterSetterFactory} that can handle * {@link org.springframework.expression.spel.standard.SpelExpression}s. */ @@ -170,7 +169,6 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar /** * @param parser must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. - * @param parameters must not be {@literal null}. */ ExpressionBasedQueryParameterSetterFactory(ValueExpressionParser parser, ValueEvaluationContextProvider evaluationContextProvider) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java index 979b336528..0b3b659c8d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java @@ -46,6 +46,7 @@ static QueryTokenStream empty() { /** * Creates a QueryTokenStream from a {@link QueryToken}. + * @since 4.0 */ static QueryTokenStream from(QueryToken token) { return QueryRenderer.from(Collections.singletonList(token)); @@ -53,6 +54,7 @@ static QueryTokenStream from(QueryToken token) { /** * Creates an token QueryRenderer from an AST {@link TerminalNode}. + * @since 4.0 */ static QueryTokenStream ofToken(TerminalNode node) { return from(QueryTokens.token(node)); @@ -60,6 +62,7 @@ static QueryTokenStream ofToken(TerminalNode node) { /** * Creates an token QueryRenderer from an AST {@link Token}. + * @since 4.0 */ static QueryTokenStream ofToken(Token node) { return from(QueryTokens.token(node)); @@ -148,6 +151,7 @@ default QueryToken getFirst() { /** * @return the required first query token or throw {@link java.util.NoSuchElementException} if empty. + * @since 4.0 */ default QueryToken getRequiredFirst() { @@ -170,6 +174,7 @@ default QueryToken getLast() { /** * @return the required last query token or throw {@link java.util.NoSuchElementException} if empty. + * @since 4.0 */ default QueryToken getRequiredLast() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index c9a80e4a38..b90648223b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -84,23 +84,13 @@ private void validateQuery(String query, String errorMessage, Object... argument return; } - EntityManager validatingEm = null; - - try { - validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager(); - validatingEm.createQuery(query); - - } catch (RuntimeException e) { - - // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider - // https://java.net/projects/jpa-spec/lists/jsr338-experts/archive/2012-07/message/17 - throw new IllegalArgumentException(String.format(errorMessage, arguments), e); - - } finally { - - if (validatingEm != null) { - validatingEm.close(); - } - } + try (EntityManager validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager()) { + validatingEm.createQuery(query); + } catch (RuntimeException e) { + + // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider + // https://java.net/projects/jpa-spec/lists/jsr338-experts/archive/2012-07/message/17 + throw new IllegalArgumentException(String.format(errorMessage, arguments), e); + } } } 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 b36d7e728e..ef58f18ff4 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 @@ -401,25 +401,17 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que : ParameterOrigin.ofExpression(expression); BindingIdentifier targetBinding = queryParameter; - Function bindingFactory; - switch (ParameterBindingType.of(typeSource)) { + Function bindingFactory = switch (ParameterBindingType.of(typeSource)) { + case LIKE -> { - case LIKE: + Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + yield (identifier) -> new 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. + default -> (identifier) -> new ParameterBinding(identifier, origin); + }; - Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - bindingFactory = (identifier) -> new LikeParameterBinding(identifier, origin, likeType); - break; - - case IN: - bindingFactory = (identifier) -> new InParameterBinding(identifier, origin); - break; - - case AS_IS: // fall-through we don't need a special parameter queryParameter for the given parameter. - default: - bindingFactory = (identifier) -> new ParameterBinding(identifier, origin); - } - - if (origin.isExpression()) { + if (origin.isExpression()) { parameterBindings.register(bindingFactory.apply(queryParameter)); } else { targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java index 324c37f327..dd4690086b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java @@ -193,7 +193,7 @@ private Set scanForMappingFileLocations() { * @param uri * @return */ - private static String getResourcePath(URI uri) throws IOException { + private static String getResourcePath(URI uri) { if (uri.isOpaque()) { // e.g. jar:file:/foo/lib/somelib.jar!/com/acme/orm.xml From 12452f0057ac85306a9e03e722a879d7f5c481e4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 10:00:26 +0100 Subject: [PATCH 22/94] Extend license header copyright years to 2025. See #3734 --- .../data/jpa/repository/query/JpqlQueryBuilder.java | 2 +- .../data/jpa/repository/query/JpqlQueryCreator.java | 2 +- .../springframework/data/jpa/repository/query/JpqlUtils.java | 2 +- .../data/jpa/repository/query/PartTreeQueryCache.java | 2 +- .../data/jpa/repository/support/JpqlQueryTemplates.java | 2 +- .../jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java | 2 +- .../data/jpa/repository/query/JpaQueryCreatorTests.java | 2 +- .../data/jpa/repository/query/JpqlQueryBuilderUnitTests.java | 2 +- .../data/jpa/repository/query/PartTreeQueryCacheUnitTests.java | 2 +- .../jpa/repository/query/StubJpaParameterParameterAccessor.java | 2 +- .../java/org/springframework/data/jpa/util/TestMetaModel.java | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index 6c1817946b..e99e825338 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java index bbffd7c8a6..039392d571 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java index 500a7d4e84..354ce28aad 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java index 59d30c915f..21bead5d27 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java index 24180ae6fc..52590daa8c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpqlQueryTemplates.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java index 2221d3a87a..d8bfd1fdb9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java index 9073848ff2..f73b45e92d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java index 1146713058..d2ac172373 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java index aa3911473f..e55d89bfd1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeQueryCacheUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java index c5794c9644..e25cb03b58 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StubJpaParameterParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java index 822365b65a..c9c2611e37 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/util/TestMetaModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. From 9420153d6e9287f41c01135d2387b4bc7c14e207 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 11:35:57 +0100 Subject: [PATCH 23/94] Remove commons-logging exclusion. Closes #3736 --- spring-data-jpa/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index bb7829dcaf..19ed8b44a9 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -73,12 +73,6 @@ org.springframework spring-core - - - commons-logging - commons-logging - - From 8d942bd3376495fa8a3866266ef413f7707376b7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 Aug 2024 09:30:05 +0200 Subject: [PATCH 24/94] Explore refined Specification API. Introduce DeleteSpecification and UpdateSpecification. Add PredicateSpecification. Update SpecificationExecutor. Closes: #3521 Original Pull Request: #3578 --- .../data/jpa/domain/DeleteSpecification.java | 217 ++++++++++++ .../jpa/domain/PredicateSpecification.java | 174 ++++++++++ .../data/jpa/domain/Specification.java | 159 ++++++--- .../jpa/domain/SpecificationComposition.java | 77 ++++- .../data/jpa/domain/UpdateSpecification.java | 314 ++++++++++++++++++ .../repository/JpaSpecificationExecutor.java | 138 ++++++-- .../support/SimpleJpaRepository.java | 134 ++++++-- .../domain/DeleteSpecificationUnitTests.java | 170 ++++++++++ .../PredicateSpecificationUnitTests.java | 168 ++++++++++ .../jpa/domain/SpecificationUnitTests.java | 2 - .../domain/UpdateSpecificationUnitTests.java | 170 ++++++++++ .../jpa/domain/sample/UserSpecifications.java | 17 +- .../jpa/repository/UserRepositoryTests.java | 70 ++-- .../support/SimpleJpaRepositoryUnitTests.java | 3 +- 14 files changed, 1679 insertions(+), 134 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java new file mode 100644 index 0000000000..b3bfd93ae2 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -0,0 +1,217 @@ +/* + * Copyright 2024 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.domain; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specification in the sense of Domain Driven Design to handle Criteria Deletes. + * + * @author Mark Paluch + * @since xxx + */ +@FunctionalInterface +public interface DeleteSpecification extends Serializable { + + @Serial long serialVersionUID = 1L; + + /** + * Simple static factory method to create a specification deleting all objects. + * + * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. + * @return guaranteed to be not {@literal null}. + */ + static DeleteSpecification all() { + return (root, query, builder) -> null; + } + + /** + * Simple static factory method to add some syntactic sugar around a {@literal DeleteSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. + * @param spec must not be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static DeleteSpecification where(DeleteSpecification spec) { + + Assert.notNull(spec, "DeleteSpecification must not be null"); + + return spec; + } + + /** + * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to + * {@link DeleteSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. + * @param spec the {@link PredicateSpecification} to wrap. + * @return guaranteed to be not {@literal null}. + */ + static DeleteSpecification where(PredicateSpecification spec) { + + Assert.notNull(spec, "PredicateSpecification must not be null"); + + return where((root, delete, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder)); + } + + /** + * ANDs the given {@link DeleteSpecification} to the current one. + * + * @param other the other {@link DeleteSpecification}. + * @return the conjunction of the specifications. + */ + default DeleteSpecification and(DeleteSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, other, CriteriaBuilder::and); + } + + /** + * ANDs the given {@link DeleteSpecification} to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the conjunction of the specifications. + */ + default DeleteSpecification and(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and); + } + + /** + * ORs the given specification to the current one. + * + * @param other the other {@link DeleteSpecification}. + * @return the disjunction of the specifications. + */ + default DeleteSpecification or(DeleteSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, other, CriteriaBuilder::or); + } + + /** + * ORs the given specification to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the disjunction of the specifications. + */ + default DeleteSpecification or(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or); + } + + /** + * Negates the given {@link DeleteSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. + * @param spec can be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static DeleteSpecification not(DeleteSpecification spec) { + + Assert.notNull(spec, "Specification must not be null"); + + return (root, delete, builder) -> { + + Predicate not = spec.toPredicate(root, delete, builder); + return not != null ? builder.not(not) : null; + }; + } + + /** + * Applies an AND operation to all the given {@link DeleteSpecification}s. + * + * @param specifications the {@link DeleteSpecification}s to compose. + * @return the conjunction of the specifications. + * @see #and(DeleteSpecification) + * @see #allOf(Iterable) + */ + @SafeVarargs + static DeleteSpecification allOf(DeleteSpecification... specifications) { + return allOf(Arrays.asList(specifications)); + } + + /** + * Applies an AND operation to all the given {@link DeleteSpecification}s. + * + * @param specifications the {@link DeleteSpecification}s to compose. + * @return the conjunction of the specifications. + * @see #and(DeleteSpecification) + * @see #allOf(DeleteSpecification[]) + */ + static DeleteSpecification allOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(DeleteSpecification.all(), DeleteSpecification::and); + } + + /** + * Applies an OR operation to all the given {@link DeleteSpecification}s. + * + * @param specifications the {@link DeleteSpecification}s to compose. + * @return the disjunction of the specifications. + * @see #or(DeleteSpecification) + * @see #anyOf(Iterable) + */ + @SafeVarargs + static DeleteSpecification anyOf(DeleteSpecification... specifications) { + return anyOf(Arrays.asList(specifications)); + } + + /** + * Applies an OR operation to all the given {@link DeleteSpecification}s. + * + * @param specifications the {@link DeleteSpecification}s to compose. + * @return the disjunction of the specifications. + * @see #or(DeleteSpecification) + * @see #anyOf(Iterable) + */ + static DeleteSpecification anyOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(DeleteSpecification.all(), DeleteSpecification::or); + } + + /** + * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given + * {@link Root} and {@link CriteriaDelete}. + * + * @param root must not be {@literal null}. + * @param delete the delete criteria. + * @param criteriaBuilder must not be {@literal null}. + * @return a {@link Predicate}, may be {@literal null}. + */ + @Nullable + Predicate toPredicate(Root root, CriteriaDelete delete, CriteriaBuilder criteriaBuilder); + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java new file mode 100644 index 0000000000..b3e52f4249 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -0,0 +1,174 @@ +/* + * Copyright 2024 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.domain; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specification in the sense of Domain Driven Design. + * + * @author Mark Paluch + * @since xxx + */ +public interface PredicateSpecification extends Serializable { + + @Serial long serialVersionUID = 1L; + + /** + * Simple static factory method to create a specification matching all objects. + * + * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. + * @return guaranteed to be not {@literal null}. + */ + static PredicateSpecification all() { + return (root, builder) -> null; + } + + /** + * Simple static factory method to add some syntactic sugar around a {@literal PredicateSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. + * @param spec must not be {@literal null}. + * @return guaranteed to be not {@literal null}. + * @since 2.0 + */ + static PredicateSpecification where(PredicateSpecification spec) { + + Assert.notNull(spec, "DeleteSpecification must not be null"); + + return spec; + } + + /** + * ANDs the given {@literal PredicateSpecification} to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the conjunction of the specifications. + */ + default PredicateSpecification and(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, other, CriteriaBuilder::and); + } + + /** + * ORs the given specification to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the disjunction of the specifications. + */ + default PredicateSpecification or(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, other, CriteriaBuilder::or); + } + + /** + * Negates the given {@link PredicateSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. + * @param spec can be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static PredicateSpecification not(PredicateSpecification spec) { + + Assert.notNull(spec, "Specification must not be null"); + + return (root, builder) -> { + + Predicate not = spec.toPredicate(root, builder); + return not != null ? builder.not(not) : null; + }; + } + + /** + * Applies an AND operation to all the given {@link PredicateSpecification}s. + * + * @param specifications the {@link PredicateSpecification}s to compose. + * @return the conjunction of the specifications. + * @see #allOf(Iterable) + * @see #and(PredicateSpecification) + */ + @SafeVarargs + static PredicateSpecification allOf(PredicateSpecification... specifications) { + return allOf(Arrays.asList(specifications)); + } + + /** + * Applies an AND operation to all the given {@link PredicateSpecification}s. + * + * @param specifications the {@link PredicateSpecification}s to compose. + * @return the conjunction of the specifications. + * @see #and(PredicateSpecification) + * @see #allOf(PredicateSpecification[]) + */ + static PredicateSpecification allOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(PredicateSpecification.all(), PredicateSpecification::and); + } + + /** + * Applies an OR operation to all the given {@link PredicateSpecification}s. + * + * @param specifications the {@link PredicateSpecification}s to compose. + * @return the disjunction of the specifications. + * @see #or(PredicateSpecification) + * @see #anyOf(Iterable) + */ + @SafeVarargs + static PredicateSpecification anyOf(PredicateSpecification... specifications) { + return anyOf(Arrays.asList(specifications)); + } + + /** + * Applies an OR operation to all the given {@link PredicateSpecification}s. + * + * @param specifications the {@link PredicateSpecification}s to compose. + * @return the disjunction of the specifications. + * @see #or(PredicateSpecification) + * @see #anyOf(PredicateSpecification[]) + */ + static PredicateSpecification anyOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(PredicateSpecification.all(), PredicateSpecification::or); + } + + /** + * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given + * {@link Root} and {@link CriteriaBuilder}. + * + * @param root must not be {@literal null}. + * @param criteriaBuilder must not be {@literal null}. + * @return a {@link Predicate}, may be {@literal null}. + */ + @Nullable + Predicate toPredicate(Root root, CriteriaBuilder criteriaBuilder); + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 4586bf76f7..7908ad1a77 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -17,6 +17,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -26,6 +27,7 @@ import java.util.stream.StreamSupport; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Specification in the sense of Domain Driven Design. @@ -45,83 +47,128 @@ public interface Specification extends Serializable { @Serial long serialVersionUID = 1L; /** - * Negates the given {@link Specification}. + * Simple static factory method to create a specification matching all objects. * * @param the type of the {@link Root} the resulting {@literal Specification} operates on. - * @param spec can be {@literal null}. * @return guaranteed to be not {@literal null}. - * @since 2.0 */ - static Specification not(@Nullable Specification spec) { - - return spec == null // - ? (root, query, builder) -> null // - : (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder)); + static Specification all() { + return (root, query, builder) -> null; } /** * Simple static factory method to add some syntactic sugar around a {@link Specification}. * * @param the type of the {@link Root} the resulting {@literal Specification} operates on. - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @return guaranteed to be not {@literal null}. * @since 2.0 * @deprecated since 3.5. */ @Deprecated(since = "3.5.0", forRemoval = true) - static Specification where(@Nullable Specification spec) { - return spec == null ? (root, query, builder) -> null : spec; + static Specification where(Specification spec) { + + Assert.notNull(spec, "Specification must not be null"); + + return spec; + } + + /** + * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to + * {@link Specification}. + * + * @param the type of the {@link Root} the resulting {@literal Specification} operates on. + * @param spec the {@link PredicateSpecification} to wrap. + * @return guaranteed to be not {@literal null}. + */ + static Specification where(PredicateSpecification spec) { + + Assert.notNull(spec, "PredicateSpecification must not be null"); + + return where((root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder)); } /** * ANDs the given {@link Specification} to the current one. * - * @param other can be {@literal null}. - * @return The conjunction of the specifications + * @param other the other {@link Specification}. + * @return the conjunction of the specifications. * @since 2.0 */ - default Specification and(@Nullable Specification other) { + default Specification and(Specification other) { + + Assert.notNull(other, "Other specification must not be null"); + return SpecificationComposition.composed(this, other, CriteriaBuilder::and); } + /** + * ANDs the given {@link Specification} to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the conjunction of the specifications. + * @since 2.0 + */ + default Specification and(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and); + } + /** * ORs the given specification to the current one. * - * @param other can be {@literal null}. - * @return The disjunction of the specifications + * @param other the other {@link Specification}. + * @return the disjunction of the specifications * @since 2.0 */ - default Specification or(@Nullable Specification other) { + default Specification or(Specification other) { + + Assert.notNull(other, "Other specification must not be null"); + return SpecificationComposition.composed(this, other, CriteriaBuilder::or); } /** - * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given - * {@link Root} and {@link CriteriaQuery}. + * ORs the given specification to the current one. * - * @param root must not be {@literal null}. - * @param query can be {@literal null} to allow overrides that accept {@link jakarta.persistence.criteria.CriteriaDelete} which is an {@link jakarta.persistence.criteria.AbstractQuery} but no {@link CriteriaQuery}. - * @param criteriaBuilder must not be {@literal null}. - * @return a {@link Predicate}, may be {@literal null}. + * @param other the other {@link PredicateSpecification}. + * @return the disjunction of the specifications + * @since 2.0 */ - @Nullable - Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder criteriaBuilder); + default Specification or(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or); + } /** - * Applies an AND operation to all the given {@link Specification}s. + * Negates the given {@link Specification}. * - * @param specifications The {@link Specification}s to compose. Can contain {@code null}s. - * @return The conjunction of the specifications - * @see #and(Specification) - * @since 3.0 + * @param the type of the {@link Root} the resulting {@literal Specification} operates on. + * @param spec can be {@literal null}. + * @return guaranteed to be not {@literal null}. + * @since 2.0 */ - static Specification allOf(Iterable> specifications) { + static Specification not(Specification spec) { - return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.where(null), Specification::and); + Assert.notNull(spec, "Specification must not be null"); + + return (root, query, builder) -> { + + Predicate not = spec.toPredicate(root, query, builder); + return not != null ? builder.not(not) : null; + }; } /** + * Applies an AND operation to all the given {@link Specification}s. + * + * @param specifications the {@link Specification}s to compose. + * @return the conjunction of the specifications. + * @see #and(Specification) * @see #allOf(Iterable) * @since 3.0 */ @@ -131,20 +178,26 @@ static Specification allOf(Specification... specifications) { } /** - * Applies an OR operation to all the given {@link Specification}s. + * Applies an AND operation to all the given {@link Specification}s. * - * @param specifications The {@link Specification}s to compose. Can contain {@code null}s. - * @return The disjunction of the specifications - * @see #or(Specification) + * @param specifications the {@link Specification}s to compose. + * @return the conjunction of the specifications. + * @see #and(Specification) + * @see #allOf(Specification[]) * @since 3.0 */ - static Specification anyOf(Iterable> specifications) { + static Specification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.where(null), Specification::or); + .reduce(Specification.all(), Specification::and); } /** + * Applies an OR operation to all the given {@link Specification}s. + * + * @param specifications the {@link Specification}s to compose. + * @return the disjunction of the specifications + * @see #or(Specification) * @see #anyOf(Iterable) * @since 3.0 */ @@ -152,4 +205,32 @@ static Specification anyOf(Iterable> specifications) { static Specification anyOf(Specification... specifications) { return anyOf(Arrays.asList(specifications)); } + + /** + * Applies an OR operation to all the given {@link Specification}s. + * + * @param specifications the {@link Specification}s to compose. + * @return the disjunction of the specifications + * @see #or(Specification) + * @see #anyOf(Iterable) + * @since 3.0 + */ + static Specification anyOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(Specification.all(), Specification::or); + } + + /** + * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given + * {@link Root} and {@link CriteriaUpdate}. + * + * @param root must not be {@literal null}. + * @param query the criteria query. + * @param criteriaBuilder must not be {@literal null}. + * @return a {@link Predicate}, may be {@literal null}. + */ + @Nullable + Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder); + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java index ad78749e39..0b6e90014c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java @@ -15,13 +15,15 @@ */ package org.springframework.data.jpa.domain; -import java.io.Serializable; - import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import java.io.Serializable; + import org.springframework.lang.Nullable; /** @@ -57,8 +59,75 @@ static Specification composed(@Nullable Specification lhs, @Nullable S } @Nullable - private static Predicate toPredicate(@Nullable Specification specification, Root root, @Nullable CriteriaQuery query, - CriteriaBuilder builder) { + private static Predicate toPredicate(@Nullable Specification specification, Root root, + @Nullable CriteriaQuery query, CriteriaBuilder builder) { return specification == null ? null : specification.toPredicate(root, query, builder); } + + static DeleteSpecification composed(@Nullable DeleteSpecification lhs, @Nullable DeleteSpecification rhs, + Combiner combiner) { + + return (root, query, builder) -> { + + Predicate thisPredicate = toPredicate(lhs, root, query, builder); + Predicate otherPredicate = toPredicate(rhs, root, query, builder); + + if (thisPredicate == null) { + return otherPredicate; + } + + return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate); + }; + } + + @Nullable + private static Predicate toPredicate(@Nullable DeleteSpecification specification, Root root, + @Nullable CriteriaDelete delete, CriteriaBuilder builder) { + return specification == null ? null : specification.toPredicate(root, delete, builder); + } + + static UpdateSpecification composed(@Nullable UpdateSpecification lhs, @Nullable UpdateSpecification rhs, + Combiner combiner) { + + return (root, query, builder) -> { + + Predicate thisPredicate = toPredicate(lhs, root, query, builder); + Predicate otherPredicate = toPredicate(rhs, root, query, builder); + + if (thisPredicate == null) { + return otherPredicate; + } + + return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate); + }; + } + + @Nullable + private static Predicate toPredicate(@Nullable UpdateSpecification specification, Root root, + CriteriaUpdate update, CriteriaBuilder builder) { + return specification == null ? null : specification.toPredicate(root, update, builder); + } + + static PredicateSpecification composed(PredicateSpecification lhs, PredicateSpecification rhs, + Combiner combiner) { + + return (root, builder) -> { + + Predicate thisPredicate = toPredicate(lhs, root, builder); + Predicate otherPredicate = toPredicate(rhs, root, builder); + + if (thisPredicate == null) { + return otherPredicate; + } + + return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate); + }; + } + + @Nullable + private static Predicate toPredicate(@Nullable PredicateSpecification specification, Root root, + CriteriaBuilder builder) { + return specification == null ? null : specification.toPredicate(root, builder); + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java new file mode 100644 index 0000000000..8e217fc0f4 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -0,0 +1,314 @@ +/* + * Copyright 2024 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.domain; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Arrays; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specification in the sense of Domain Driven Design to handle Criteria Updates. + * + * @author Mark Paluch + * @since xxx + */ +@FunctionalInterface +public interface UpdateSpecification extends Serializable { + + @Serial long serialVersionUID = 1L; + + /** + * Simple static factory method to create a specification deleting all objects. + * + * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @return guaranteed to be not {@literal null}. + */ + static UpdateSpecification all() { + return (root, query, builder) -> null; + } + + /** + * Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}. For example: + * + *
+	 * UpdateSpecification<User> updateLastname = UpdateSpecification
+	 * 		.<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
+	 * 		.where(userHasFirstname("Walter").and(userHasLastname("White")));
+	 *
+	 * repository.update(updateLastname);
+	 * 
+ * + * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @param spec must not be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static UpdateOperation update(UpdateOperation spec) { + + Assert.notNull(spec, "UpdateSpecification must not be null"); + + return spec; + } + + /** + * Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @param spec must not be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static UpdateSpecification where(UpdateSpecification spec) { + + Assert.notNull(spec, "UpdateSpecification must not be null"); + + return spec; + } + + /** + * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to + * {@link UpdateSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @param spec the {@link PredicateSpecification} to wrap. + * @return guaranteed to be not {@literal null}. + */ + static UpdateSpecification where(PredicateSpecification spec) { + + Assert.notNull(spec, "PredicateSpecification must not be null"); + + return where((root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder)); + } + + /** + * ANDs the given {@link UpdateSpecification} to the current one. + * + * @param other the other {@link UpdateSpecification}. + * @return the conjunction of the specifications. + */ + default UpdateSpecification and(UpdateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, other, CriteriaBuilder::and); + } + + /** + * ANDs the given {@link UpdateSpecification} to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the conjunction of the specifications. + */ + default UpdateSpecification and(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::and); + } + + /** + * ORs the given specification to the current one. + * + * @param other the other {@link UpdateSpecification}. + * @return the disjunction of the specifications. + */ + default UpdateSpecification or(UpdateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, other, CriteriaBuilder::or); + } + + /** + * ORs the given specification to the current one. + * + * @param other the other {@link PredicateSpecification}. + * @return the disjunction of the specifications. + */ + default UpdateSpecification or(PredicateSpecification other) { + + Assert.notNull(other, "Other specification must not be null"); + + return SpecificationComposition.composed(this, where(other), CriteriaBuilder::or); + } + + /** + * Negates the given {@link UpdateSpecification}. + * + * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @param spec can be {@literal null}. + * @return guaranteed to be not {@literal null}. + */ + static UpdateSpecification not(UpdateSpecification spec) { + + Assert.notNull(spec, "Specification must not be null"); + + return (root, update, builder) -> { + + Predicate not = spec.toPredicate(root, update, builder); + return not != null ? builder.not(not) : null; + }; + } + + /** + * Applies an AND operation to all the given {@link UpdateSpecification}s. + * + * @param specifications the {@link UpdateSpecification}s to compose. + * @return the conjunction of the specifications. + * @see #and(UpdateSpecification) + * @see #allOf(Iterable) + */ + @SafeVarargs + static UpdateSpecification allOf(UpdateSpecification... specifications) { + return allOf(Arrays.asList(specifications)); + } + + /** + * Applies an AND operation to all the given {@link UpdateSpecification}s. + * + * @param specifications the {@link UpdateSpecification}s to compose. + * @return the conjunction of the specifications. + * @see #and(UpdateSpecification) + * @see #allOf(UpdateSpecification[]) + */ + static UpdateSpecification allOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(UpdateSpecification.all(), UpdateSpecification::and); + } + + /** + * Applies an OR operation to all the given {@link UpdateSpecification}s. + * + * @param specifications the {@link UpdateSpecification}s to compose. + * @return the disjunction of the specifications. + * @see #or(UpdateSpecification) + * @see #anyOf(Iterable) + */ + @SafeVarargs + static UpdateSpecification anyOf(UpdateSpecification... specifications) { + return anyOf(Arrays.asList(specifications)); + } + + /** + * Applies an OR operation to all the given {@link UpdateSpecification}s. + * + * @param specifications the {@link UpdateSpecification}s to compose. + * @return the disjunction of the specifications. + * @see #or(UpdateSpecification) + * @see #anyOf(Iterable) + */ + static UpdateSpecification anyOf(Iterable> specifications) { + + return StreamSupport.stream(specifications.spliterator(), false) // + .reduce(UpdateSpecification.all(), UpdateSpecification::or); + } + + /** + * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given + * {@link Root} and {@link CriteriaUpdate}. + * + * @param root must not be {@literal null}. + * @param update the update criteria. + * @param criteriaBuilder must not be {@literal null}. + * @return a {@link Predicate}, may be {@literal null}. + */ + @Nullable + Predicate toPredicate(Root root, CriteriaUpdate update, CriteriaBuilder criteriaBuilder); + + /** + * Simplified extension to {@link UpdateSpecification} that only considers the {@code UPDATE} part without specifying + * a predicate. This is useful to separate concerns for reusable specifications, for example: + * + *
+	 * UpdateSpecification<User> updateLastname = UpdateSpecification
+	 * 		.<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
+	 * 		.where(userHasFirstname("Walter").and(userHasLastname("White")));
+	 *
+	 * repository.update(updateLastname);
+	 * 
+ * + * @param + */ + @FunctionalInterface + interface UpdateOperation { + + /** + * ANDs the given {@link UpdateOperation} to the current one. + * + * @param other the other {@link UpdateOperation}. + * @return the conjunction of the specifications. + */ + default UpdateOperation and(UpdateOperation other) { + + Assert.notNull(other, "Other UpdateOperation must not be null"); + + return (root, update, criteriaBuilder) -> { + this.apply(root, update, criteriaBuilder); + other.apply(root, update, criteriaBuilder); + }; + } + + /** + * Creates a {@link UpdateSpecification} from this and the given {@link UpdateSpecification}. + * + * @param specification the {@link PredicateSpecification}. + * @return the conjunction of the specifications. + */ + default UpdateSpecification where(PredicateSpecification specification) { + + Assert.notNull(specification, "PredicateSpecification must not be null"); + + return (root, update, criteriaBuilder) -> { + this.apply(root, update, criteriaBuilder); + return specification.toPredicate(root, criteriaBuilder); + }; + } + + /** + * Creates a {@link UpdateSpecification} from this and the given {@link UpdateSpecification}. + * + * @param specification the {@link UpdateSpecification}. + * @return the conjunction of the specifications. + */ + default UpdateSpecification where(UpdateSpecification specification) { + + Assert.notNull(specification, "UpdateSpecification must not be null"); + + return (root, update, criteriaBuilder) -> { + this.apply(root, update, criteriaBuilder); + return specification.toPredicate(root, update, criteriaBuilder); + }; + } + + /** + * Accept the given {@link Root} and {@link CriteriaUpdate} to apply the update operation. + * + * @param root must not be {@literal null}. + * @param update the update criteria. + * @param criteriaBuilder must not be {@literal null}. + */ + void apply(Root root, CriteriaUpdate update, CriteriaBuilder criteriaBuilder); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index c3249502e4..da03816ec4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -29,9 +29,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.DeleteSpecification; +import org.springframework.data.jpa.domain.PredicateSpecification; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.UpdateSpecification; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.lang.Nullable; /** * Interface to allow execution of {@link Specification}s based on the JPA criteria API. @@ -41,38 +43,65 @@ * @author Diego Krupitza * @author Mark Paluch * @author Joshua Chen + * @see Specification + * @see org.springframework.data.jpa.domain.UpdateSpecification + * @see DeleteSpecification + * @see PredicateSpecification */ public interface JpaSpecificationExecutor { + /** + * Returns a single entity matching the given {@link PredicateSpecification} or {@link Optional#empty()} if none + * found. + * + * @param spec must not be {@literal null}. + * @return never {@literal null}. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. + * @see Specification#all() + */ + default Optional findOne(PredicateSpecification spec) { + return findOne(Specification.where(spec)); + } + /** * Returns a single entity matching the given {@link Specification} or {@link Optional#empty()} if none found. * * @param spec must not be {@literal null}. * @return never {@literal null}. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. + * @see Specification#all() */ Optional findOne(Specification spec); + /** + * Returns all entities matching the given {@link PredicateSpecification}. + * + * @param spec must not be {@literal null}. + * @return never {@literal null}. + * @see Specification#all() + */ + default List findAll(PredicateSpecification spec) { + return findAll(Specification.where(spec)); + } + /** * Returns all entities matching the given {@link Specification}. - *

- * If no {@link Specification} is given all entities matching {@code } will be selected. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @return never {@literal null}. + * @see Specification#all() */ - List findAll(@Nullable Specification spec); + List findAll(Specification spec); /** * Returns a {@link Page} of entities matching the given {@link Specification}. - *

- * If no {@link Specification} is given all entities matching {@code } will be selected. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param pageable must not be {@literal null}. * @return never {@literal null}. + * @see Specification#all() */ - Page findAll(@Nullable Specification spec, Pageable pageable); + Page findAll(Specification spec, Pageable pageable); /** * Returns a {@link Page} of entities matching the given {@link Specification}. @@ -92,52 +121,109 @@ public interface JpaSpecificationExecutor { /** * Returns all entities matching the given {@link Specification} and {@link Sort}. - *

- * If no {@link Specification} is given all entities matching {@code } will be selected. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param sort must not be {@literal null}. * @return never {@literal null}. + * @see Specification#all() + */ + List findAll(Specification spec, Sort sort); + + /** + * Returns the number of instances that the given {@link PredicateSpecification} will return. + * + * @param spec the {@link PredicateSpecification} to count instances for, must not be {@literal null}. + * @return the number of instances. + * @see Specification#all() */ - List findAll(@Nullable Specification spec, Sort sort); + default long count(PredicateSpecification spec) { + return count(Specification.where(spec)); + } /** * Returns the number of instances that the given {@link Specification} will return. - *

- * If no {@link Specification} is given all entities matching {@code } will be counted. * * @param spec the {@link Specification} to count instances for, must not be {@literal null}. * @return the number of instances. + * @see Specification#all() + */ + long count(Specification spec); + + /** + * Checks whether the data store contains elements that match the given {@link PredicateSpecification}. + * + * @param spec the {@link PredicateSpecification} to use for the existence check, must not be {@literal null}. + * @return {@code true} if the data store contains elements that match the given {@link PredicateSpecification} + * otherwise {@code false}. + * @see Specification#all() */ - long count(@Nullable Specification spec); + default boolean exists(PredicateSpecification spec) { + return exists(Specification.where(spec)); + } /** * Checks whether the data store contains elements that match the given {@link Specification}. * - * @param spec the {@link Specification} to use for the existence check, ust not be {@literal null}. + * @param spec the {@link Specification} to use for the existence check, must not be {@literal null}. * @return {@code true} if the data store contains elements that match the given {@link Specification} otherwise * {@code false}. + * @see Specification#all() */ boolean exists(Specification spec); /** - * Deletes by the {@link Specification} and returns the number of rows deleted. + * Updates entities by the {@link UpdateSpecification} and returns the number of rows updated. + *

+ * This method uses {@link jakarta.persistence.criteria.CriteriaUpdate Criteria API bulk update} that maps directly to + * database update operations. The persistence context is not synchronized with the result of the bulk update. + * + * @param spec the {@link UpdateSpecification} to use for the update query must not be {@literal null}. + * @return the number of entities deleted. + * @since xxx + */ + long update(UpdateSpecification spec); + + /** + * Deletes by the {@link PredicateSpecification} and returns the number of rows deleted. *

* This method uses {@link jakarta.persistence.criteria.CriteriaDelete Criteria API bulk delete} that maps directly to * database delete operations. The persistence context is not synchronized with the result of the bulk delete. + * + * @param spec the {@link PredicateSpecification} to use for the delete query, must not be {@literal null}. + * @return the number of entities deleted. + * @since 3.0 + * @see PredicateSpecification#all() + */ + default long delete(PredicateSpecification spec) { + return delete(DeleteSpecification.where(spec)); + } + + /** + * Deletes by the {@link UpdateSpecification} and returns the number of rows deleted. *

- * Please note that {@link jakarta.persistence.criteria.CriteriaQuery} in, - * {@link Specification#toPredicate(Root, CriteriaQuery, CriteriaBuilder)} will be {@literal null} because - * {@link jakarta.persistence.criteria.CriteriaBuilder#createCriteriaDelete(Class)} does not implement - * {@code CriteriaQuery}. - *

- * If no {@link Specification} is given all entities matching {@code } will be deleted. + * This method uses {@link jakarta.persistence.criteria.CriteriaDelete Criteria API bulk delete} that maps directly to + * database delete operations. The persistence context is not synchronized with the result of the bulk delete. * - * @param spec the {@link Specification} to use for the existence check, can not be {@literal null}. + * @param spec the {@link UpdateSpecification} to use for the delete query must not be {@literal null}. * @return the number of entities deleted. * @since 3.0 + * @see DeleteSpecification#all() + */ + long delete(DeleteSpecification spec); + + /** + * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query + * and its result type. + * + * @param spec must not be null. + * @param queryFunction the query function defining projection, sorting, and the result type + * @return all entities matching the given Example. + * @since xxx */ - long delete(@Nullable Specification spec); + default R findBy(PredicateSpecification spec, + Function, R> queryFunction) { + return findBy(Specification.where(spec), queryFunction); + } /** * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 25ffed505b..2f18aeca98 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -25,6 +25,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; @@ -52,7 +53,9 @@ import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder; +import org.springframework.data.jpa.domain.DeleteSpecification; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.UpdateSpecification; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.query.EscapeCharacter; @@ -398,7 +401,7 @@ public boolean existsById(ID id) { @Override public List findAll() { - return getQuery(null, Sort.unsorted()).getResultList(); + return getQuery(Specification.all(), Sort.unsorted()).getResultList(); } @Override @@ -431,12 +434,12 @@ public List findAllById(Iterable ids) { @Override public List findAll(Sort sort) { - return getQuery(null, sort).getResultList(); + return getQuery(Specification.all(), sort).getResultList(); } @Override public Page findAll(Pageable pageable) { - return findAll((Specification) null, pageable); + return findAll(Specification.all(), pageable); } @Override @@ -450,7 +453,7 @@ public List findAll(Specification spec) { } @Override - public Page findAll(@Nullable Specification spec, Pageable pageable) { + public Page findAll(Specification spec, Pageable pageable) { return findAll(spec, spec, pageable); } @@ -463,13 +466,15 @@ public Page findAll(@Nullable Specification spec, @Nullable Specification< } @Override - public List findAll(@Nullable Specification spec, Sort sort) { + public List findAll(Specification spec, Sort sort) { return getQuery(spec, sort).getResultList(); } @Override public boolean exists(Specification spec) { + Assert.notNull(spec, "Specification must not be null"); + CriteriaQuery cq = this.entityManager.getCriteriaBuilder() // .createQuery(Integer.class) // .select(this.entityManager.getCriteriaBuilder().literal(1)); @@ -482,21 +487,20 @@ public boolean exists(Specification spec) { @Override @Transactional - public long delete(@Nullable Specification spec) { + public long update(UpdateSpecification spec) { - CriteriaBuilder builder = this.entityManager.getCriteriaBuilder(); - CriteriaDelete delete = builder.createCriteriaDelete(getDomainClass()); + Assert.notNull(spec, "Specification must not be null"); - if (spec != null) { - Predicate predicate = spec.toPredicate(delete.from(getDomainClass()), builder.createQuery(getDomainClass()), - builder); + return getUpdate(spec, getDomainClass()).executeUpdate(); + } - if (predicate != null) { - delete.where(predicate); - } - } + @Override + @Transactional + public long delete(DeleteSpecification spec) { + + Assert.notNull(spec, "Specification must not be null"); - return this.entityManager.createQuery(delete).executeUpdate(); + return getDelete(spec, getDomainClass()).executeUpdate(); } @Override @@ -747,17 +751,17 @@ protected TypedQuery getQuery(@Nullable Specification spec, /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param sort must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Sort sort) { + protected TypedQuery getQuery(Specification spec, Sort sort) { return getQuery(spec, getDomainClass(), sort); } /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param domainClass must not be {@literal null}. * @param sort must not be {@literal null}. */ @@ -779,6 +783,8 @@ protected TypedQuery getQuery(@Nullable Specification spec, private TypedQuery getQuery(ReturnedType returnedType, @Nullable Specification spec, Class domainClass, Sort sort, Collection inputProperties, @Nullable ScrollPosition scrollPosition) { + Assert.notNull(spec, "Specification must not be null"); + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query; @@ -832,6 +838,42 @@ private TypedQuery getQuery(ReturnedType returnedType, @Nullabl return applyRepositoryMethodMetadata(entityManager.createQuery(query)); } + /** + * Creates a {@link Query} for the given {@link UpdateSpecification}. + * + * @param spec must not be {@literal null}. + * @param domainClass must not be {@literal null}. + */ + protected Query getUpdate(UpdateSpecification spec, Class domainClass) { + + Assert.notNull(spec, "Specification must not be null"); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaUpdate query = builder.createCriteriaUpdate(domainClass); + + applySpecificationToCriteria(spec, domainClass, query); + + return applyRepositoryMethodMetadata(entityManager.createQuery(query)); + } + + /** + * Creates a {@link Query} for the given {@link DeleteSpecification}. + * + * @param spec must not be {@literal null}. + * @param domainClass must not be {@literal null}. + */ + protected Query getDelete(DeleteSpecification spec, Class domainClass) { + + Assert.notNull(spec, "Specification must not be null"); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaDelete query = builder.createCriteriaDelete(domainClass); + + applySpecificationToCriteria(spec, domainClass, query); + + return applyRepositoryMethodMetadata(entityManager.createQuery(query)); + } + /** * Creates a new count query for the given {@link Specification}. * @@ -883,33 +925,45 @@ protected QueryHints getQueryHintsForCount() { return metadata == null ? NoHints.INSTANCE : DefaultQueryHints.of(entityInformation, metadata).forCounts(); } - /** - * Applies the given {@link Specification} to the given {@link CriteriaQuery}. - * - * @param spec can be {@literal null}. - * @param domainClass must not be {@literal null}. - * @param query must not be {@literal null}. - */ - private Root applySpecificationToCriteria(@Nullable Specification spec, Class domainClass, + private Root applySpecificationToCriteria(Specification spec, Class domainClass, CriteriaQuery query) { - Assert.notNull(domainClass, "Domain class must not be null"); - Assert.notNull(query, "CriteriaQuery must not be null"); - Root root = query.from(domainClass); - if (spec == null) { - return root; + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Predicate predicate = spec.toPredicate(root, query, builder); + + if (predicate != null) { + query.where(predicate); } + return root; + } + + private void applySpecificationToCriteria(UpdateSpecification spec, Class domainClass, + CriteriaUpdate query) { + + Root root = query.from(domainClass); + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); Predicate predicate = spec.toPredicate(root, query, builder); if (predicate != null) { query.where(predicate); } + } - return root; + private void applySpecificationToCriteria(DeleteSpecification spec, Class domainClass, + CriteriaDelete query) { + + Root root = query.from(domainClass); + + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + Predicate predicate = spec.toPredicate(root, query, builder); + + if (predicate != null) { + query.where(predicate); + } } private TypedQuery applyRepositoryMethodMetadata(TypedQuery query) { @@ -926,6 +980,20 @@ private TypedQuery applyRepositoryMethodMetadata(TypedQuery query) { return toReturn; } + private Query applyRepositoryMethodMetadata(Query query) { + + if (metadata == null) { + return query; + } + + LockModeType type = metadata.getLockModeType(); + Query toReturn = type == null ? query : query.setLockMode(type); + + applyQueryHints(toReturn); + + return toReturn; + } + private void applyQueryHints(Query query) { if (metadata == null) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java new file mode 100644 index 0000000000..79e531ad7f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2024 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.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.util.SerializationUtils.*; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serializable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Unit tests for {@link DeleteSpecification}. + * + * @author Mark Paluch + */ +@SuppressWarnings("serial") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class DeleteSpecificationUnitTests implements Serializable { + + private DeleteSpecification spec; + @Mock(serializable = true) Root root; + @Mock(serializable = true) CriteriaDelete delete; + @Mock(serializable = true) CriteriaBuilder builder; + @Mock(serializable = true) Predicate predicate; + @Mock(serializable = true) Predicate another; + + @BeforeEach + void setUp() { + spec = (root, delete, cb) -> predicate; + } + + @Test // GH-3521 + void allReturnsEmptyPredicate() { + + DeleteSpecification specification = DeleteSpecification.all(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isNull(); + } + + @Test // GH-3521 + void allOfCombinesPredicatesInOrder() { + + DeleteSpecification specification = DeleteSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void anyOfCombinesPredicatesInOrder() { + + DeleteSpecification specification = DeleteSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void emptyAllOfReturnsEmptySpecification() { + + DeleteSpecification specification = DeleteSpecification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isNull(); + } + + @Test // GH-3521 + void emptyAnyOfReturnsEmptySpecification() { + + DeleteSpecification specification = DeleteSpecification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, delete, builder)).isNull(); + } + + @Test // GH-3521 + void specificationsShouldBeSerializable() { + + DeleteSpecification serializableSpec = new SerializableSpecification(); + DeleteSpecification specification = serializableSpec.and(serializableSpec); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void complexSpecificationsShouldBeSerializable() { + + SerializableSpecification serializableSpec = new SerializableSpecification(); + DeleteSpecification specification = DeleteSpecification + .not(serializableSpec.and(serializableSpec).or(serializableSpec)); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void andCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + DeleteSpecification first = ((root1, delete, criteriaBuilder) -> firstPredicate); + DeleteSpecification second = ((root1, delete, criteriaBuilder) -> secondPredicate); + + first.and(second).toPredicate(root, delete, builder); + + verify(builder).and(firstPredicate, secondPredicate); + } + + @Test // GH-3521 + void orCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + DeleteSpecification first = ((root1, delete, criteriaBuilder) -> firstPredicate); + DeleteSpecification second = ((root1, delete, criteriaBuilder) -> secondPredicate); + + first.or(second).toPredicate(root, delete, builder); + + verify(builder).or(firstPredicate, secondPredicate); + } + + static class SerializableSpecification implements Serializable, DeleteSpecification { + + @Override + public Predicate toPredicate(Root root, CriteriaDelete delete, CriteriaBuilder cb) { + return null; + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java new file mode 100644 index 0000000000..f2f8a83a43 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024 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.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.util.SerializationUtils.*; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serializable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Unit tests for {@link PredicateSpecification}. + * + * @author Mark Paluch + */ +@SuppressWarnings("serial") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PredicateSpecificationUnitTests implements Serializable { + + private PredicateSpecification spec; + @Mock(serializable = true) Root root; + @Mock(serializable = true) CriteriaBuilder builder; + @Mock(serializable = true) Predicate predicate; + @Mock(serializable = true) Predicate another; + + @BeforeEach + void setUp() { + spec = (root, cb) -> predicate; + } + + @Test // GH-3521 + void allReturnsEmptyPredicate() { + + PredicateSpecification specification = PredicateSpecification.all(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isNull(); + } + + @Test // GH-3521 + void allOfCombinesPredicatesInOrder() { + + PredicateSpecification specification = PredicateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void anyOfCombinesPredicatesInOrder() { + + PredicateSpecification specification = PredicateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void emptyAllOfReturnsEmptySpecification() { + + PredicateSpecification specification = PredicateSpecification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isNull(); + } + + @Test // GH-3521 + void emptyAnyOfReturnsEmptySpecification() { + + PredicateSpecification specification = PredicateSpecification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, builder)).isNull(); + } + + @Test // GH-3521 + void specificationsShouldBeSerializable() { + + PredicateSpecification serializableSpec = new SerializableSpecification(); + PredicateSpecification specification = serializableSpec.and(serializableSpec); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void complexSpecificationsShouldBeSerializable() { + + SerializableSpecification serializableSpec = new SerializableSpecification(); + PredicateSpecification specification = PredicateSpecification + .not(serializableSpec.and(serializableSpec).or(serializableSpec)); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void andCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + PredicateSpecification first = ((root1, criteriaBuilder) -> firstPredicate); + PredicateSpecification second = ((root1, criteriaBuilder) -> secondPredicate); + + first.and(second).toPredicate(root, builder); + + verify(builder).and(firstPredicate, secondPredicate); + } + + @Test // GH-3521 + void orCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + PredicateSpecification first = ((root1, criteriaBuilder) -> firstPredicate); + PredicateSpecification second = ((root1, criteriaBuilder) -> secondPredicate); + + first.or(second).toPredicate(root, builder); + + verify(builder).or(firstPredicate, secondPredicate); + } + + static class SerializableSpecification implements Serializable, PredicateSpecification { + + @Override + public Predicate toPredicate(Root root, CriteriaBuilder cb) { + return null; + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index 368ccc7ff5..38d2adbdb3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -17,8 +17,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.data.jpa.domain.Specification.*; -import static org.springframework.data.jpa.domain.Specification.not; import static org.springframework.util.SerializationUtils.*; import jakarta.persistence.criteria.CriteriaBuilder; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java new file mode 100644 index 0000000000..f66bba7d73 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2024 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.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.util.SerializationUtils.*; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import java.io.Serializable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** + * Unit tests for {@link UpdateSpecification}. + * + * @author Mark Paluch + */ +@SuppressWarnings("serial") +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class UpdateSpecificationUnitTests implements Serializable { + + private UpdateSpecification spec; + @Mock(serializable = true) Root root; + @Mock(serializable = true) CriteriaUpdate update; + @Mock(serializable = true) CriteriaBuilder builder; + @Mock(serializable = true) Predicate predicate; + @Mock(serializable = true) Predicate another; + + @BeforeEach + void setUp() { + spec = (root, update, cb) -> predicate; + } + + @Test // GH-3521 + void allReturnsEmptyPredicate() { + + UpdateSpecification specification = UpdateSpecification.all(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isNull(); + } + + @Test // GH-3521 + void allOfCombinesPredicatesInOrder() { + + UpdateSpecification specification = UpdateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void anyOfCombinesPredicatesInOrder() { + + UpdateSpecification specification = UpdateSpecification.allOf(spec); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isSameAs(predicate); + } + + @Test // GH-3521 + void emptyAllOfReturnsEmptySpecification() { + + UpdateSpecification specification = UpdateSpecification.allOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isNull(); + } + + @Test // GH-3521 + void emptyAnyOfReturnsEmptySpecification() { + + UpdateSpecification specification = UpdateSpecification.anyOf(); + + assertThat(specification).isNotNull(); + assertThat(specification.toPredicate(root, update, builder)).isNull(); + } + + @Test // GH-3521 + void specificationsShouldBeSerializable() { + + UpdateSpecification serializableSpec = new SerializableSpecification(); + UpdateSpecification specification = serializableSpec.and(serializableSpec); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void complexSpecificationsShouldBeSerializable() { + + SerializableSpecification serializableSpec = new SerializableSpecification(); + UpdateSpecification specification = UpdateSpecification + .not(serializableSpec.and(serializableSpec).or(serializableSpec)); + + assertThat(specification).isNotNull(); + + @SuppressWarnings("unchecked") + UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( + serialize(specification)); + + assertThat(transferredSpecification).isNotNull(); + } + + @Test // GH-3521 + void andCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + UpdateSpecification first = ((root1, update, criteriaBuilder) -> firstPredicate); + UpdateSpecification second = ((root1, update, criteriaBuilder) -> secondPredicate); + + first.and(second).toPredicate(root, update, builder); + + verify(builder).and(firstPredicate, secondPredicate); + } + + @Test // GH-3521 + void orCombinesSpecificationsInOrder() { + + Predicate firstPredicate = mock(Predicate.class); + Predicate secondPredicate = mock(Predicate.class); + + UpdateSpecification first = ((root1, update, criteriaBuilder) -> firstPredicate); + UpdateSpecification second = ((root1, update, criteriaBuilder) -> secondPredicate); + + first.or(second).toPredicate(root, update, builder); + + verify(builder).or(firstPredicate, secondPredicate); + } + + static class SerializableSpecification implements Serializable, UpdateSpecification { + + @Override + public Predicate toPredicate(Root root, CriteriaUpdate update, CriteriaBuilder cb) { + return null; + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java index 304dcb5607..cbd8ffd410 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserSpecifications.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jpa.domain.sample; +import org.springframework.data.jpa.domain.PredicateSpecification; import org.springframework.data.jpa.domain.Specification; /** @@ -25,24 +26,24 @@ */ public class UserSpecifications { - public static Specification userHasFirstname(final String firstname) { + public static PredicateSpecification userHasFirstname(final String firstname) { return simplePropertySpec("firstname", firstname); } - public static Specification userHasLastname(final String lastname) { + public static PredicateSpecification userHasLastname(final String lastname) { return simplePropertySpec("lastname", lastname); } - public static Specification userHasFirstnameLike(final String expression) { + public static PredicateSpecification userHasFirstnameLike(final String expression) { - return (root, query, cb) -> cb.like(root.get("firstname").as(String.class), String.format("%%%s%%", expression)); + return (root, cb) -> cb.like(root.get("firstname").as(String.class), String.format("%%%s%%", expression)); } - public static Specification userHasAgeLess(final Integer age) { + public static PredicateSpecification userHasAgeLess(final Integer age) { - return (root, query, cb) -> cb.lessThan(root.get("age").as(Integer.class), age); + return (root, cb) -> cb.lessThan(root.get("age").as(Integer.class), age); } public static Specification userHasLastnameLikeWithSort(final String expression) { @@ -55,8 +56,8 @@ public static Specification userHasLastnameLikeWithSort(final String expre }; } - private static Specification simplePropertySpec(final String property, final Object value) { + private static PredicateSpecification simplePropertySpec(final String property, final Object value) { - return (root, query, builder) -> builder.equal(root.get(property), value); + return (root, builder) -> builder.equal(root.get(property), value); } } 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 7feedd68fc..86f59e144d 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 @@ -20,8 +20,6 @@ import static org.springframework.data.domain.Example.*; import static org.springframework.data.domain.ExampleMatcher.*; 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.sample.UserSpecifications.*; import jakarta.persistence.EntityManager; @@ -62,7 +60,10 @@ import org.springframework.data.domain.*; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.jpa.domain.DeleteSpecification; +import org.springframework.data.jpa.domain.PredicateSpecification; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.UpdateSpecification; import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.QUser; import org.springframework.data.jpa.domain.sample.Role; @@ -470,7 +471,7 @@ void testExecutionOfProjectingMethod() { void executesSpecificationCorrectly() { flushTestUsers(); - assertThat(repository.findAll(where(userHasFirstname("Oliver")))).hasSize(1); + assertThat(repository.findAll(Specification.where(userHasFirstname("Oliver")))).hasSize(1); } @Test @@ -500,11 +501,11 @@ void throwsExceptionForUnderSpecifiedSingleEntitySpecification() { void executesCombinedSpecificationsCorrectly() { flushTestUsers(); - Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + PredicateSpecification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); List users1 = repository.findAll(spec1); assertThat(users1).hasSize(2); - Specification spec2 = Specification.anyOf( // + PredicateSpecification spec2 = PredicateSpecification.anyOf( // userHasFirstname("Oliver"), // userHasLastname("Arrasz")); List users2 = repository.findAll(spec2); @@ -517,7 +518,8 @@ void executesCombinedSpecificationsCorrectly() { void executesNegatingSpecificationCorrectly() { flushTestUsers(); - Specification spec = not(userHasFirstname("Oliver")).and(userHasLastname("Arrasz")); + PredicateSpecification spec = PredicateSpecification.not(userHasFirstname("Oliver")) + .and(userHasLastname("Arrasz")); assertThat(repository.findAll(spec)).containsOnly(secondUser); } @@ -526,18 +528,18 @@ void executesNegatingSpecificationCorrectly() { void executesCombinedSpecificationsWithPageableCorrectly() { flushTestUsers(); - Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + PredicateSpecification spec1 = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); - Page users1 = repository.findAll(spec1, PageRequest.of(0, 1)); + Page users1 = repository.findAll(Specification.where(spec1), PageRequest.of(0, 1)); assertThat(users1.getSize()).isOne(); assertThat(users1.hasPrevious()).isFalse(); assertThat(users1.getTotalElements()).isEqualTo(2L); - Specification spec2 = Specification.anyOf( // + PredicateSpecification spec2 = PredicateSpecification.anyOf( // userHasFirstname("Oliver"), // userHasLastname("Arrasz")); - Page users2 = repository.findAll(spec2, PageRequest.of(0, 1)); + Page users2 = repository.findAll(Specification.where(spec2), PageRequest.of(0, 1)); assertThat(users2.getSize()).isOne(); assertThat(users2.hasPrevious()).isFalse(); assertThat(users2.getTotalElements()).isEqualTo(2L); @@ -592,7 +594,7 @@ void executesSimpleNotCorrectly() { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll((Specification) null)); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.all())); } @Test @@ -608,15 +610,41 @@ void returnsSamePageIfNoSpecGiven() { Pageable pageable = PageRequest.of(0, 1); flushTestUsers(); - assertThat(repository.findAll((Specification) null, pageable)).isEqualTo(repository.findAll(pageable)); + assertThat(repository.findAll(Specification.all(), pageable)).isEqualTo(repository.findAll(pageable)); + } + + @Test // GH-3521 + void updateSpecificationUpdatesMarriedEntities() { + + flushTestUsers(); + + UpdateSpecification updateLastname = UpdateSpecification. update((root, update, criteriaBuilder) -> { + update.set("lastname", "Drotbohm"); + }).where(userHasFirstname("Oliver").and(userHasLastname("Gierke"))); + + long updated = repository.update(updateLastname); + + assertThat(updated).isOne(); + assertThat(repository.count(userHasFirstname("Oliver").and(userHasLastname("Gierke")))).isZero(); + assertThat(repository.count(userHasFirstname("Oliver").and(userHasLastname("Drotbohm")))).isOne(); + } + + @Test // GH-2796 + void predicateSpecificationRemovesAll() { + + flushTestUsers(); + + repository.delete(DeleteSpecification.all()); + + assertThat(repository.count()).isEqualTo(0L); } @Test // GH-2796 - void removesAllIfSpecificationIsNull() { + void deleteSpecificationRemovesAll() { flushTestUsers(); - repository.delete((Specification) null); + repository.delete(DeleteSpecification.all()); assertThat(repository.count()).isEqualTo(0L); } @@ -3396,8 +3424,8 @@ void existsWithSpec() { flushTestUsers(); - Specification minorSpec = userHasAgeLess(18); - Specification hundredYearsOld = userHasAgeLess(100); + PredicateSpecification minorSpec = userHasAgeLess(18); + PredicateSpecification hundredYearsOld = userHasAgeLess(100); assertThat(repository.exists(minorSpec)).isFalse(); assertThat(repository.exists(hundredYearsOld)).isTrue(); @@ -3422,7 +3450,7 @@ void deleteWithSpec() { flushTestUsers(); - Specification usersWithEInTheirName = userHasFirstnameLike("e"); + PredicateSpecification usersWithEInTheirName = userHasFirstnameLike("e"); long initialCount = repository.count(); assertThat(repository.delete(usersWithEInTheirName)).isEqualTo(3L); @@ -3569,16 +3597,16 @@ private Page executeSpecWithSort(Sort sort) { flushTestUsers(); - Specification spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + PredicateSpecification spec1 = userHasFirstname("Oliver").or(userHasLastname("Matthews")); - Page result1 = repository.findAll(spec1, PageRequest.of(0, 1, sort)); + Page result1 = repository.findAll(Specification.where(spec1), PageRequest.of(0, 1, sort)); assertThat(result1.getTotalElements()).isEqualTo(2L); - Specification spec2 = Specification.anyOf( // + PredicateSpecification spec2 = PredicateSpecification.anyOf( // userHasFirstname("Oliver"), // userHasLastname("Matthews")); - Page result2 = repository.findAll(spec2, PageRequest.of(0, 1, sort)); + Page result2 = repository.findAll(Specification.where(spec2), PageRequest.of(0, 1, sort)); assertThat(result2.getTotalElements()).isEqualTo(2L); assertThat(result1).containsExactlyElementsOf(result2); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index afd9634b44..ba3d607701 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java @@ -46,6 +46,7 @@ import org.mockito.quality.Strictness; import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; import org.springframework.data.repository.CrudRepository; @@ -218,7 +219,7 @@ void applyQueryHintsToCountQueriesForSpecificationPageables() { when(query.getResultList()).thenReturn(Arrays.asList(new User(), new User())); - repo.findAll(where(null), PageRequest.of(2, 1)); + repo.findAll(Specification.all(), PageRequest.of(2, 1)); verify(metadata).getQueryHintsForCount(); } From 6fac30a5cc7415e19e17d8379761b8ab97df959f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 19 Aug 2024 15:41:07 +0200 Subject: [PATCH 25/94] Remove Specification.where method in favour of all(). Also remove serialVersionUID. Original Pull Request: #3578 --- .../data/jpa/domain/DeleteSpecification.java | 3 --- .../jpa/domain/PredicateSpecification.java | 3 --- .../data/jpa/domain/Specification.java | 22 +------------------ .../data/jpa/domain/UpdateSpecification.java | 3 --- 4 files changed, 1 insertion(+), 30 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index b3bfd93ae2..738ad212ce 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -20,7 +20,6 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import java.io.Serial; import java.io.Serializable; import java.util.Arrays; import java.util.stream.StreamSupport; @@ -37,8 +36,6 @@ @FunctionalInterface public interface DeleteSpecification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification deleting all objects. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index b3e52f4249..49ff92c5ba 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -19,7 +19,6 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import java.io.Serial; import java.io.Serializable; import java.util.Arrays; import java.util.stream.StreamSupport; @@ -35,8 +34,6 @@ */ public interface PredicateSpecification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification matching all objects. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 7908ad1a77..73e45e308f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -21,7 +21,6 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import java.io.Serial; import java.io.Serializable; import java.util.Arrays; import java.util.stream.StreamSupport; @@ -44,8 +43,6 @@ @FunctionalInterface public interface Specification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification matching all objects. * @@ -56,23 +53,6 @@ static Specification all() { return (root, query, builder) -> null; } - /** - * Simple static factory method to add some syntactic sugar around a {@link Specification}. - * - * @param the type of the {@link Root} the resulting {@literal Specification} operates on. - * @param spec must not be {@literal null}. - * @return guaranteed to be not {@literal null}. - * @since 2.0 - * @deprecated since 3.5. - */ - @Deprecated(since = "3.5.0", forRemoval = true) - static Specification where(Specification spec) { - - Assert.notNull(spec, "Specification must not be null"); - - return spec; - } - /** * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to * {@link Specification}. @@ -85,7 +65,7 @@ static Specification where(PredicateSpecification spec) { Assert.notNull(spec, "PredicateSpecification must not be null"); - return where((root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder)); + return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java index 8e217fc0f4..2872b0ab0f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -20,7 +20,6 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import java.io.Serial; import java.io.Serializable; import java.util.Arrays; import java.util.stream.StreamSupport; @@ -37,8 +36,6 @@ @FunctionalInterface public interface UpdateSpecification extends Serializable { - @Serial long serialVersionUID = 1L; - /** * Simple static factory method to create a specification deleting all objects. * From a3e85185e44e2c360c765f7758783bc4612d8568 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 20 Aug 2024 14:47:34 +0200 Subject: [PATCH 26/94] Polishing. Revise nullability requirements around non-nullable specifications. Original Pull Request: #3578 --- .../repository/JpaSpecificationExecutor.java | 3 + .../FetchableFluentQueryBySpecification.java | 8 +- .../support/SimpleJpaRepository.java | 32 ++++---- .../jpa/domain/SpecificationUnitTests.java | 73 ------------------- 4 files changed, 27 insertions(+), 89 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index da03816ec4..f4fac79516 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -25,6 +25,9 @@ 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.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java index 5d87904ec3..68b4eb2582 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java @@ -26,6 +26,8 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -174,18 +176,18 @@ public Window scroll(ScrollPosition scrollPosition) { @Override public Slice slice(Pageable pageable) { - return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSortOr(this.sort))) : readSlice(pageable); + return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSort())) : readSlice(pageable); } @Override public Page page(Pageable pageable) { - return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSortOr(this.sort))) : readPage(pageable, spec); + return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSort())) : readPage(pageable, spec); } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public Page page(Pageable pageable, Specification countSpec) { - return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSortOr(this.sort))) + return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSort())) : readPage(pageable, (Specification) countSpec); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 2f18aeca98..f006c4cd96 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -513,6 +513,7 @@ public R findBy(Specification spec, return doFindBy(spec, getDomainClass(), queryFunction); } + @SuppressWarnings("unchecked") private R doFindBy(Specification spec, Class domainClass, Function, R> queryFunction) { @@ -610,6 +611,7 @@ public Page findAll(Example example, Pageable pageable) { } @Override + @SuppressWarnings("unchecked") public R findBy(Example example, Function, R> queryFunction) { Assert.notNull(example, EXAMPLE_MUST_NOT_BE_NULL); @@ -632,7 +634,7 @@ public long count() { } @Override - public long count(@Nullable Specification spec) { + public long count(Specification spec) { return executeCountQuery(getCountQuery(spec, getDomainClass())); } @@ -701,7 +703,7 @@ public void flush() { * @deprecated use {@link #readPage(TypedQuery, Class, Pageable, Specification)} instead */ @Deprecated - protected Page readPage(TypedQuery query, Pageable pageable, @Nullable Specification spec) { + protected Page readPage(TypedQuery query, Pageable pageable, Specification spec) { return readPage(query, getDomainClass(), pageable, spec); } @@ -711,11 +713,13 @@ protected Page readPage(TypedQuery query, Pageable pageable, @Nullable Spe * * @param query must not be {@literal null}. * @param domainClass must not be {@literal null}. - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param pageable can be {@literal null}. */ protected Page readPage(TypedQuery query, Class domainClass, Pageable pageable, - @Nullable Specification spec) { + Specification spec) { + + Assert.notNull(spec, "Specification must not be null"); if (pageable.isPaged()) { query.setFirstResult(PageableUtils.getOffsetAsInteger(pageable)); @@ -729,21 +733,21 @@ protected Page readPage(TypedQuery query, Class domainCla /** * Creates a new {@link TypedQuery} from the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Pageable pageable) { + protected TypedQuery getQuery(Specification spec, Pageable pageable) { return getQuery(spec, getDomainClass(), pageable.getSort()); } /** * Creates a new {@link TypedQuery} from the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param domainClass must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(@Nullable Specification spec, Class domainClass, + protected TypedQuery getQuery(Specification spec, Class domainClass, Pageable pageable) { return getQuery(spec, domainClass, pageable.getSort()); } @@ -877,21 +881,23 @@ protected Query getDelete(DeleteSpecification spec, Class domainClass) /** * Creates a new count query for the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @deprecated override {@link #getCountQuery(Specification, Class)} instead */ @Deprecated - protected TypedQuery getCountQuery(@Nullable Specification spec) { + protected TypedQuery getCountQuery(Specification spec) { return getCountQuery(spec, getDomainClass()); } /** * Creates a new count query for the given {@link Specification}. * - * @param spec can be {@literal null}. + * @param spec must not be {@literal null}. * @param domainClass must not be {@literal null}. */ - protected TypedQuery getCountQuery(@Nullable Specification spec, Class domainClass) { + protected TypedQuery getCountQuery(Specification spec, Class domainClass) { + + Assert.notNull(spec, "Specification must not be null"); CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(Long.class); @@ -1041,7 +1047,7 @@ private Map getHints() { private void applyComment(CrudMethodMetadata metadata, BiConsumer consumer) { if (metadata.getComment() != null && provider.getCommentHintKey() != null) { - consumer.accept(provider.getCommentHintKey(), provider.getCommentHintValue(this.metadata.getComment())); + consumer.accept(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment())); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index 38d2adbdb3..fc1bc5431c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -62,79 +62,6 @@ void setUp() { spec = (root, query, cb) -> predicate; } - @Test // DATAJPA-300, DATAJPA-1170 - void createsSpecificationsFromNull() { - - Specification specification = where(null); - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isNull(); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void negatesNullSpecToNull() { - - Specification specification = not(null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isNull(); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void andConcatenatesSpecToNullSpec() { - - Specification specification = where(null); - specification = specification.and(spec); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void andConcatenatesNullSpecToSpec() { - - Specification specification = spec.and(null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void orConcatenatesSpecToNullSpec() { - - Specification specification = where(null); - specification = specification.or(spec); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // DATAJPA-300, DATAJPA-1170 - void orConcatenatesNullSpecToSpec() { - - Specification specification = spec.or(null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // GH-1943 - void allOfConcatenatesNull() { - - Specification specification = Specification.allOf(null, spec, null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - - @Test // GH-1943 - void anyOfConcatenatesNull() { - - Specification specification = Specification.anyOf(null, spec, null); - - assertThat(specification).isNotNull(); - assertThat(specification.toPredicate(root, query, builder)).isEqualTo(predicate); - } - @Test // GH-1943 void emptyAllOfReturnsEmptySpecification() { From b863511ff0c90399034e23730b0052d703b6b056 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 7 Nov 2024 15:56:30 +0100 Subject: [PATCH 27/94] Add Contract annotations. Original Pull Request: #3578 --- .../data/jpa/domain/DeleteSpecification.java | 12 +++++++++++- .../jpa/domain/PredicateSpecification.java | 10 ++++++++-- .../data/jpa/domain/Specification.java | 10 ++++++++++ .../data/jpa/domain/UpdateSpecification.java | 19 ++++++++++++++++++- .../support/SimpleJpaRepository.java | 10 +++++----- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index 738ad212ce..310ed0c6da 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.stream.StreamSupport; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -31,7 +33,7 @@ * Specification in the sense of Domain Driven Design to handle Criteria Deletes. * * @author Mark Paluch - * @since xxx + * @since 4.0 */ @FunctionalInterface public interface DeleteSpecification extends Serializable { @@ -81,6 +83,8 @@ static DeleteSpecification where(PredicateSpecification spec) { * @param other the other {@link DeleteSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default DeleteSpecification and(DeleteSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -94,6 +98,8 @@ default DeleteSpecification and(DeleteSpecification other) { * @param other the other {@link PredicateSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default DeleteSpecification and(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -107,6 +113,8 @@ default DeleteSpecification and(PredicateSpecification other) { * @param other the other {@link DeleteSpecification}. * @return the disjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default DeleteSpecification or(DeleteSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -120,6 +128,8 @@ default DeleteSpecification or(DeleteSpecification other) { * @param other the other {@link PredicateSpecification}. * @return the disjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default DeleteSpecification or(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index 49ff92c5ba..f237715bc0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -23,6 +23,8 @@ import java.util.Arrays; import java.util.stream.StreamSupport; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -30,7 +32,7 @@ * Specification in the sense of Domain Driven Design. * * @author Mark Paluch - * @since xxx + * @since 4.0 */ public interface PredicateSpecification extends Serializable { @@ -54,7 +56,7 @@ static PredicateSpecification all() { */ static PredicateSpecification where(PredicateSpecification spec) { - Assert.notNull(spec, "DeleteSpecification must not be null"); + Assert.notNull(spec, "PredicateSpecification must not be null"); return spec; } @@ -65,6 +67,8 @@ static PredicateSpecification where(PredicateSpecification spec) { * @param other the other {@link PredicateSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default PredicateSpecification and(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -78,6 +82,8 @@ default PredicateSpecification and(PredicateSpecification other) { * @param other the other {@link PredicateSpecification}. * @return the disjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default PredicateSpecification or(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 73e45e308f..975d52d6ec 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -25,6 +25,8 @@ import java.util.Arrays; import java.util.stream.StreamSupport; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -75,6 +77,8 @@ static Specification where(PredicateSpecification spec) { * @return the conjunction of the specifications. * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification and(Specification other) { Assert.notNull(other, "Other specification must not be null"); @@ -89,6 +93,8 @@ default Specification and(Specification other) { * @return the conjunction of the specifications. * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification and(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -103,6 +109,8 @@ default Specification and(PredicateSpecification other) { * @return the disjunction of the specifications * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification or(Specification other) { Assert.notNull(other, "Other specification must not be null"); @@ -117,6 +125,8 @@ default Specification or(Specification other) { * @return the disjunction of the specifications * @since 2.0 */ + @Contract("_ -> new") + @CheckReturnValue default Specification or(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java index 2872b0ab0f..7667faa9c4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.stream.StreamSupport; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -31,7 +33,7 @@ * Specification in the sense of Domain Driven Design to handle Criteria Updates. * * @author Mark Paluch - * @since xxx + * @since 4.0 */ @FunctionalInterface public interface UpdateSpecification extends Serializable { @@ -103,6 +105,8 @@ static UpdateSpecification where(PredicateSpecification spec) { * @param other the other {@link UpdateSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateSpecification and(UpdateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -116,6 +120,8 @@ default UpdateSpecification and(UpdateSpecification other) { * @param other the other {@link PredicateSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateSpecification and(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -129,6 +135,8 @@ default UpdateSpecification and(PredicateSpecification other) { * @param other the other {@link UpdateSpecification}. * @return the disjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateSpecification or(UpdateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -142,6 +150,8 @@ default UpdateSpecification or(UpdateSpecification other) { * @param other the other {@link PredicateSpecification}. * @return the disjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateSpecification or(PredicateSpecification other) { Assert.notNull(other, "Other specification must not be null"); @@ -256,6 +266,8 @@ interface UpdateOperation { * @param other the other {@link UpdateOperation}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateOperation and(UpdateOperation other) { Assert.notNull(other, "Other UpdateOperation must not be null"); @@ -272,6 +284,8 @@ default UpdateOperation and(UpdateOperation other) { * @param specification the {@link PredicateSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateSpecification where(PredicateSpecification specification) { Assert.notNull(specification, "PredicateSpecification must not be null"); @@ -288,6 +302,8 @@ default UpdateSpecification where(PredicateSpecification specification) { * @param specification the {@link UpdateSpecification}. * @return the conjunction of the specifications. */ + @Contract("_ -> new") + @CheckReturnValue default UpdateSpecification where(UpdateSpecification specification) { Assert.notNull(specification, "UpdateSpecification must not be null"); @@ -306,6 +322,7 @@ default UpdateSpecification where(UpdateSpecification specification) { * @param criteriaBuilder must not be {@literal null}. */ void apply(Root root, CriteriaUpdate update, CriteriaBuilder criteriaBuilder); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index f006c4cd96..99d25f49d1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -262,7 +262,7 @@ public void deleteAllByIdInBatch(Iterable ids) { /* * Some JPA providers require {@code ids} to be a {@link Collection} so we must convert if it's not already. */ - Collection idCollection = toCollection(ids); + Collection idCollection = toCollection(ids); query.setParameter("ids", idCollection); applyQueryHints(query); @@ -747,8 +747,7 @@ protected TypedQuery getQuery(Specification spec, Pageable pageable) { * @param domainClass must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(Specification spec, Class domainClass, - Pageable pageable) { + protected TypedQuery getQuery(Specification spec, Class domainClass, Pageable pageable) { return getQuery(spec, domainClass, pageable.getSort()); } @@ -1095,7 +1094,7 @@ private static long executeCountQuery(TypedQuery query) { @SuppressWarnings("rawtypes") private static final class ByIdsSpecification implements Specification { - @Serial private static final long serialVersionUID = 1L; + @Serial private static final @Serial long serialVersionUID = 1L; private final JpaEntityInformation entityInformation; @@ -1106,6 +1105,7 @@ private static final class ByIdsSpecification implements Specification { } @Override + @SuppressWarnings("unchecked") public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { Path path = root.get(entityInformation.getIdAttribute()); @@ -1124,7 +1124,7 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild */ private static class ExampleSpecification implements Specification { - @Serial private static final long serialVersionUID = 1L; + @Serial private static final @Serial long serialVersionUID = 1L; private final Example example; private final EscapeCharacter escapeCharacter; From fd878f3c744e73d7750f72f49a514d7407d3f76b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 11:24:51 +0100 Subject: [PATCH 28/94] Address review findings. Original Pull Request: #3578 --- .../data/jpa/domain/DeleteSpecification.java | 28 ++++++++++----- .../jpa/domain/PredicateSpecification.java | 28 ++++++++++----- .../data/jpa/domain/Specification.java | 28 ++++++++++----- .../data/jpa/domain/UpdateSpecification.java | 36 ++++++++++++------- .../repository/JpaSpecificationExecutor.java | 24 ++++++------- .../support/SimpleJpaRepository.java | 10 +++--- .../domain/DeleteSpecificationUnitTests.java | 2 +- .../PredicateSpecificationUnitTests.java | 2 +- .../domain/UpdateSpecificationUnitTests.java | 2 +- .../jpa/repository/UserRepositoryTests.java | 8 ++--- .../support/SimpleJpaRepositoryUnitTests.java | 2 +- 11 files changed, 105 insertions(+), 65 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index 310ed0c6da..3337ae5fb1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -31,6 +31,12 @@ /** * Specification in the sense of Domain Driven Design to handle Criteria Deletes. + *

+ * Specifications can be composed into higher order functions from other specifications using + * {@link #and(DeleteSpecification)}, {@link #or(DeleteSpecification)} or factory methods such as + * {@link #allOf(Iterable)}. Composition considers whether one or more specifications contribute to the overall + * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are + * considered to not contribute to the overall predicate and their result is not considered in the final predicate. * * @author Mark Paluch * @since 4.0 @@ -44,7 +50,7 @@ public interface DeleteSpecification extends Serializable { * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. * @return guaranteed to be not {@literal null}. */ - static DeleteSpecification all() { + static DeleteSpecification unrestricted() { return (root, query, builder) -> null; } @@ -150,13 +156,14 @@ static DeleteSpecification not(DeleteSpecification spec) { return (root, delete, builder) -> { - Predicate not = spec.toPredicate(root, delete, builder); - return not != null ? builder.not(not) : null; + Predicate predicate = spec.toPredicate(root, delete, builder); + return predicate != null ? builder.not(predicate) : null; }; } /** - * Applies an AND operation to all the given {@link DeleteSpecification}s. + * Applies an AND operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the + * resulting {@link DeleteSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the conjunction of the specifications. @@ -169,7 +176,8 @@ static DeleteSpecification allOf(DeleteSpecification... specifications } /** - * Applies an AND operation to all the given {@link DeleteSpecification}s. + * Applies an AND operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the + * resulting {@link DeleteSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the conjunction of the specifications. @@ -179,11 +187,12 @@ static DeleteSpecification allOf(DeleteSpecification... specifications static DeleteSpecification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(DeleteSpecification.all(), DeleteSpecification::and); + .reduce(DeleteSpecification.unrestricted(), DeleteSpecification::and); } /** - * Applies an OR operation to all the given {@link DeleteSpecification}s. + * Applies an OR operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the + * resulting {@link DeleteSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the disjunction of the specifications. @@ -196,7 +205,8 @@ static DeleteSpecification anyOf(DeleteSpecification... specifications } /** - * Applies an OR operation to all the given {@link DeleteSpecification}s. + * Applies an OR operation to all the given {@link DeleteSpecification}s. If {@code specifications} is empty, the + * resulting {@link DeleteSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the disjunction of the specifications. @@ -206,7 +216,7 @@ static DeleteSpecification anyOf(DeleteSpecification... specifications static DeleteSpecification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(DeleteSpecification.all(), DeleteSpecification::or); + .reduce(DeleteSpecification.unrestricted(), DeleteSpecification::or); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index f237715bc0..dc17edbfc4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -30,6 +30,12 @@ /** * Specification in the sense of Domain Driven Design. + *

+ * Specifications can be composed into higher order functions from other specifications using + * {@link #and(PredicateSpecification)}, {@link #or(PredicateSpecification)} or factory methods such as + * {@link #allOf(Iterable)}. Composition considers whether one or more specifications contribute to the overall + * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are + * considered to not contribute to the overall predicate and their result is not considered in the final predicate. * * @author Mark Paluch * @since 4.0 @@ -42,7 +48,7 @@ public interface PredicateSpecification extends Serializable { * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. * @return guaranteed to be not {@literal null}. */ - static PredicateSpecification all() { + static PredicateSpecification unrestricted() { return (root, builder) -> null; } @@ -104,13 +110,14 @@ static PredicateSpecification not(PredicateSpecification spec) { return (root, builder) -> { - Predicate not = spec.toPredicate(root, builder); - return not != null ? builder.not(not) : null; + Predicate predicate = spec.toPredicate(root, builder); + return predicate != null ? builder.not(predicate) : null; }; } /** - * Applies an AND operation to all the given {@link PredicateSpecification}s. + * Applies an AND operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the + * resulting {@link PredicateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the conjunction of the specifications. @@ -123,7 +130,8 @@ static PredicateSpecification allOf(PredicateSpecification... specific } /** - * Applies an AND operation to all the given {@link PredicateSpecification}s. + * Applies an AND operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the + * resulting {@link PredicateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the conjunction of the specifications. @@ -133,11 +141,12 @@ static PredicateSpecification allOf(PredicateSpecification... specific static PredicateSpecification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(PredicateSpecification.all(), PredicateSpecification::and); + .reduce(PredicateSpecification.unrestricted(), PredicateSpecification::and); } /** - * Applies an OR operation to all the given {@link PredicateSpecification}s. + * Applies an OR operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the + * resulting {@link PredicateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the disjunction of the specifications. @@ -150,7 +159,8 @@ static PredicateSpecification anyOf(PredicateSpecification... specific } /** - * Applies an OR operation to all the given {@link PredicateSpecification}s. + * Applies an OR operation to all the given {@link PredicateSpecification}s. If {@code specifications} is empty, the + * resulting {@link PredicateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the disjunction of the specifications. @@ -160,7 +170,7 @@ static PredicateSpecification anyOf(PredicateSpecification... specific static PredicateSpecification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(PredicateSpecification.all(), PredicateSpecification::or); + .reduce(PredicateSpecification.unrestricted(), PredicateSpecification::or); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 975d52d6ec..b0b44dc0f6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -32,6 +32,12 @@ /** * Specification in the sense of Domain Driven Design. + *

+ * Specifications can be composed into higher order functions from other specifications using + * {@link #and(Specification)}, {@link #or(Specification)} or factory methods such as {@link #allOf(Iterable)}. + * Composition considers whether one or more specifications contribute to the overall predicate by returning a + * {@link Predicate} or {@literal null}. Specifications returning {@literal null} are considered to not contribute to + * the overall predicate and their result is not considered in the final predicate. * * @author Oliver Gierke * @author Thomas Darimont @@ -51,7 +57,7 @@ public interface Specification extends Serializable { * @param the type of the {@link Root} the resulting {@literal Specification} operates on. * @return guaranteed to be not {@literal null}. */ - static Specification all() { + static Specification unrestricted() { return (root, query, builder) -> null; } @@ -148,13 +154,14 @@ static Specification not(Specification spec) { return (root, query, builder) -> { - Predicate not = spec.toPredicate(root, query, builder); - return not != null ? builder.not(not) : null; + Predicate predicate = spec.toPredicate(root, query, builder); + return predicate != null ? builder.not(predicate) : null; }; } /** - * Applies an AND operation to all the given {@link Specification}s. + * Applies an AND operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting + * {@link Specification} will be unrestricted applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the conjunction of the specifications. @@ -168,7 +175,8 @@ static Specification allOf(Specification... specifications) { } /** - * Applies an AND operation to all the given {@link Specification}s. + * Applies an AND operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting + * {@link Specification} will be unrestricted applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the conjunction of the specifications. @@ -179,11 +187,12 @@ static Specification allOf(Specification... specifications) { static Specification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.all(), Specification::and); + .reduce(Specification.unrestricted(), Specification::and); } /** - * Applies an OR operation to all the given {@link Specification}s. + * Applies an OR operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting + * {@link Specification} will be unrestricted applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the disjunction of the specifications @@ -197,7 +206,8 @@ static Specification anyOf(Specification... specifications) { } /** - * Applies an OR operation to all the given {@link Specification}s. + * Applies an OR operation to all the given {@link Specification}s. If {@code specifications} is empty, the resulting + * {@link Specification} will be unrestricted applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the disjunction of the specifications @@ -208,7 +218,7 @@ static Specification anyOf(Specification... specifications) { static Specification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(Specification.all(), Specification::or); + .reduce(Specification.unrestricted(), Specification::or); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java index 7667faa9c4..2e9d93b82a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -31,6 +31,12 @@ /** * Specification in the sense of Domain Driven Design to handle Criteria Updates. + *

+ * Specifications can be composed into higher order functions from other specifications using + * {@link #and(UpdateSpecification)}, {@link #or(UpdateSpecification)} or factory methods such as + * {@link #allOf(Iterable)}. Composition considers whether one or more specifications contribute to the overall + * predicate by returning a {@link Predicate} or {@literal null}. Specifications returning {@literal null} are + * considered to not contribute to the overall predicate and their result is not considered in the final predicate. * * @author Mark Paluch * @since 4.0 @@ -39,27 +45,27 @@ public interface UpdateSpecification extends Serializable { /** - * Simple static factory method to create a specification deleting all objects. + * Simple static factory method to create a specification updating all objects. * * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. * @return guaranteed to be not {@literal null}. */ - static UpdateSpecification all() { + static UpdateSpecification unrestricted() { return (root, query, builder) -> null; } /** - * Simple static factory method to add some syntactic sugar around a {@literal UpdateSpecification}. For example: + * Simple static factory method to add some syntactic sugar around a {@literal UpdateOperation}. For example: * *

-	 * UpdateSpecification<User> updateLastname = UpdateSpecification
+	 * UpdateSpecification<User> updateLastname = UpdateOperation
 	 * 		.<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Heisenberg"))
 	 * 		.where(userHasFirstname("Walter").and(userHasLastname("White")));
 	 *
 	 * repository.update(updateLastname);
 	 * 
* - * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. + * @param the type of the {@link Root} the resulting {@literal UpdateOperation} operates on. * @param spec must not be {@literal null}. * @return guaranteed to be not {@literal null}. */ @@ -172,13 +178,14 @@ static UpdateSpecification not(UpdateSpecification spec) { return (root, update, builder) -> { - Predicate not = spec.toPredicate(root, update, builder); - return not != null ? builder.not(not) : null; + Predicate predicate = spec.toPredicate(root, update, builder); + return predicate != null ? builder.not(predicate) : null; }; } /** - * Applies an AND operation to all the given {@link UpdateSpecification}s. + * Applies an AND operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the + * resulting {@link UpdateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the conjunction of the specifications. @@ -191,7 +198,8 @@ static UpdateSpecification allOf(UpdateSpecification... specifications } /** - * Applies an AND operation to all the given {@link UpdateSpecification}s. + * Applies an AND operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the + * resulting {@link UpdateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the conjunction of the specifications. @@ -201,11 +209,12 @@ static UpdateSpecification allOf(UpdateSpecification... specifications static UpdateSpecification allOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(UpdateSpecification.all(), UpdateSpecification::and); + .reduce(UpdateSpecification.unrestricted(), UpdateSpecification::and); } /** - * Applies an OR operation to all the given {@link UpdateSpecification}s. + * Applies an OR operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the + * resulting {@link UpdateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the disjunction of the specifications. @@ -218,7 +227,8 @@ static UpdateSpecification anyOf(UpdateSpecification... specifications } /** - * Applies an OR operation to all the given {@link UpdateSpecification}s. + * Applies an OR operation to all the given {@link UpdateSpecification}s. If {@code specifications} is empty, the + * resulting {@link UpdateSpecification} will be unrestricted applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the disjunction of the specifications. @@ -228,7 +238,7 @@ static UpdateSpecification anyOf(UpdateSpecification... specifications static UpdateSpecification anyOf(Iterable> specifications) { return StreamSupport.stream(specifications.spliterator(), false) // - .reduce(UpdateSpecification.all(), UpdateSpecification::or); + .reduce(UpdateSpecification.unrestricted(), UpdateSpecification::or); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index f4fac79516..65e352105f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -60,7 +60,7 @@ public interface JpaSpecificationExecutor { * @param spec must not be {@literal null}. * @return never {@literal null}. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. - * @see Specification#all() + * @see Specification#unrestricted() */ default Optional findOne(PredicateSpecification spec) { return findOne(Specification.where(spec)); @@ -72,7 +72,7 @@ default Optional findOne(PredicateSpecification spec) { * @param spec must not be {@literal null}. * @return never {@literal null}. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. - * @see Specification#all() + * @see Specification#unrestricted() */ Optional findOne(Specification spec); @@ -81,7 +81,7 @@ default Optional findOne(PredicateSpecification spec) { * * @param spec must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ default List findAll(PredicateSpecification spec) { return findAll(Specification.where(spec)); @@ -92,7 +92,7 @@ default List findAll(PredicateSpecification spec) { * * @param spec must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ List findAll(Specification spec); @@ -102,7 +102,7 @@ default List findAll(PredicateSpecification spec) { * @param spec must not be {@literal null}. * @param pageable must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ Page findAll(Specification spec, Pageable pageable); @@ -128,7 +128,7 @@ default List findAll(PredicateSpecification spec) { * @param spec must not be {@literal null}. * @param sort must not be {@literal null}. * @return never {@literal null}. - * @see Specification#all() + * @see Specification#unrestricted() */ List findAll(Specification spec, Sort sort); @@ -137,7 +137,7 @@ default List findAll(PredicateSpecification spec) { * * @param spec the {@link PredicateSpecification} to count instances for, must not be {@literal null}. * @return the number of instances. - * @see Specification#all() + * @see Specification#unrestricted() */ default long count(PredicateSpecification spec) { return count(Specification.where(spec)); @@ -148,7 +148,7 @@ default long count(PredicateSpecification spec) { * * @param spec the {@link Specification} to count instances for, must not be {@literal null}. * @return the number of instances. - * @see Specification#all() + * @see Specification#unrestricted() */ long count(Specification spec); @@ -158,7 +158,7 @@ default long count(PredicateSpecification spec) { * @param spec the {@link PredicateSpecification} to use for the existence check, must not be {@literal null}. * @return {@code true} if the data store contains elements that match the given {@link PredicateSpecification} * otherwise {@code false}. - * @see Specification#all() + * @see Specification#unrestricted() */ default boolean exists(PredicateSpecification spec) { return exists(Specification.where(spec)); @@ -170,7 +170,7 @@ default boolean exists(PredicateSpecification spec) { * @param spec the {@link Specification} to use for the existence check, must not be {@literal null}. * @return {@code true} if the data store contains elements that match the given {@link Specification} otherwise * {@code false}. - * @see Specification#all() + * @see Specification#unrestricted() */ boolean exists(Specification spec); @@ -195,7 +195,7 @@ default boolean exists(PredicateSpecification spec) { * @param spec the {@link PredicateSpecification} to use for the delete query, must not be {@literal null}. * @return the number of entities deleted. * @since 3.0 - * @see PredicateSpecification#all() + * @see PredicateSpecification#unrestricted() */ default long delete(PredicateSpecification spec) { return delete(DeleteSpecification.where(spec)); @@ -210,7 +210,7 @@ default long delete(PredicateSpecification spec) { * @param spec the {@link UpdateSpecification} to use for the delete query must not be {@literal null}. * @return the number of entities deleted. * @since 3.0 - * @see DeleteSpecification#all() + * @see DeleteSpecification#unrestricted() */ long delete(DeleteSpecification spec); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 99d25f49d1..dbbdea2d15 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -401,7 +401,7 @@ public boolean existsById(ID id) { @Override public List findAll() { - return getQuery(Specification.all(), Sort.unsorted()).getResultList(); + return getQuery(Specification.unrestricted(), Sort.unsorted()).getResultList(); } @Override @@ -434,12 +434,12 @@ public List findAllById(Iterable ids) { @Override public List findAll(Sort sort) { - return getQuery(Specification.all(), sort).getResultList(); + return getQuery(Specification.unrestricted(), sort).getResultList(); } @Override public Page findAll(Pageable pageable) { - return findAll(Specification.all(), pageable); + return findAll(Specification.unrestricted(), pageable); } @Override @@ -1094,7 +1094,7 @@ private static long executeCountQuery(TypedQuery query) { @SuppressWarnings("rawtypes") private static final class ByIdsSpecification implements Specification { - @Serial private static final @Serial long serialVersionUID = 1L; + private static final @Serial long serialVersionUID = 1L; private final JpaEntityInformation entityInformation; @@ -1124,7 +1124,7 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild */ private static class ExampleSpecification implements Specification { - @Serial private static final @Serial long serialVersionUID = 1L; + private static final @Serial long serialVersionUID = 1L; private final Example example; private final EscapeCharacter escapeCharacter; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java index 79e531ad7f..02e59fa2db 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java @@ -59,7 +59,7 @@ void setUp() { @Test // GH-3521 void allReturnsEmptyPredicate() { - DeleteSpecification specification = DeleteSpecification.all(); + DeleteSpecification specification = DeleteSpecification.unrestricted(); assertThat(specification).isNotNull(); assertThat(specification.toPredicate(root, delete, builder)).isNull(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java index f2f8a83a43..f0cd8ca085 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java @@ -57,7 +57,7 @@ void setUp() { @Test // GH-3521 void allReturnsEmptyPredicate() { - PredicateSpecification specification = PredicateSpecification.all(); + PredicateSpecification specification = PredicateSpecification.unrestricted(); assertThat(specification).isNotNull(); assertThat(specification.toPredicate(root, builder)).isNull(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java index f66bba7d73..540cc91e40 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java @@ -59,7 +59,7 @@ void setUp() { @Test // GH-3521 void allReturnsEmptyPredicate() { - UpdateSpecification specification = UpdateSpecification.all(); + UpdateSpecification specification = UpdateSpecification.unrestricted(); assertThat(specification).isNotNull(); assertThat(specification.toPredicate(root, update, builder)).isNull(); 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 86f59e144d..478cff1ab9 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 @@ -594,7 +594,7 @@ void executesSimpleNotCorrectly() { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.all())); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.unrestricted())); } @Test @@ -610,7 +610,7 @@ void returnsSamePageIfNoSpecGiven() { Pageable pageable = PageRequest.of(0, 1); flushTestUsers(); - assertThat(repository.findAll(Specification.all(), pageable)).isEqualTo(repository.findAll(pageable)); + assertThat(repository.findAll(Specification.unrestricted(), pageable)).isEqualTo(repository.findAll(pageable)); } @Test // GH-3521 @@ -634,7 +634,7 @@ void predicateSpecificationRemovesAll() { flushTestUsers(); - repository.delete(DeleteSpecification.all()); + repository.delete(DeleteSpecification.unrestricted()); assertThat(repository.count()).isEqualTo(0L); } @@ -644,7 +644,7 @@ void deleteSpecificationRemovesAll() { flushTestUsers(); - repository.delete(DeleteSpecification.all()); + repository.delete(DeleteSpecification.unrestricted()); assertThat(repository.count()).isEqualTo(0L); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index ba3d607701..583ab2330f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java @@ -219,7 +219,7 @@ void applyQueryHintsToCountQueriesForSpecificationPageables() { when(query.getResultList()).thenReturn(Arrays.asList(new User(), new User())); - repo.findAll(Specification.all(), PageRequest.of(2, 1)); + repo.findAll(Specification.unrestricted(), PageRequest.of(2, 1)); verify(metadata).getQueryHintsForCount(); } From de7dd80a70c44eb508888447e9f8764b67962d82 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Jan 2025 11:37:41 +0100 Subject: [PATCH 29/94] Polishing. Refine JOIN and function keyword rendering. See #3692 --- .../query/HqlCountQueryTransformer.java | 1 + .../repository/query/HqlQueryRenderer.java | 19 ++++++++++--------- .../jpa/repository/query/QueryTokens.java | 1 + 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java index ed66a41ad9..c85ec88bda 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java @@ -160,6 +160,7 @@ public QueryRendererBuilder visitJoin(HqlParser.JoinContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(TOKEN_SPACE); builder.appendExpression(visit(ctx.joinType())); builder.append(QueryTokens.expression(ctx.JOIN())); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index b4cc3f5386..90aede7d7f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -336,8 +336,8 @@ public QueryTokenStream visitEntityWithJoins(HqlParser.EntityWithJoinsContext ct QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.fromRoot())); - builder.appendInline(QueryTokenStream.concat(ctx.joinSpecifier(), this::visit, TOKEN_SPACE)); + builder.appendInline(visit(ctx.fromRoot())); + builder.appendInline(QueryTokenStream.concat(ctx.joinSpecifier(), this::visit, EMPTY_TOKEN)); return builder; } @@ -396,6 +396,7 @@ public QueryTokenStream visitJoin(HqlParser.JoinContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(TOKEN_SPACE); builder.append(visit(ctx.joinType())); builder.append(QueryTokens.expression(ctx.JOIN())); @@ -753,7 +754,7 @@ public QueryTokenStream visitOffsetClause(HqlParser.OffsetClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.OFFSET())); - builder.append(visit(ctx.parameterOrIntegerLiteral())); + builder.appendExpression(visit(ctx.parameterOrIntegerLiteral())); if (ctx.ROW() != null) { builder.append(QueryTokens.expression(ctx.ROW())); @@ -3384,7 +3385,7 @@ public QueryTokenStream visitEveryFunction(HqlParser.EveryFunctionContext ctx) { builder.append(TOKEN_CLOSE_PAREN); } else { - builder.appendExpression(visit(ctx.collectionQuantifier())); + builder.append(visit(ctx.collectionQuantifier())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.simplePath())); @@ -3419,7 +3420,7 @@ public QueryTokenStream visitAnyFunction(HqlParser.AnyFunctionContext ctx) { builder.append(TOKEN_CLOSE_PAREN); } else { - builder.appendExpression(visit(ctx.collectionQuantifier())); + builder.append(visit(ctx.collectionQuantifier())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.simplePath())); @@ -3808,9 +3809,9 @@ public QueryTokenStream visitInList(HqlParser.InListContext ctx) { if (ctx.simplePath() != null) { if (ctx.ELEMENTS() != null) { - builder.append(QueryTokens.expression(ctx.ELEMENTS())); + builder.append(QueryTokens.token(ctx.ELEMENTS())); } else if (ctx.INDICES() != null) { - builder.append(QueryTokens.expression(ctx.INDICES())); + builder.append(QueryTokens.token(ctx.INDICES())); } builder.append(TOKEN_OPEN_PAREN); @@ -3843,9 +3844,9 @@ public QueryTokenStream visitExistsExpression(HqlParser.ExistsExpressionContext builder.append(QueryTokens.expression(ctx.EXISTS())); if (ctx.ELEMENTS() != null) { - builder.append(QueryTokens.expression(ctx.ELEMENTS())); + builder.append(QueryTokens.token(ctx.ELEMENTS())); } else if (ctx.INDICES() != null) { - builder.append(QueryTokens.expression(ctx.INDICES())); + builder.append(QueryTokens.token(ctx.INDICES())); } builder.append(TOKEN_OPEN_PAREN); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java index 33ff1bc5ed..0a60c39acd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokens.java @@ -31,6 +31,7 @@ class QueryTokens { /** * Commonly use tokens. */ + static final QueryToken EMPTY_TOKEN = token(""); static final QueryToken TOKEN_COMMA = token(", "); static final QueryToken TOKEN_SPACE = token(" "); static final QueryToken TOKEN_DOT = token("."); From 00034da425e51776937b179a85d6722b234ef16d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Jan 2025 11:37:56 +0100 Subject: [PATCH 30/94] Merge `(Hql|Jpql|Eql)SpecificationTests` with their corresponding `QueryRendererTests`. Closes #3692 --- .../query/EqlQueryRendererTests.java | 39 +- .../query/EqlSpecificationTests.java | 937 --------- .../query/HqlQueryRendererTests.java | 965 ++++++--- .../query/HqlSpecificationTests.java | 1788 ----------------- .../repository/query/JpqlComplianceTests.java | 321 --- .../query/JpqlQueryRendererTests.java | 292 +++ .../query/JpqlSpecificationTests.java | 941 --------- .../query/QueryEnhancerUnitTests.java | 2 +- 8 files changed, 996 insertions(+), 4289 deletions(-) delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java index 1f346a4970..f98f722c82 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java @@ -338,6 +338,38 @@ OR TREAT(e AS Contractor).hours > 100 """); } + @Test // GH-3136 + void substring() { + + assertQuery("select substring(c.number, 1, 2) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1) " + // + "from Call c"); + } + + @Test // GH-3136 + void currentDateFunctions() { + + assertQuery("select CURRENT_DATE " + // + "from Call c "); + + assertQuery("select CURRENT_TIME " + // + "from Call c "); + + assertQuery("select CURRENT_TIMESTAMP " + // + "from Call c "); + + assertQuery("select LOCAL_DATE " + // + "from Call c "); + + assertQuery("select LOCAL_TIME " + // + "from Call c "); + + assertQuery("select LOCAL_DATETIME " + // + "from Call c "); + } + @Test void pathExpressionsNamedParametersExample() { @@ -449,18 +481,13 @@ AND INDEX(w) = 0 * @see #functionInvocationExampleWithCorrection() */ @Test - @Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator") - void functionInvocationExample_SPEC_BUG() { + void functionInvocationExample() { assertQuery(""" SELECT c FROM Customer c WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) """); - } - - @Test - void functionInvocationExampleWithCorrection() { assertQuery(""" SELECT c diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java deleted file mode 100644 index df67e51b7d..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java +++ /dev/null @@ -1,937 +0,0 @@ -/* - * Copyright 2023-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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; - -/** - * Tests built around examples of EQL found in the JPA spec - * https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc
- *
- * IMPORTANT: Purely verifies the parser without any transformations. - * - * @author Greg Turnquist - */ -class EqlSpecificationTests { - - private static final String SPEC_FAULT = "Disabled due to spec fault> "; - - private static String parseWithoutChanges(String query) { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery(query); - - return TokenRenderer.render(new EqlQueryRenderer().visit(parser.getContext())); - } - - private void assertQuery(String query) { - - String slimmedDownQuery = reduceWhitespace(query); - assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); - } - - private String reduceWhitespace(String original) { - - return original // - .replaceAll("[ \\t\\n]{1,}", " ") // - .trim(); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - */ - @Test - void joinExample1() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order AS o JOIN o.lineItems AS l - WHERE l.shipped = FALSE - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables - */ - @Test - void joinExample2() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l JOIN l.product p - WHERE p.productType = 'office_supplies' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#range-variable-declarations - */ - @Test - void rangeVariableDeclarations() { - - assertQuery(""" - SELECT DISTINCT o1 - FROM Order o1, Order o2 - WHERE o1.quantity > o2.quantity AND - o2.customer.lastname = 'Smith' AND - o2.customer.firstname = 'John' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample1() { - - assertQuery(""" - SELECT i.name, VALUE(p) - FROM Item i JOIN i.photos p - WHERE KEY(p) LIKE '%egret' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample2() { - - assertQuery(""" - SELECT i.name, p - FROM Item i JOIN i.photos p - WHERE KEY(p) LIKE '%egret' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample3() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo.phones p - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample4() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo c JOIN c.phones p - WHERE e.contactInfo.address.zipcode = '95054' - """); - } - - @Test - void pathExpressionSyntaxExample1() { - - assertQuery(""" - SELECT DISTINCT l.product - FROM Order AS o JOIN o.lineItems l - """); - } - - @Test - void joinsExample1() { - - assertQuery(""" - SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize - """); - } - - @Test - void joinsExample2() { - - assertQuery(""" - SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1 - """); - } - - @Test - void joinsInnerExample() { - - assertQuery(""" - SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1 - """); - } - - @Test - void joinsInExample() { - - assertQuery(""" - SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1 - """); - } - - @Test - void doubleJoinExample() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo c JOIN c.phones p - WHERE c.address.zipcode = '95054' - """); - } - - @Test - void leftJoinExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - GROUP BY s.name - """); - } - - @Test - void leftJoinOnExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - ON p.status = 'inStock' - GROUP BY s.name - """); - } - - @Test - void leftJoinWhereExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - WHERE p.status = 'inStock' - GROUP BY s.name - """); - } - - @Test - void leftJoinFetchExample() { - - assertQuery(""" - SELECT d - FROM Department d LEFT JOIN FETCH d.employees - WHERE d.deptno = 1 - """); - } - - @Test - void collectionMemberExample() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.product.productType = 'office_supplies' - """); - } - - @Test - void collectionMemberInExample() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o, IN(o.lineItems) l - WHERE l.product.productType = 'office_supplies' - """); - } - - @Test - void fromClauseExample() { - - assertQuery(""" - SELECT o - FROM Order AS o JOIN o.lineItems l JOIN l.product p - """); - } - - @Test - void fromClauseDowncastingExample1() { - - assertQuery(""" - SELECT b.name, b.ISBN - FROM Order o JOIN TREAT(o.product AS Book) b - """); - } - - @Test - void fromClauseDowncastingExample2() { - - assertQuery(""" - SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp - WHERE lp.budget > 1000 - """); - } - - /** - * @see #fromClauseDowncastingExample3fixed() - */ - @Test - @Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal") - void fromClauseDowncastingExample3_SPEC_BUG() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.projects p - WHERE TREAT(p AS LargeProject).budget > 1000 - OR TREAT(p AS SmallProject).name LIKE 'Persist%' - OR p.description LIKE "cost overrun" - """); - } - - @Test - void fromClauseDowncastingExample3fixed() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.projects p - WHERE TREAT(p AS LargeProject).budget > 1000 - OR TREAT(p AS SmallProject).name LIKE 'Persist%' - OR p.description LIKE 'cost overrun' - """); - } - - @Test - void fromClauseDowncastingExample4() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE TREAT(e AS Exempt).vacationDays > 10 - OR TREAT(e AS Contractor).hours > 100 - """); - } - - @Test // GH-3136 - void substring() { - - assertQuery("select substring(c.number, 1, 2) " + // - "from Call c"); - - assertQuery("select substring(c.number, 1) " + // - "from Call c"); - } - - @Test // GH-3136 - void currentDateFunctions() { - - assertQuery("select CURRENT_DATE " + // - "from Call c "); - - assertQuery("select CURRENT_TIME " + // - "from Call c "); - - assertQuery("select CURRENT_TIMESTAMP " + // - "from Call c "); - - assertQuery("select LOCAL_DATE " + // - "from Call c "); - - assertQuery("select LOCAL_TIME " + // - "from Call c "); - - assertQuery("select LOCAL_DATETIME " + // - "from Call c "); - } - - @Test - void pathExpressionsNamedParametersExample() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.status = :stat - """); - } - - @Test - void betweenExpressionsExample() { - - assertQuery(""" - SELECT t - FROM CreditCard c JOIN c.transactionHistory t - WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 - """); - } - - @Test - void isEmptyExample() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void memberOfExample() { - - assertQuery(""" - SELECT p - FROM Person p - WHERE 'Joe' MEMBER OF p.nicknames - """); - } - - @Test - void existsSubSelectExample1() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - - @Test - void allExample() { - - assertQuery(""" - SELECT emp - FROM Employee emp - WHERE emp.salary > ALL (SELECT m.salary - FROM Manager m - WHERE m.department = emp.department) - """); - } - - @Test - void existsSubSelectExample2() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - - @Test - void subselectNumericComparisonExample1() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE (SELECT AVG(o.price) FROM c.orders o) > 100 - """); - } - - @Test - void subselectNumericComparisonExample2() { - - assertQuery(""" - SELECT goodCustomer - FROM Customer goodCustomer - WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c) - """); - } - - @Test - void indexExample() { - - assertQuery(""" - SELECT w.name - FROM Course c JOIN c.studentWaitlist w - WHERE c.name = 'Calculus' - AND INDEX(w) = 0 - """); - } - - /** - * @see #functionInvocationExampleWithCorrection() - */ - @Test - @Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator") - void functionInvocationExample_SPEC_BUG() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) - """); - } - - @Test - void functionInvocationExampleWithCorrection() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE - """); - } - - @Test - void updateCaseExample1() { - - assertQuery(""" - UPDATE Employee e - SET e.salary = - CASE WHEN e.rating = 1 THEN e.salary * 1.1 - WHEN e.rating = 2 THEN e.salary * 1.05 - ELSE e.salary * 1.01 - END - """); - } - - @Test - void updateCaseExample2() { - - assertQuery(""" - UPDATE Employee e - SET e.salary = - CASE e.rating WHEN 1 THEN e.salary * 1.1 - WHEN 2 THEN e.salary * 1.05 - ELSE e.salary * 1.01 - END - """); - } - - @Test - void selectCaseExample1() { - - assertQuery(""" - SELECT e.name, - CASE TYPE(e) WHEN Exempt THEN 'Exempt' - WHEN Contractor THEN 'Contractor' - WHEN Intern THEN 'Intern' - ELSE 'NonExempt' - END - FROM Employee e - WHERE e.dept.name = 'Engineering' - """); - } - - @Test - void selectCaseExample2() { - - assertQuery(""" - SELECT e.name, - f.name, - CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' - WHEN f.annualMiles > 25000 THEN 'Gold ' - ELSE '' - END, - 'Frequent Flyer') - FROM Employee e JOIN e.frequentFlierPlan f - """); - } - - @Test - void theRest() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (Exempt, Contractor) - """); - } - - @Test - void theRest2() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (:empType1, :empType2) - """); - } - - @Test - void theRest3() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN :empTypes - """); - } - - @Test - void theRest4() { - - assertQuery(""" - SELECT TYPE(e) - FROM Employee e - WHERE TYPE(e) <> Exempt - """); - } - - @Test - void theRest5() { - - assertQuery(""" - SELECT c.status, AVG(c.filledOrderCount), COUNT(c) - FROM Customer c - GROUP BY c.status - HAVING c.status IN (1, 2) - """); - } - - @Test - void theRest6() { - - assertQuery(""" - SELECT c.country, COUNT(c) - FROM Customer c - GROUP BY c.country - HAVING COUNT(c) > 30 - """); - } - - @Test - void theRest7() { - - assertQuery(""" - SELECT c, COUNT(o) - FROM Customer c JOIN c.orders o - GROUP BY c - HAVING COUNT(o) >= 5 - """); - } - - @Test - void theRest8() { - - assertQuery(""" - SELECT c.id, c.status - FROM Customer c JOIN c.orders o - WHERE o.count > 100 - """); - } - - @Test - void theRest9() { - - assertQuery(""" - SELECT v.location.street, KEY(i).title, VALUE(i) - FROM VideoStore v JOIN v.videoInventory i - WHERE v.location.zipcode = '94301' AND VALUE(i) > 0 - """); - } - - @Test - void theRest10() { - - assertQuery(""" - SELECT o.lineItems FROM Order AS o - """); - } - - @Test - void theRest11() { - - assertQuery(""" - SELECT c, COUNT(l) AS itemCount - FROM Customer c JOIN c.Orders o JOIN o.lineItems l - WHERE c.address.state = 'CA' - GROUP BY c - ORDER BY itemCount - """); - } - - @Test - void theRest12() { - - assertQuery(""" - SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) - FROM Customer c JOIN c.orders o - WHERE o.count > 100 - """); - } - - @Test - void theRest13() { - - assertQuery(""" - SELECT e.address AS addr - FROM Employee e - """); - } - - @Test - void theRest14() { - - assertQuery(""" - SELECT AVG(o.quantity) FROM Order o - """); - } - - @Test - void theRest15() { - - assertQuery(""" - SELECT SUM(l.price) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - """); - } - - @Test - void theRest16() { - - assertQuery(""" - SELECT COUNT(o) FROM Order o - """); - } - - @Test - void theRest17() { - - assertQuery(""" - SELECT COUNT(l.price) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - """); - } - - @Test - void theRest18() { - - assertQuery(""" - SELECT COUNT(l) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL - """); - } - - @Test - void theRest19() { - - assertQuery(""" - SELECT o - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - ORDER BY o.quantity DESC, o.totalcost - """); - } - - @Test - void theRest20() { - - assertQuery(""" - SELECT o.quantity, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - ORDER BY o.quantity, a.zipcode - """); - } - - @Test - void theRest21() { - - assertQuery(""" - SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' AND a.county = 'Santa Clara' - ORDER BY o.quantity, taxedCost, a.zipcode - """); - } - - @Test - void theRest22() { - - assertQuery(""" - SELECT AVG(o.quantity) as q, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - GROUP BY a.zipcode - ORDER BY q DESC - """); - } - - @Test - void theRest23() { - - assertQuery(""" - SELECT p.product_name - FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - ORDER BY p.price - """); - } - - /** - * This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason. - */ - @Test - void theRest24() { - - assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> { - assertQuery(""" - SELECT p.product_name - FROM Order o, IN(o.lineItems) l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - ORDER BY o.quantity - """); - }); - } - - @Test - void theRest25() { - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - """); - } - - @Test - void theRest26() { - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - AND c.orders IS EMPTY - """); - } - - @Test - void theRest27() { - - assertQuery(""" - UPDATE Customer c - SET c.status = 'outstanding' - WHERE c.balance < 10000 - """); - } - - @Test - void theRest28() { - - assertQuery(""" - UPDATE Employee e - SET e.address.building = 22 - WHERE e.address.building = 14 - AND e.address.city = 'Santa Clara' - AND e.project = 'Jakarta EE' - """); - } - - @Test - void theRest29() { - - assertQuery(""" - SELECT o - FROM Order o - """); - } - - @Test - void theRest30() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.shippingAddress.state = 'CA' - """); - } - - @Test - void theRest31() { - - assertQuery(""" - SELECT DISTINCT o.shippingAddress.state - FROM Order o - """); - } - - @Test - void theRest32() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - """); - } - - @Test - void theRest33() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS NOT EMPTY - """); - } - - @Test - void theRest34() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void theRest35() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.shipped = FALSE - """); - } - - @Test - void theRest36() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE - NOT (o.shippingAddress.state = o.billingAddress.state AND - o.shippingAddress.city = o.billingAddress.city AND - o.shippingAddress.street = o.billingAddress.street) - """); - } - - @Test - void theRest37() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.shippingAddress <> o.billingAddress - """); - } - - @Test - void theRest38() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.product.name = ?1 - """); - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 2025f116ad..b35267da11 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -19,6 +19,8 @@ import java.util.stream.Stream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -41,12 +43,10 @@ */ class HqlQueryRendererTests { - private static final String SPEC_FAULT = "Disabled due to spec fault> "; - /** * Parse the query using {@link HqlParser} then run it through the query-preserving {@link HqlQueryRenderer}. */ - private static String parseWithoutChanges(String query) { + static String parseWithoutChanges(String query) { JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery(query); @@ -71,33 +71,6 @@ private String reduceWhitespace(String original) { .trim(); } - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - */ - @Test - void joinExample1() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order AS o JOIN o.lineItems AS l - WHERE l.shipped = FALSE - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables - */ - @Test - void joinExample2() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l JOIN l.product p - WHERE p.productType = 'office_supplies' - """); - } - /** * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#range-variable-declarations */ @@ -169,11 +142,11 @@ void pathExpressionSyntaxExample1() { assertQuery(""" SELECT DISTINCT l.product - FROM Order AS o JOIN o.lineItems l + FROM Order AS o JOIN o.lineItems l LEFT JOIN l.product p """); } - @Test // GH-3711 + @Test // GH-3711, GH-2970 void entityTypeReference() { assertQuery(""" @@ -185,6 +158,42 @@ SELECT TYPE(e) SELECT TYPE(?0) FROM Employee e """); + + assertQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN (Exempt, Contractor) + """); + + assertQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN (:empType1, :empType2) + """); + + assertQuery(""" + SELECT e + FROM Employee e + WHERE TYPE(e) IN :empTypes + """); + + assertQuery(""" + SELECT TYPE(e) + FROM Employee e + WHERE TYPE(e) <> Exempt + """); + + assertQuery(""" + SELECT TYPE(e) + FROM Employee e + WHERE TYPE(e) != Exempt + """); + + assertQuery(""" + SELECT TYPE(e) + FROM Employee e + WHERE TYPE(e) ^= Exempt + """); } @Test // GH-3711 @@ -347,6 +356,366 @@ SELECT some_function().foo """); } + @ParameterizedTest // GH-3689 + @ValueSource(strings = { "RESPECT NULLS", "IGNORE NULLS" }) + void generic(String nullHandling) { + + // not in the official documentation but supported in the grammar. + assertQuery(""" + SELECT e FROM Employee e + WHERE FOO(x).bar %s + """.formatted(nullHandling)); + } + + @Test // GH-3689 + void size() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE SIZE(x) > 1 + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE SIZE(e.skills) > 1 + """); + } + + @Test // GH-3689 + void collectionAggregate() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE MAXELEMENT(foo) > MINELEMENT(bar) + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE MININDEX(foo) > MAXINDEX(bar) + """); + } + + @Test // GH-3689 + void trunc() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(x) = TRUNCATE(y) + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, 'foo') = TRUNCATE(e, 'bar') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, 'YEAR') = TRUNCATE(LOCAL DATETIME, 'YEAR') + """); + } + + @ParameterizedTest // GH-3689 + @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", + "NANOSECOND", "EPOCH" }) + void trunc(String truncation) { + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, %1$s) = TRUNCATE(e, %1$s) + """.formatted(truncation)); + } + + @Test // GH-3689 + void format() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE FORMAT(x AS 'yyyy') = FORMAT(e.hiringDate AS 'yyyy') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE e.hiringDate = format(LOCAL DATETIME as 'yyyy-MM-dd') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE e.hiringDate = format(LOCAL_DATE() as 'yyyy-MM-dd') + """); + } + + @Test // GH-3689 + void collate() { + + assertQuery(""" + SELECT e FROM Employee e + WHERE COLLATE(x AS ucs_basic) = COLLATE(e.name AS ucs_basic) + """); + } + + @Test // GH-3689 + void substring() { + + assertQuery("select substring(c.number, 1, 2) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1, position('/0' in c.number)) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1 FOR 2) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1 FOR position('/0' in c.number)) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1) AS shortNumber " + // + "from Call c"); + } + + @Test // GH-3689 + void overlay() { + + assertQuery("select OVERLAY(c.number PLACING 1 FROM 2) " + // + "from Call c "); + + assertQuery("select OVERLAY(p.number PLACING 1 FROM 2 FOR 3) " + // + "from Call c "); + } + + @Test // GH-3689 + void pad() { + + assertQuery("select PAD(c.number WITH 1 LEADING) " + // + "from Call c "); + + assertQuery("select PAD(c.number WITH 1 TRAILING) " + // + "from Call c "); + + assertQuery("select PAD(c.number WITH 1 LEADING '0') " + // + "from Call c "); + + assertQuery("select PAD(c.number WITH 1 TRAILING '0') " + // + "from Call c "); + } + + @Test // GH-3689 + void position() { + + assertQuery("select POSITION(c.number IN 'foo') " + // + "from Call c "); + + assertQuery("select POSITION(c.number IN 'foo') + 1 AS pos " + // + "from Call c "); + } + + @Test // GH-3689 + void currentDateFunctions() { + + assertQuery("select CURRENT DATE, CURRENT_DATE() " + // + "from Call c "); + + assertQuery("select CURRENT TIME, CURRENT_TIME() " + // + "from Call c "); + + assertQuery("select CURRENT TIMESTAMP, CURRENT_TIMESTAMP() " + // + "from Call c "); + + assertQuery("select INSTANT, CURRENT_INSTANT() " + // + "from Call c "); + + assertQuery("select LOCAL DATE, LOCAL_DATE() " + // + "from Call c "); + + assertQuery("select LOCAL TIME, LOCAL_TIME() " + // + "from Call c "); + + assertQuery("select LOCAL DATETIME, LOCAL_DATETIME() " + // + "from Call c "); + + assertQuery("select OFFSET DATETIME, OFFSET_DATETIME() " + // + "from Call c "); + + assertQuery("select OFFSET DATETIME AS offsetDatetime, OFFSET_DATETIME() AS offset_datetime " + // + "from Call c "); + } + + @Test // GH-3689 + void cube() { + + assertQuery("select CUBE(foo), CUBE(foo, bar) " + // + "from Call c "); + + assertQuery("select c.callerId from Call c GROUP BY CUBE(state, province)"); + } + + @Test // GH-3689 + void rollup() { + + assertQuery("select ROLLUP(foo), ROLLUP(foo, bar) " + // + "from Call c "); + + assertQuery("select c.callerId from Call c GROUP BY ROLLUP(state, province)"); + } + + @Test + void pathExpressionsNamedParametersExample() { + + assertQuery(""" + SELECT c + FROM Customer c + WHERE c.status = :stat + """); + } + + @Test + void betweenExpressionsExample() { + + assertQuery(""" + SELECT t + FROM CreditCard c JOIN c.transactionHistory t + WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 + """); + } + + @Test + void isEmptyExample() { + + assertQuery(""" + SELECT o + FROM Order o + WHERE o.lineItems IS EMPTY + """); + } + + @Test + void memberOfExample() { + + assertQuery(""" + SELECT p + FROM Person p + WHERE 'Joe' MEMBER OF p.nicknames + """); + } + + @Test + void existsSubSelectExample1() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EXISTS (SELECT spouseEmp + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) + """); + } + + @Test // GH-3689 + void everyAll() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE EVERY (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL (foo > 1) OVER (PARTITION BY bar) + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL VALUES(foo) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ALL ELEMENTS(foo) > 1 + """); + } + + @Test // GH-3689 + void anySome() { + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ANY (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE SOME (SELECT spouseEmp + FROM Employee spouseEmp) > 1 + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ANY (foo > 1) OVER (PARTITION BY bar) + """); + + assertQuery(""" + SELECT DISTINCT emp + FROM Employee emp + WHERE ANY VALUES(foo) > 1 + """); + } + + @Test // GH-3689 + void listAgg() { + + assertQuery("select listagg(p.number, ', ') within group (order by p.type, p.number) " + // + "from Phone p " + // + "group by p.person"); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example + */ + @Test + void joinExample1() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order AS o JOIN o.lineItems AS l + WHERE l.shipped = FALSE + """); + } + + /** + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example + * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables + */ + @Test + void joinExample2() { + + assertQuery(""" + SELECT DISTINCT o + FROM Order o JOIN o.lineItems l JOIN l.product p + WHERE p.productType = 'office_supplies' + """); + } + @Test void joinsExample1() { @@ -371,7 +740,6 @@ void joinsInnerExample() { """); } - @Disabled("Deprecated syntax dating back to EJB-QL prior to EJB 3, required by JPA, never documented in Hibernate") @Test void joinsInExample() { @@ -447,7 +815,7 @@ void collectionMemberInExample() { assertQuery(""" SELECT DISTINCT o - FROM Order o , IN(o.lineItems) l + FROM Order o, IN(o.lineItems) l WHERE l.product.productType = 'office_supplies' """); } @@ -483,8 +851,7 @@ SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp * @see #fromClauseDowncastingExample3fixed() */ @Test - @Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal") - void fromClauseDowncastingExample3_SPEC_BUG() { + void fromClauseDowncastingExample3() { assertQuery(""" SELECT e FROM Employee e JOIN e.projects p @@ -492,10 +859,6 @@ WHERE TREAT(p AS LargeProject).budget > 1000 OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" """); - } - - @Test - void fromClauseDowncastingExample3fixed() { assertQuery(""" SELECT e FROM Employee e JOIN e.projects p @@ -515,58 +878,6 @@ OR TREAT(e AS Contractor).hours > 100 """); } - @Test - void pathExpressionsNamedParametersExample() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.status = :stat - """); - } - - @Test - void betweenExpressionsExample() { - - assertQuery(""" - SELECT t - FROM CreditCard c JOIN c.transactionHistory t - WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 - """); - } - - @Test - void isEmptyExample() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void memberOfExample() { - - assertQuery(""" - SELECT p - FROM Person p - WHERE 'Joe' MEMBER OF p.nicknames - """); - } - - @Test - void existsSubSelectExample1() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - @Test void allExample() { @@ -626,8 +937,7 @@ AND INDEX(w) = 0 * @see #functionInvocationExampleWithCorrection() */ @Test - @Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator") - void functionInvocationExample_SPEC_BUG() { + void functionInvocationExample() { assertQuery(""" SELECT c @@ -646,6 +956,15 @@ WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE """); } + @ParameterizedTest // GH-3628 + @ValueSource(strings = { "is true", "is not true", "is false", "is not false" }) + void functionInvocationWithIsBoolean(String booleanComparison) { + + assertQuery(""" + from RoleTmpl where find_in_set(:appId, appIds) %s + """.formatted(booleanComparison)); + } + @Test void updateCaseExample1() { @@ -703,61 +1022,95 @@ void selectCaseExample2() { } @Test - void theRest() { + void collectionIsEmpty() { assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (Exempt, Contractor) + DELETE + FROM Customer c + WHERE c.status = 'inactive' + AND c.orders IS EMPTY """); - } - - @Test - void theRest2() { assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (:empType1, :empType2) + DELETE + FROM Customer c + WHERE c.status = 'inactive' + AND c.orders IS NOT EMPTY """); } - @Test - void theRest3() { + @Test // GH-3628 + void booleanPredicate() { assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN :empTypes + SELECT c + FROM Customer c + WHERE c.orders IS TRUE """); - } - @Test - void theRest4() { + assertQuery(""" + SELECT c + FROM Customer c + WHERE c.orders IS NOT TRUE + """); assertQuery(""" - SELECT TYPE(e) - FROM Employee e - WHERE TYPE(e) <> Exempt + SELECT c + FROM Customer c + WHERE c.orders IS FALSE """); - } - @Test // GH-2970 - void alternateNotEqualsShouldAlsoWork() { + assertQuery(""" + SELECT c + FROM Customer c + WHERE c.orders IS NOT FALSE + """); assertQuery(""" - SELECT TYPE(e) - FROM Employee e - WHERE TYPE(e) != Exempt + SELECT c + FROM Customer c + WHERE c.orders IS NULL """); assertQuery(""" - SELECT TYPE(e) - FROM Employee e - WHERE TYPE(e) ^= Exempt + SELECT c + FROM Customer c + WHERE c.orders IS NOT NULL """); } + @ParameterizedTest // GH-3628 + @ValueSource(strings = { "IS DISTINCT FROM", "IS NOT DISTINCT FROM" }) + void distinctFromPredicate(String distinctFrom) { + + assertQuery(""" + SELECT c + FROM Customer c + WHERE c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + WHERE c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + GROUP BY c.lastname + HAVING c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + WHERE EXISTS (SELECT c2 + FROM Customer c2 + WHERE c2.orders %s c.orders) + """.formatted(distinctFrom)); + } + @Test void theRest5() { @@ -960,7 +1313,7 @@ void theRest24() { assertQuery(""" SELECT p.product_name - FROM Order o , IN(o.lineItems) l JOIN o.customer c + FROM Order o, IN(o.lineItems) l JOIN o.customer c WHERE c.lastname = 'Smith' AND c.firstname = 'John' ORDER BY o.quantity """); @@ -1109,85 +1462,108 @@ void theRest38() { """); } + @Test // GH-3689 + void insertQueries() { + + assertQuery("insert Person (id, name) values (100L, 'Jane Doe')"); + + assertQuery("insert Person (id, name) values " + // + "(101L, 'J A Doe III'), " + // + "(102L, 'J X Doe'), " + // + "(103L, 'John Doe, Jr')"); + + assertQuery("insert into Partner (id, name) " + // + "select p.id, p.name from Person p "); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT (range) DO UPDATE SET price = :price, type = :priceType"); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT ON CONSTRAINT foo DO UPDATE SET price = :price, type = :priceType"); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT ON CONSTRAINT foo DO NOTHING"); + } + @Test void hqlQueries() { - parseWithoutChanges("from Person"); - parseWithoutChanges("select local datetime"); - parseWithoutChanges("from Person p select p.name"); - parseWithoutChanges("update Person set nickName = 'Nacho' " + // + assertQuery("from Person"); + assertQuery("select local datetime"); + assertQuery("from Person p select p.name"); + assertQuery("update Person set nickName = 'Nacho' " + // "where name = 'Ignacio'"); - parseWithoutChanges("update Person p " + // + assertQuery("update Person p " + // "set p.name = :newName " + // "where p.name = :oldName"); - parseWithoutChanges("update Person " + // + assertQuery("update Person " + // "set name = :newName " + // "where name = :oldName"); - parseWithoutChanges("update versioned Person " + // + assertQuery("update versioned Person " + // "set name = :newName " + // "where name = :oldName"); - parseWithoutChanges("insert Person (id, name) " + // + assertQuery("insert Person (id, name) " + // "values (100L, 'Jane Doe')"); - parseWithoutChanges("insert Person (id, name) " + // + assertQuery("insert Person (id, name) " + // "values (101L, 'J A Doe III'), " + // "(102L, 'J X Doe'), " + // "(103L, 'John Doe, Jr')"); - parseWithoutChanges("insert into Partner (id, name) " + // + assertQuery("insert into Partner (id, name) " + // "select p.id, p.name " + // "from Person p "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name like 'Joe'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name ilike 'Joe'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name like 'Joe''s'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.id = 1"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.id = 1L"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where c.duration > 100.5"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where c.duration > 100.5F"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where c.duration > 1e+2"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where c.duration > 1e+2F"); - parseWithoutChanges("from Phone ph " + // + assertQuery("from Phone ph " + // "where ph.type = LAND_LINE"); - parseWithoutChanges("select java.lang.Math.PI"); - parseWithoutChanges("select 'Customer ' || p.name " + // + assertQuery("select java.lang.Math.PI"); + assertQuery("select 'Customer ' || p.name " + // "from Person p " + // "where p.id = 1"); - parseWithoutChanges("select sum(ch.duration) * :multiplier " + // + assertQuery("select sum(ch.duration) * :multiplier " + // "from Person pr " + // "join pr.phones ph " + // "join ph.callHistory ch " + // "where ph.id = 1L "); - parseWithoutChanges("select year(local date) - year(p.createdOn) " + // + assertQuery("select year(local date) - year(p.createdOn) " + // "from Person p " + // "where p.id = 1L"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where year(local date) - year(p.createdOn) > 1"); - parseWithoutChanges("select " + // + assertQuery("select " + // " case p.nickName " + // " when 'NA' " + // " then '' " + // " else p.nickName " + // " end " + // "from Person p"); - parseWithoutChanges("select " + // + assertQuery("select " + // " case " + // " when p.nickName is null " + // " then " + // @@ -1199,336 +1575,336 @@ void hqlQueries() { " else p.nickName " + // " end " + // "from Person p"); - parseWithoutChanges("select " + // + assertQuery("select " + // " case when p.nickName is null " + // " then p.id * 1000 " + // " else p.id " + // " end " + // "from Person p " + // "order by p.id"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p " + // "where type(p) = CreditCardPayment"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p " + // "where type(p) = :type"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p " + // "where length(treat(p as CreditCardPayment).cardNumber) between 16 and 20"); - parseWithoutChanges("select nullif(p.nickName, p.name) " + // + assertQuery("select nullif(p.nickName, p.name) " + // "from Person p"); - parseWithoutChanges("select " + // + assertQuery("select " + // " case" + // " when p.nickName = p.name" + // " then null" + // " else p.nickName" + // " end " + // "from Person p"); - parseWithoutChanges("select coalesce(p.nickName, '') " + // + assertQuery("select coalesce(p.nickName, '') " + // "from Person p"); - parseWithoutChanges("select coalesce(p.nickName, p.name, '') " + // + assertQuery("select coalesce(p.nickName, p.name, '') " + // "from Person p"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where size(p.phones) >= 2"); - parseWithoutChanges("select concat(p.number, ' : ' , cast(c.duration as string)) " + // + assertQuery("select concat(p.number, ' : ', cast(c.duration as string)) " + // "from Call c " + // "join c.phone p"); - parseWithoutChanges("select substring(p.number, 1, 2) " + // + assertQuery("select substring(p.number, 1, 2) " + // "from Call c " + // "join c.phone p"); - parseWithoutChanges("select upper(p.name) " + // + assertQuery("select upper(p.name) " + // "from Person p "); - parseWithoutChanges("select lower(p.name) " + // + assertQuery("select lower(p.name) " + // "from Person p "); - parseWithoutChanges("select trim(p.name) " + // + assertQuery("select trim(p.name) " + // "from Person p "); - parseWithoutChanges("select trim(leading ' ' from p.name) " + // + assertQuery("select trim(leading ' ' from p.name) " + // "from Person p "); - parseWithoutChanges("select length(p.name) " + // + assertQuery("select length(p.name) " + // "from Person p "); - parseWithoutChanges("select locate('John', p.name) " + // + assertQuery("select locate('John', p.name) " + // "from Person p "); - parseWithoutChanges("select abs(c.duration) " + // + assertQuery("select abs(c.duration) " + // "from Call c "); - parseWithoutChanges("select mod(c.duration, 10) " + // + assertQuery("select mod(c.duration, 10) " + // "from Call c "); - parseWithoutChanges("select sqrt(c.duration) " + // + assertQuery("select sqrt(c.duration) " + // "from Call c "); - parseWithoutChanges("select cast(c.duration as String) " + // + assertQuery("select cast(c.duration as String) " + // "from Call c "); - parseWithoutChanges("select str(c.timestamp) " + // + assertQuery("select str(c.timestamp) " + // "from Call c "); - parseWithoutChanges("select str(cast(duration as float) / 60, 4, 2) " + // + assertQuery("select str(cast(duration as float) / 60, 4, 2) " + // "from Call c "); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where extract(date from c.timestamp) = local date"); - parseWithoutChanges("select extract(year from c.timestamp) " + // + assertQuery("select extract(year from c.timestamp) " + // "from Call c "); - parseWithoutChanges("select year(c.timestamp) " + // + assertQuery("select year(c.timestamp) " + // "from Call c "); - parseWithoutChanges("select var_samp(c.duration) as sampvar, var_pop(c.duration) as popvar " + // + assertQuery("select var_samp(c.duration) as sampvar, var_pop(c.duration) as popvar " + // "from Call c "); - parseWithoutChanges("select bit_length(c.phone.number) " + // + assertQuery("select bit_length(c.phone.number) " + // "from Call c "); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where c.duration < 30 "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name like 'John%' "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.createdOn > '1950-01-01' "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "where p.type = 'MOBILE' "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p " + // "where p.completed = true "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p " + // "where type(p) = WireTransferPayment "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p, Phone ph " + // "where p.person = ph.person "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "join p.phones ph " + // "where p.id = 1L and index(ph) between 0 and 3"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.createdOn between '1999-01-01' and '2001-01-02'"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "where c.duration between 5 and 20"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name between 'H' and 'M'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.nickName is not null"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.nickName is null"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name like 'Jo%'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name not like 'Jo%'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.name like 'Dr|_%' escape '|'"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p " + // "where type(p) in (CreditCardPayment, WireTransferPayment)"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "where type in ('MOBILE', 'LAND_LINE')"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "where type in :types"); - parseWithoutChanges("select distinct p " + // + assertQuery("select distinct p " + // "from Phone p " + // - "where p.person.id in (" + // - " select py.person.id " + // + "where p.person.id in " + // + "(select py.person.id " + // " from Payment py" + // - " where py.completed = true and py.amount > 50 " + // + " where py.completed = true and py.amount > 50" + // ")"); - parseWithoutChanges("select distinct p " + // + assertQuery("select distinct p " + // "from Phone p " + // - "where p.person in (" + // - " select py.person " + // + "where p.person in " + // + "(select py.person " + // " from Payment py" + // - " where py.completed = true and py.amount > 50 " + // + " where py.completed = true and py.amount > 50" + // ")"); - parseWithoutChanges("select distinct p " + // + assertQuery("select distinct p " + // "from Payment p " + // "where (p.amount, p.completed) in (" + // - " (50, true)," + // + "(50, true)," + // " (100, true)," + // " (5, false)" + // ")"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where 1 in indices(p.phones)"); - parseWithoutChanges("select distinct p.person " + // + assertQuery("select distinct p.person " + // "from Phone p " + // "join p.calls c " + // - "where 50 > all (" + // - " select duration" + // + "where 50 > all " + // + "(select duration" + // " from Call" + // - " where phone = p " + // + " where phone = p" + // ") "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "where local date > all elements(p.repairTimestamps)"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where :phone = some elements(p.phones)"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where :phone member of p.phones"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where exists elements(p.phones)"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.phones is empty"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.phones is not empty"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.phones is not empty"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where 'Home address' member of p.addresses"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where 'Home address' not member of p.addresses"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from org.hibernate.userguide.model.Person p"); - parseWithoutChanges("select distinct pr, ph " + // + assertQuery("select distinct pr, ph " + // "from Person pr, Phone ph " + // "where ph.person = pr and ph is not null"); - parseWithoutChanges("select distinct pr1 " + // + assertQuery("select distinct pr1 " + // "from Person pr1, Person pr2 " + // "where pr1.id <> pr2.id " + // " and pr1.address = pr2.address " + // " and pr1.createdOn < pr2.createdOn"); - parseWithoutChanges("select distinct pr, ph " + // + assertQuery("select distinct pr, ph " + // "from Person pr cross join Phone ph " + // "where ph.person = pr and ph is not null"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Payment p "); - parseWithoutChanges("select d.owner, d.payed " + // - "from (" + // - " select p.person as owner, c.payment is not null as payed " + // + assertQuery("select d.owner, d.payed " + // + "from " + // + "(select p.person as owner, c.payment is not null as payed " + // " from Call c " + // " join c.phone p " + // " where p.number = :phoneNumber) d"); - parseWithoutChanges("select distinct pr " + // + assertQuery("select distinct pr " + // "from Person pr " + // "join Phone ph on ph.person = pr " + // "where ph.type = :phoneType"); - parseWithoutChanges("select distinct pr " + // + assertQuery("select distinct pr " + // "from Person pr " + // "join pr.phones ph " + // "where ph.type = :phoneType"); - parseWithoutChanges("select distinct pr " + // + assertQuery("select distinct pr " + // "from Person pr " + // "inner join pr.phones ph " + // "where ph.type = :phoneType"); - parseWithoutChanges("select distinct pr " + // + assertQuery("select distinct pr " + // "from Person pr " + // "left join pr.phones ph " + // "where ph is null " + // " or ph.type = :phoneType"); - parseWithoutChanges("select distinct pr " + // + assertQuery("select distinct pr " + // "from Person pr " + // "left outer join pr.phones ph " + // "where ph is null " + // " or ph.type = :phoneType"); - parseWithoutChanges("select pr.name, ph.number " + // + assertQuery("select pr.name, ph.number " + // "from Person pr " + // "left join pr.phones ph with ph.type = :phoneType "); - parseWithoutChanges("select pr.name, ph.number " + // + assertQuery("select pr.name, ph.number " + // "from Person pr " + // "left join pr.phones ph on ph.type = :phoneType "); - parseWithoutChanges("select distinct pr " + // + assertQuery("select distinct pr " + // "from Person pr " + // "left join fetch pr.phones "); - parseWithoutChanges("select a, ccp " + // + assertQuery("select a, ccp " + // "from Account a " + // "join treat(a.payments as CreditCardPayment) ccp " + // "where length(ccp.cardNumber) between 16 and 20"); - parseWithoutChanges("select c, ccp " + // + assertQuery("select c, ccp " + // "from Call c " + // "join treat(c.payment as CreditCardPayment) ccp " + // "where length(ccp.cardNumber) between 16 and 20"); - parseWithoutChanges("select longest.duration " + // + assertQuery("select longest.duration " + // "from Phone p " + // - "left join lateral (" + // - " select c.duration as duration " + // + "left join lateral " + // + "(select c.duration as duration " + // " from p.calls c" + // " order by c.duration desc" + // " limit 1 " + // " ) longest " + // "where p.number = :phoneNumber"); - parseWithoutChanges("select ph " + // + assertQuery("select ph " + // "from Phone ph " + // "where ph.person.address = :address "); - parseWithoutChanges("select ph " + // + assertQuery("select ph " + // "from Phone ph " + // "join ph.person pr " + // "where pr.address = :address "); - parseWithoutChanges("select ph " + // + assertQuery("select ph " + // "from Phone ph " + // "where ph.person.address = :address " + // " and ph.person.createdOn > :timestamp"); - parseWithoutChanges("select ph " + // + assertQuery("select ph " + // "from Phone ph " + // "inner join ph.person pr " + // "where pr.address = :address " + // " and pr.createdOn > :timestamp"); - parseWithoutChanges("select ph " + // + assertQuery("select ph " + // "from Person pr " + // "join pr.phones ph " + // "join ph.calls c " + // "where pr.address = :address " + // " and c.duration > :duration"); - parseWithoutChanges("select ch " + // + assertQuery("select ch " + // "from Phone ph " + // "join ph.callHistory ch " + // "where ph.id = :id "); - parseWithoutChanges("select value(ch) " + // + assertQuery("select value(ch) " + // "from Phone ph " + // "join ph.callHistory ch " + // "where ph.id = :id "); - parseWithoutChanges("select key(ch) " + // + assertQuery("select key(ch) " + // "from Phone ph " + // "join ph.callHistory ch " + // "where ph.id = :id "); - parseWithoutChanges("select key(ch) " + // + assertQuery("select key(ch) " + // "from Phone ph " + // "join ph.callHistory ch " + // "where ph.id = :id "); - parseWithoutChanges("select entry(ch) " + // + assertQuery("select entry (ch) " + // "from Phone ph " + // "join ph.callHistory ch " + // "where ph.id = :id "); - parseWithoutChanges("select sum(ch.duration) " + // + assertQuery("select sum(ch.duration) " + // "from Person pr " + // "join pr.phones ph " + // "join ph.callHistory ch " + // "where ph.id = :id " + // " and index(ph) = :phoneIndex"); - parseWithoutChanges("select value(ph.callHistory) " + // + assertQuery("select value(ph.callHistory) " + // "from Phone ph " + // "where ph.id = :id "); - parseWithoutChanges("select key(ph.callHistory) " + // + assertQuery("select key(ph.callHistory) " + // "from Phone ph " + // "where ph.id = :id "); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.phones[0].type = LAND_LINE"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where p.addresses['HOME'] = :address"); - parseWithoutChanges("select pr " + // + assertQuery("select pr " + // "from Person pr " + // "where pr.phones[max(indices(pr.phones))].type = 'LAND_LINE'"); - parseWithoutChanges("select p.name, p.nickName " + // + assertQuery("select p.name, p.nickName " + // "from Person p "); - parseWithoutChanges("select p.name as name, p.nickName as nickName " + // + assertQuery("select p.name as name, p.nickName as nickName " + // "from Person p "); - parseWithoutChanges("select new org.hibernate.userguide.hql.CallStatistics(" + // - " count(c), " + // + assertQuery("select new org.hibernate.userguide.hql.CallStatistics" + // + "(count(c), " + // " sum(c.duration), " + // " min(c.duration), " + // " max(c.duration), " + // @@ -1536,100 +1912,99 @@ void hqlQueries() { " 1" + // ") " + // "from Call c "); - parseWithoutChanges("select new map(" + // - " p.number as phoneNumber , " + // + assertQuery("select new map(" + // + "p.number as phoneNumber, " + // " sum(c.duration) as totalDuration, " + // - " avg(c.duration) as averageDuration " + // + " avg(c.duration) as averageDuration" + // ") " + // "from Call c " + // "join c.phone p " + // "group by p.number "); - parseWithoutChanges("select new list(" + // - " p.number, " + // - " c.duration " + // - ") " + // + assertQuery("select new list(" + // + "p.number, " + // + " c.duration) " + // "from Call c " + // "join c.phone p "); - parseWithoutChanges("select distinct p.lastName " + // + assertQuery("select distinct p.lastName " + // "from Person p"); - parseWithoutChanges("select " + // + assertQuery("select " + // " count(c), " + // " sum(c.duration), " + // " min(c.duration), " + // " max(c.duration), " + // " avg(c.duration) " + // "from Call c "); - parseWithoutChanges("select count(distinct c.phone) " + // + assertQuery("select count(distinct c.phone) " + // "from Call c "); - parseWithoutChanges("select p.number, count(c) " + // + assertQuery("select p.number, count(c) " + // "from Call c " + // "join c.phone p " + // "group by p.number"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "where max(elements(p.calls)) = :call"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "where min(elements(p.calls)) = :call"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "where max(indices(p.phones)) = 0"); - parseWithoutChanges("select count(c) filter (where c.duration < 30) " + // + assertQuery("select count(c) filter (where c.duration < 30) " + // "from Call c "); - parseWithoutChanges("select p.number, count(c) filter (where c.duration < 30) " + // + assertQuery("select p.number, count(c) filter (where c.duration < 30) " + // "from Call c " + // "join c.phone p " + // "group by p.number"); - parseWithoutChanges("select listagg(p.number, ', ') within group (order by p.type,p.number) " + // + assertQuery("select listagg(p.number, ', ') within group (order by p.type, p.number) " + // "from Phone p " + // "group by p.person"); - parseWithoutChanges("select sum(c.duration) " + // + assertQuery("select sum(c.duration) " + // "from Call c "); - parseWithoutChanges("select p.name, sum(c.duration) " + // + assertQuery("select p.name, sum(c.duration) " + // "from Call c " + // "join c.phone ph " + // "join ph.person p " + // "group by p.name"); - parseWithoutChanges("select p, sum(c.duration) " + // + assertQuery("select p, sum(c.duration) " + // "from Call c " + // "join c.phone ph " + // "join ph.person p " + // "group by p"); - parseWithoutChanges("select p.name, sum(c.duration) " + // + assertQuery("select p.name, sum(c.duration) " + // "from Call c " + // "join c.phone ph " + // "join ph.person p " + // "group by p.name " + // "having sum(c.duration) > 1000"); - parseWithoutChanges("select p.name from Person p " + // + assertQuery("select p.name from Person p " + // "union " + // "select p.nickName from Person p where p.nickName is not null"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Person p " + // "order by p.name"); - parseWithoutChanges("select p.name, sum(c.duration) as total " + // + assertQuery("select p.name, sum(c.duration) as total " + // "from Call c " + // "join c.phone ph " + // "join ph.person p " + // "group by p.name " + // "order by total"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "join c.phone p " + // "order by p.number " + // "limit 50"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "join c.phone p " + // "order by p.number " + // "fetch first 50 rows only"); - parseWithoutChanges("select c " + // + assertQuery("select c " + // "from Call c " + // "join c.phone p " + // "order by p.number " + // "offset 10 rows " + // "fetch first 50 rows with ties"); - parseWithoutChanges("select p " + // + assertQuery("select p " + // "from Phone p " + // "join fetch p.calls " + // "order by p " + // @@ -1955,7 +2330,7 @@ void entityNameWithPackageContainingReservedWord(String reservedWord) { } @ParameterizedTest // GH-3136 - @ValueSource(strings = {"LEFT", "RIGHT"}) + @ValueSource(strings = { "LEFT", "RIGHT" }) void leftRightStringFunctions(String keyword) { assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java deleted file mode 100644 index be05e3fceb..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ /dev/null @@ -1,1788 +0,0 @@ -/* - * Copyright 2022-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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; - -/** - * Tests built around examples of HQL found in - * https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc and - * https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#query-language
- *
- * IMPORTANT: Purely verifies the parser without any transformations. - * - * @author Greg Turnquist - * @author Mark Paluch - * @author Christoph Strobl - * @since 3.1 - */ -class HqlSpecificationTests { - - private static final String SPEC_FAULT = "Disabled due to spec fault> "; - - private static String parseWithoutChanges(String query) { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery(query); - - return TokenRenderer.render(new HqlQueryRenderer().visit(parser.getContext())); - } - - private void assertQuery(String query) { - - String slimmedDownQuery = reduceWhitespace(query); - assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); - } - - private String reduceWhitespace(String original) { - - return original // - .replaceAll("[ \\t\\n]{1,}", " ") // - .trim(); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - */ - @Test - void joinExample1() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order AS o JOIN o.lineItems AS l - WHERE l.shipped = FALSE - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables - */ - @Test - void joinExample2() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l JOIN l.product p - WHERE p.productType = 'office_supplies' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#range-variable-declarations - */ - @Test - void rangeVariableDeclarations() { - - assertQuery(""" - SELECT DISTINCT o1 - FROM Order o1, Order o2 - WHERE o1.quantity > o2.quantity AND - o2.customer.lastname = 'Smith' AND - o2.customer.firstname = 'John' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample1() { - - assertQuery(""" - SELECT i.name, VALUE(p) - FROM Item i JOIN i.photos p - WHERE KEY(p) LIKE '%egret' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample2() { - - assertQuery(""" - SELECT i.name, p - FROM Item i JOIN i.photos p - WHERE KEY(p) LIKE '%egret' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample3() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo.phones p - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample4() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo c JOIN c.phones p - WHERE e.contactInfo.address.zipcode = '95054' - """); - } - - @Test - void pathExpressionSyntaxExample1() { - - assertQuery(""" - SELECT DISTINCT l.product - FROM Order AS o JOIN o.lineItems l - """); - } - - @Test - void joinsExample1() { - - assertQuery(""" - SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize - """); - } - - @Test - void joinsExample2() { - - assertQuery(""" - SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1 - """); - } - - @Test - void joinsInnerExample() { - - assertQuery(""" - SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1 - """); - } - - @Test - void joinsInExample() { - - assertQuery(""" - SELECT OBJECT(c) FROM Customer c , IN(c.orders) o WHERE c.status = 1 - """); - } - - @Test - void doubleJoinExample() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo c JOIN c.phones p - WHERE c.address.zipcode = '95054' - """); - } - - @Test - void leftJoinExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - GROUP BY s.name - """); - } - - @Test - void leftJoinOnExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - ON p.status = 'inStock' - GROUP BY s.name - """); - } - - @Test - void leftJoinWhereExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - WHERE p.status = 'inStock' - GROUP BY s.name - """); - } - - @Test - void leftJoinFetchExample() { - - assertQuery(""" - SELECT d - FROM Department d LEFT JOIN FETCH d.employees - WHERE d.deptno = 1 - """); - } - - @Test - void collectionMemberExample() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.product.productType = 'office_supplies' - """); - } - - @Test - void collectionMemberInExample() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o , IN(o.lineItems) l - WHERE l.product.productType = 'office_supplies' - """); - } - - @Test - void fromClauseExample() { - - assertQuery(""" - SELECT o - FROM Order AS o JOIN o.lineItems l JOIN l.product p - """); - } - - @Test - void fromClauseDowncastingExample1() { - - assertQuery(""" - SELECT b.name, b.ISBN - FROM Order o JOIN TREAT(o.product AS Book) b - """); - } - - @Test - void fromClauseDowncastingExample2() { - - assertQuery(""" - SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp - WHERE lp.budget > 1000 - """); - } - - /** - * @see #fromClauseDowncastingExample3fixed() - */ - @Test - @Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal") - void fromClauseDowncastingExample3_SPEC_BUG() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.projects p - WHERE TREAT(p AS LargeProject).budget > 1000 - OR TREAT(p AS SmallProject).name LIKE 'Persist%' - OR p.description LIKE "cost overrun" - """); - } - - @Test - void fromClauseDowncastingExample3fixed() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.projects p - WHERE TREAT(p AS LargeProject).budget > 1000 - OR TREAT(p AS SmallProject).name LIKE 'Persist%' - OR p.description LIKE 'cost overrun' - """); - } - - @Test - void fromClauseDowncastingExample4() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE TREAT(e AS Exempt).vacationDays > 10 - OR TREAT(e AS Contractor).hours > 100 - """); - } - - @ParameterizedTest // GH-3689 - @ValueSource(strings = { "RESPECT NULLS", "IGNORE NULLS" }) - void generic(String nullHandling) { - - // not in the official documentation but supported in the grammar. - assertQuery(""" - SELECT e FROM Employee e - WHERE FOO(x).bar %s - """.formatted(nullHandling)); - } - - @Test // GH-3689 - void size() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE SIZE(x) > 1 - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE SIZE(e.skills) > 1 - """); - } - - @Test // GH-3689 - void collectionAggregate() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE MAXELEMENT(foo) > MINELEMENT(bar) - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE MININDEX(foo) > MAXINDEX(bar) - """); - } - - @Test // GH-3689 - void trunc() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE TRUNC(x) = TRUNCATE(y) - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE TRUNC(e, 'foo') = TRUNCATE(e, 'bar') - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE TRUNC(e, 'YEAR') = TRUNCATE(LOCAL DATETIME, 'YEAR') - """); - } - - @ParameterizedTest // GH-3689 - @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", - "NANOSECOND", "EPOCH" }) - void trunc(String truncation) { - - assertQuery(""" - SELECT e FROM Employee e - WHERE TRUNC(e, %1$s) = TRUNCATE(e, %1$s) - """.formatted(truncation)); - } - - @Test // GH-3689 - void format() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE FORMAT(x AS 'yyyy') = FORMAT(e.hiringDate AS 'yyyy') - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE e.hiringDate = format(LOCAL DATETIME as 'yyyy-MM-dd') - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE e.hiringDate = format(LOCAL_DATE() as 'yyyy-MM-dd') - """); - } - - @Test // GH-3689 - void collate() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE COLLATE(x AS ucs_basic) = COLLATE(e.name AS ucs_basic) - """); - } - - @Test // GH-3689 - void substring() { - - assertQuery("select substring(c.number, 1, 2) " + // - "from Call c"); - - assertQuery("select substring(c.number, 1) " + // - "from Call c"); - - assertQuery("select substring(c.number, 1, position('/0' in c.number)) " + // - "from Call c"); - - assertQuery("select substring(c.number FROM 1 FOR 2) " + // - "from Call c"); - - assertQuery("select substring(c.number FROM 1) " + // - "from Call c"); - - assertQuery("select substring(c.number FROM 1 FOR position('/0' in c.number)) " + // - "from Call c"); - - assertQuery("select substring(c.number FROM 1) AS shortNumber " + // - "from Call c"); - } - - @Test // GH-3689 - void overlay() { - - assertQuery("select OVERLAY(c.number PLACING 1 FROM 2) " + // - "from Call c "); - - assertQuery("select OVERLAY(p.number PLACING 1 FROM 2 FOR 3) " + // - "from Call c "); - } - - @Test // GH-3689 - void pad() { - - assertQuery("select PAD(c.number WITH 1 LEADING) " + // - "from Call c "); - - assertQuery("select PAD(c.number WITH 1 TRAILING) " + // - "from Call c "); - - assertQuery("select PAD(c.number WITH 1 LEADING '0') " + // - "from Call c "); - - assertQuery("select PAD(c.number WITH 1 TRAILING '0') " + // - "from Call c "); - } - - @Test // GH-3689 - void position() { - - assertQuery("select POSITION(c.number IN 'foo') " + // - "from Call c "); - - assertQuery("select POSITION(c.number IN 'foo') + 1 AS pos " + // - "from Call c "); - } - - @Test // GH-3689 - void currentDateFunctions() { - - assertQuery("select CURRENT DATE, CURRENT_DATE() " + // - "from Call c "); - - assertQuery("select CURRENT TIME, CURRENT_TIME() " + // - "from Call c "); - - assertQuery("select CURRENT TIMESTAMP, CURRENT_TIMESTAMP() " + // - "from Call c "); - - assertQuery("select INSTANT, CURRENT_INSTANT() " + // - "from Call c "); - - assertQuery("select LOCAL DATE, LOCAL_DATE() " + // - "from Call c "); - - assertQuery("select LOCAL TIME, LOCAL_TIME() " + // - "from Call c "); - - assertQuery("select LOCAL DATETIME, LOCAL_DATETIME() " + // - "from Call c "); - - assertQuery("select OFFSET DATETIME, OFFSET_DATETIME() " + // - "from Call c "); - - assertQuery("select OFFSET DATETIME AS offsetDatetime, OFFSET_DATETIME() AS offset_datetime " + // - "from Call c "); - } - - @Test // GH-3689 - void cube() { - - assertQuery("select CUBE(foo), CUBE(foo, bar) " + // - "from Call c "); - - assertQuery("select c.callerId from Call c GROUP BY CUBE(state, province)"); - } - - @Test // GH-3689 - void rollup() { - - assertQuery("select ROLLUP(foo), ROLLUP(foo, bar) " + // - "from Call c "); - - assertQuery("select c.callerId from Call c GROUP BY ROLLUP(state, province)"); - } - - @Test - void pathExpressionsNamedParametersExample() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.status = :stat - """); - } - - @Test - void betweenExpressionsExample() { - - assertQuery(""" - SELECT t - FROM CreditCard c JOIN c.transactionHistory t - WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 - """); - } - - @Test - void isEmptyExample() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void memberOfExample() { - - assertQuery(""" - SELECT p - FROM Person p - WHERE 'Joe' MEMBER OF p.nicknames - """); - } - - @Test - void existsSubSelectExample1() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - - @Test // GH-3689 - void everyAll() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EVERY (SELECT spouseEmp - FROM Employee spouseEmp) > 1 - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ALL (SELECT spouseEmp - FROM Employee spouseEmp) > 1 - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ALL (foo > 1) OVER (PARTITION BY bar) - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ALL VALUES (foo) > 1 - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ALL ELEMENTS (foo) > 1 - """); - } - - @Test // GH-3689 - void anySome() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ANY (SELECT spouseEmp - FROM Employee spouseEmp) > 1 - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE SOME (SELECT spouseEmp - FROM Employee spouseEmp) > 1 - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ANY (foo > 1) OVER (PARTITION BY bar) - """); - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE ANY VALUES (foo) > 1 - """); - } - - @Test // GH-3689 - void listAgg() { - - assertQuery("select listagg(p.number, ', ') within group (order by p.type, p.number) " + // - "from Phone p " + // - "group by p.person"); - } - - @Test - void allExample() { - - assertQuery(""" - SELECT emp - FROM Employee emp - WHERE emp.salary > ALL (SELECT m.salary - FROM Manager m - WHERE m.department = emp.department) - """); - } - - @Test - void existsSubSelectExample2() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - - @Test - void subselectNumericComparisonExample1() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE (SELECT AVG(o.price) FROM c.orders o) > 100 - """); - } - - @Test - void subselectNumericComparisonExample2() { - - assertQuery(""" - SELECT goodCustomer - FROM Customer goodCustomer - WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c) - """); - } - - @Test - void indexExample() { - - assertQuery(""" - SELECT w.name - FROM Course c JOIN c.studentWaitlist w - WHERE c.name = 'Calculus' - AND INDEX(w) = 0 - """); - } - - /** - * @see #functionInvocationExampleWithCorrection() - */ - @Test - void functionInvocationExampleAsBooleanExpression() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) - """); - } - - @Test - void functionInvocationExampleWithCorrection() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE - """); - } - - @ParameterizedTest // GH-3628 - @ValueSource(strings = { "is true", "is not true", "is false", "is not false" }) - void functionInvocationWithIsBoolean(String booleanComparison) { - - assertQuery(""" - from RoleTmpl where find_in_set(:appId, appIds) %s - """.formatted(booleanComparison)); - } - - @Test - void updateCaseExample1() { - - assertQuery(""" - UPDATE Employee e - SET e.salary = - CASE WHEN e.rating = 1 THEN e.salary * 1.1 - WHEN e.rating = 2 THEN e.salary * 1.05 - ELSE e.salary * 1.01 - END - """); - } - - @Test - void updateCaseExample2() { - - assertQuery(""" - UPDATE Employee e - SET e.salary = - CASE e.rating WHEN 1 THEN e.salary * 1.1 - WHEN 2 THEN e.salary * 1.05 - ELSE e.salary * 1.01 - END - """); - } - - @Test - void selectCaseExample1() { - - assertQuery(""" - SELECT e.name, - CASE TYPE(e) WHEN Exempt THEN 'Exempt' - WHEN Contractor THEN 'Contractor' - WHEN Intern THEN 'Intern' - ELSE 'NonExempt' - END - FROM Employee e - WHERE e.dept.name = 'Engineering' - """); - } - - @Test - void selectCaseExample2() { - - assertQuery(""" - SELECT e.name, - f.name, - CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' - WHEN f.annualMiles > 25000 THEN 'Gold ' - ELSE '' - END, - 'Frequent Flyer') - FROM Employee e JOIN e.frequentFlierPlan f - """); - } - - @Test - void theRest() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (Exempt, Contractor) - """); - } - - @Test - void theRest2() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (:empType1, :empType2) - """); - } - - @Test - void theRest3() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN :empTypes - """); - } - - @Test - void theRest4() { - - assertQuery(""" - SELECT TYPE(e) - FROM Employee e - WHERE TYPE(e) <> Exempt - """); - } - - @Test - void theRest5() { - - assertQuery(""" - SELECT c.status, AVG(c.filledOrderCount), COUNT(c) - FROM Customer c - GROUP BY c.status - HAVING c.status IN (1, 2) - """); - } - - @Test - void theRest6() { - - assertQuery(""" - SELECT c.country, COUNT(c) - FROM Customer c - GROUP BY c.country - HAVING COUNT(c) > 30 - """); - } - - @Test - void theRest7() { - - assertQuery(""" - SELECT c, COUNT(o) - FROM Customer c JOIN c.orders o - GROUP BY c - HAVING COUNT(o) >= 5 - """); - } - - @Test - void theRest8() { - - assertQuery(""" - SELECT c.id, c.status - FROM Customer c JOIN c.orders o - WHERE o.count > 100 - """); - } - - @Test - void theRest9() { - - assertQuery(""" - SELECT v.location.street, KEY(i).title, VALUE(i) - FROM VideoStore v JOIN v.videoInventory i - WHERE v.location.zipcode = '94301' AND VALUE(i) > 0 - """); - } - - @Test - void theRest10() { - - assertQuery(""" - SELECT o.lineItems FROM Order AS o - """); - } - - @Test - void theRest11() { - - assertQuery(""" - SELECT c, COUNT(l) AS itemCount - FROM Customer c JOIN c.Orders o JOIN o.lineItems l - WHERE c.address.state = 'CA' - GROUP BY c - ORDER BY itemCount - """); - } - - @Test - void theRest12() { - - assertQuery(""" - SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) - FROM Customer c JOIN c.orders o - WHERE o.count > 100 - """); - } - - @Test - void theRest13() { - - assertQuery(""" - SELECT e.address AS addr - FROM Employee e - """); - } - - @Test - void theRest14() { - - assertQuery(""" - SELECT AVG(o.quantity) FROM Order o - """); - } - - @Test - void theRest15() { - - assertQuery(""" - SELECT SUM(l.price) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - """); - } - - @Test - void theRest16() { - - assertQuery(""" - SELECT COUNT(o) FROM Order o - """); - } - - @Test - void theRest17() { - - assertQuery(""" - SELECT COUNT(l.price) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - """); - } - - @Test - void theRest18() { - - assertQuery(""" - SELECT COUNT(l) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL - """); - } - - @Test - void theRest19() { - - assertQuery(""" - SELECT o - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - ORDER BY o.quantity DESC, o.totalcost - """); - } - - @Test - void theRest20() { - - assertQuery(""" - SELECT o.quantity, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - ORDER BY o.quantity, a.zipcode - """); - } - - @Test - void theRest21() { - - assertQuery(""" - SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' AND a.county = 'Santa Clara' - ORDER BY o.quantity, taxedCost, a.zipcode - """); - } - - @Test - void theRest22() { - - assertQuery(""" - SELECT AVG(o.quantity) as q, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - GROUP BY a.zipcode - ORDER BY q DESC - """); - } - - @Test - void theRest23() { - - assertQuery(""" - SELECT p.product_name - FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - ORDER BY p.price - """); - } - - /** - * This query is specifically dubbed illegal in the spec, but apparently works with Hibernate. - */ - @Test - void theRest24() { - - assertQuery(""" - SELECT p.product_name - FROM Order o , IN(o.lineItems) l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - ORDER BY o.quantity - """); - } - - @Test - void theRest25() { - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - """); - } - - @Test - void collectionIsEmpty() { - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - AND c.orders IS EMPTY - """); - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - AND c.orders IS NOT EMPTY - """); - } - - @Test // GH-3628 - void booleanPredicate() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders IS TRUE - """); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders IS NOT TRUE - """); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders IS FALSE - """); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders IS NOT FALSE - """); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders IS NULL - """); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders IS NOT NULL - """); - } - - @ParameterizedTest // GH-3628 - @ValueSource(strings = { "IS DISTINCT FROM", "IS NOT DISTINCT FROM" }) - void distinctFromPredicate(String distinctFrom) { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders %s c.payments - """.formatted(distinctFrom)); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.orders %s c.payments - """.formatted(distinctFrom)); - - assertQuery(""" - SELECT c - FROM Customer c - GROUP BY c.lastname - HAVING c.orders %s c.payments - """.formatted(distinctFrom)); - - assertQuery(""" - SELECT c - FROM Customer c - WHERE EXISTS (SELECT c2 - FROM Customer c2 - WHERE c2.orders %s c.orders) - """.formatted(distinctFrom)); - } - - @Test - void theRest27() { - - assertQuery(""" - UPDATE Customer c - SET c.status = 'outstanding' - WHERE c.balance < 10000 - """); - } - - @Test - void theRest28() { - - assertQuery(""" - UPDATE Employee e - SET e.address.building = 22 - WHERE e.address.building = 14 - AND e.address.city = 'Santa Clara' - AND e.project = 'Jakarta EE' - """); - } - - @Test - void theRest29() { - - assertQuery(""" - SELECT o - FROM Order o - """); - } - - @Test - void theRest30() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.shippingAddress.state = 'CA' - """); - } - - @Test - void theRest31() { - - assertQuery(""" - SELECT DISTINCT o.shippingAddress.state - FROM Order o - """); - } - - @Test - void theRest32() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - """); - } - - @Test - void theRest33() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS NOT EMPTY - """); - } - - @Test - void theRest34() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void theRest35() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.shipped = FALSE - """); - } - - @Test - void theRest36() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE - NOT (o.shippingAddress.state = o.billingAddress.state AND - o.shippingAddress.city = o.billingAddress.city AND - o.shippingAddress.street = o.billingAddress.street) - """); - } - - @Test - void theRest37() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.shippingAddress <> o.billingAddress - """); - } - - @Test - void theRest38() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.product.name = ?1 - """); - } - - @Test // GH-3689 - void insertQueries() { - - assertQuery("insert Person (id, name) values (100L, 'Jane Doe')"); - - assertQuery("insert Person (id, name) values " + // - "(101L, 'J A Doe III'), " + // - "(102L, 'J X Doe'), " + // - "(103L, 'John Doe, Jr')"); - - assertQuery("insert into Partner (id, name) " + // - "select p.id, p.name from Person p "); - - assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " - + "ON CONFLICT (range) DO UPDATE SET price = :price, type = :priceType"); - - assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " - + "ON CONFLICT ON CONSTRAINT foo DO UPDATE SET price = :price, type = :priceType"); - - assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " - + "ON CONFLICT ON CONSTRAINT foo DO NOTHING"); - } - - @Test - void hqlQueries() { - - assertQuery("from Person"); - assertQuery("select local datetime"); - assertQuery("from Person p select p.name"); - assertQuery("update Person set nickName = 'Nacho' " + // - "where name = 'Ignacio'"); - assertQuery("update Person p " + // - "set p.name = :newName " + // - "where p.name = :oldName"); - assertQuery("update Person " + // - "set name = :newName " + // - "where name = :oldName"); - assertQuery("update versioned Person " + // - "set name = :newName " + // - "where name = :oldName"); - - assertQuery("select p " + // - "from Person p " + // - "where p.name like 'Joe'"); - assertQuery("select p " + // - "from Person p " + // - "where p.name like 'Joe''s'"); - assertQuery("select p " + // - "from Person p " + // - "where p.id = 1"); - assertQuery("select p " + // - "from Person p " + // - "where p.id = 1L"); - assertQuery("select c " + // - "from Call c " + // - "where c.duration > 100.5"); - assertQuery("select c " + // - "from Call c " + // - "where c.duration > 100.5F"); - assertQuery("select c " + // - "from Call c " + // - "where c.duration > 1e+2"); - assertQuery("select c " + // - "from Call c " + // - "where c.duration > 1e+2F"); - assertQuery("from Phone ph " + // - "where ph.type = LAND_LINE"); - assertQuery("select java.lang.Math.PI"); - assertQuery("select 'Customer ' || p.name " + // - "from Person p " + // - "where p.id = 1"); - assertQuery("select sum(ch.duration) * :multiplier " + // - "from Person pr " + // - "join pr.phones ph " + // - "join ph.callHistory ch " + // - "where ph.id = 1L "); - assertQuery("select year(local date) - year(p.createdOn) " + // - "from Person p " + // - "where p.id = 1L"); - assertQuery("select p " + // - "from Person p " + // - "where year(local date) - year(p.createdOn) > 1"); - assertQuery("select " + // - " case p.nickName " + // - " when 'NA' " + // - " then '' " + // - " else p.nickName " + // - " end " + // - "from Person p"); - assertQuery("select " + // - " case " + // - " when p.nickName is null " + // - " then " + // - " case " + // - " when p.name is null " + // - " then '' " + // - " else p.name " + // - " end" + // - " else p.nickName " + // - " end " + // - "from Person p"); - assertQuery("select " + // - " case when p.nickName is null " + // - " then p.id * 1000 " + // - " else p.id " + // - " end " + // - "from Person p " + // - "order by p.id"); - assertQuery("select p " + // - "from Payment p " + // - "where type(p) = CreditCardPayment"); - assertQuery("select p " + // - "from Payment p " + // - "where type(p) = :type"); - assertQuery("select p " + // - "from Payment p " + // - "where length(treat(p as CreditCardPayment).cardNumber) between 16 and 20"); - assertQuery("select nullif(p.nickName, p.name) " + // - "from Person p"); - assertQuery("select " + // - " case" + // - " when p.nickName = p.name" + // - " then null" + // - " else p.nickName" + // - " end " + // - "from Person p"); - assertQuery("select coalesce(p.nickName, '') " + // - "from Person p"); - assertQuery("select coalesce(p.nickName, p.name, '') " + // - "from Person p"); - assertQuery("select p " + // - "from Person p " + // - "where size(p.phones) >= 2"); - assertQuery("select concat(p.number, ' : ', cast(c.duration as string)) " + // - "from Call c " + // - "join c.phone p"); - assertQuery("select upper(p.name) " + // - "from Person p "); - assertQuery("select lower(p.name) " + // - "from Person p "); - assertQuery("select trim(p.name) " + // - "from Person p "); - assertQuery("select trim(leading ' ' from p.name) " + // - "from Person p "); - assertQuery("select length(p.name) " + // - "from Person p "); - assertQuery("select locate('John', p.name) " + // - "from Person p "); - assertQuery("select abs(c.duration) " + // - "from Call c "); - assertQuery("select mod(c.duration, 10) " + // - "from Call c "); - assertQuery("select sqrt(c.duration) " + // - "from Call c "); - assertQuery("select cast(c.duration as String) " + // - "from Call c "); - assertQuery("select str(c.timestamp) " + // - "from Call c "); - assertQuery("select str(cast(duration as float) / 60, 4, 2) " + // - "from Call c "); - assertQuery("select c " + // - "from Call c " + // - "where extract(date from c.timestamp) = local date"); - assertQuery("select extract(year from c.timestamp) " + // - "from Call c "); - assertQuery("select year(c.timestamp) " + // - "from Call c "); - assertQuery("select var_samp(c.duration) as sampvar, var_pop(c.duration) as popvar " + // - "from Call c "); - assertQuery("select bit_length(c.phone.number) " + // - "from Call c "); - assertQuery("select c " + // - "from Call c " + // - "where c.duration < 30 "); - assertQuery("select p " + // - "from Person p " + // - "where p.name like 'John%' "); - assertQuery("select p " + // - "from Person p " + // - "where p.createdOn > '1950-01-01' "); - assertQuery("select p " + // - "from Phone p " + // - "where p.type = 'MOBILE' "); - assertQuery("select p " + // - "from Payment p " + // - "where p.completed = true "); - assertQuery("select p " + // - "from Payment p " + // - "where type(p) = WireTransferPayment "); - assertQuery("select p " + // - "from Payment p, Phone ph " + // - "where p.person = ph.person "); - assertQuery("select p " + // - "from Person p " + // - "join p.phones ph " + // - "where p.id = 1L and index(ph) between 0 and 3"); - assertQuery("select p " + // - "from Person p " + // - "where p.createdOn between '1999-01-01' and '2001-01-02'"); - assertQuery("select c " + // - "from Call c " + // - "where c.duration between 5 and 20"); - assertQuery("select p " + // - "from Person p " + // - "where p.name between 'H' and 'M'"); - assertQuery("select p " + // - "from Person p " + // - "where p.nickName is not null"); - assertQuery("select p " + // - "from Person p " + // - "where p.nickName is null"); - assertQuery("select p " + // - "from Person p " + // - "where p.name like 'Jo%'"); - assertQuery("select p " + // - "from Person p " + // - "where p.name not like 'Jo%'"); - assertQuery("select p " + // - "from Person p " + // - "where p.name like 'Dr|_%' escape '|'"); - assertQuery("select p " + // - "from Payment p " + // - "where type(p) in (CreditCardPayment, WireTransferPayment)"); - assertQuery("select p " + // - "from Phone p " + // - "where type in ('MOBILE', 'LAND_LINE')"); - assertQuery("select p " + // - "from Phone p " + // - "where type in :types"); - assertQuery("select distinct p " + // - "from Phone p " + // - "where p.person.id in (select py.person.id " + // - " from Payment py" + // - " where py.completed = true and py.amount > 50)"); - assertQuery("select distinct p " + // - "from Phone p " + // - "where p.person in (select py.person " + // - " from Payment py" + // - " where py.completed = true and py.amount > 50)"); - assertQuery("select distinct p " + // - "from Payment p " + // - "where (p.amount, p.completed) in ((50, true)," + // - " (100, true)," + // - " (5, false))"); - assertQuery("select p " + // - "from Person p " + // - "where 1 in indices (p.phones)"); - assertQuery("select distinct p.person " + // - "from Phone p " + // - "join p.calls c " + // - "where 50 > all (select duration" + // - " from Call" + // - " where phone = p) "); - assertQuery("select p " + // - "from Phone p " + // - "where local date > all elements (p.repairTimestamps)"); - assertQuery("select p " + // - "from Person p " + // - "where :phone = some elements (p.phones)"); - assertQuery("select p " + // - "from Person p " + // - "where :phone member of p.phones"); - assertQuery("select p " + // - "from Person p " + // - "where exists elements (p.phones)"); - assertQuery("select p " + // - "from Person p " + // - "where p.phones is empty"); - assertQuery("select p " + // - "from Person p " + // - "where p.phones is not empty"); - assertQuery("select p " + // - "from Person p " + // - "where p.phones is not empty"); - assertQuery("select p " + // - "from Person p " + // - "where 'Home address' member of p.addresses"); - assertQuery("select p " + // - "from Person p " + // - "where 'Home address' not member of p.addresses"); - assertQuery("select p " + // - "from Person p"); - assertQuery("select p " + // - "from org.hibernate.userguide.model.Person p"); - assertQuery("select distinct pr, ph " + // - "from Person pr, Phone ph " + // - "where ph.person = pr and ph is not null"); - assertQuery("select distinct pr1 " + // - "from Person pr1, Person pr2 " + // - "where pr1.id <> pr2.id " + // - " and pr1.address = pr2.address " + // - " and pr1.createdOn < pr2.createdOn"); - assertQuery("select distinct pr, ph " + // - "from Person pr cross join Phone ph " + // - "where ph.person = pr and ph is not null"); - assertQuery("select p " + // - "from Payment p "); - assertQuery("select d.owner, d.payed " + // - "from (select p.person as owner, c.payment is not null as payed " + // - " from Call c " + // - " join c.phone p " + // - " where p.number = :phoneNumber) d"); - assertQuery("select distinct pr " + // - "from Person pr " + // - "join Phone ph on ph.person = pr " + // - "where ph.type = :phoneType"); - assertQuery("select distinct pr " + // - "from Person pr " + // - "join pr.phones ph " + // - "where ph.type = :phoneType"); - assertQuery("select distinct pr " + // - "from Person pr " + // - "inner join pr.phones ph " + // - "where ph.type = :phoneType"); - assertQuery("select distinct pr " + // - "from Person pr " + // - "left join pr.phones ph " + // - "where ph is null " + // - " or ph.type = :phoneType"); - assertQuery("select distinct pr " + // - "from Person pr " + // - "left outer join pr.phones ph " + // - "where ph is null " + // - " or ph.type = :phoneType"); - assertQuery("select pr.name, ph.number " + // - "from Person pr " + // - "left join pr.phones ph with ph.type = :phoneType "); - assertQuery("select pr.name, ph.number " + // - "from Person pr " + // - "left join pr.phones ph on ph.type = :phoneType "); - assertQuery("select distinct pr " + // - "from Person pr " + // - "left join fetch pr.phones "); - assertQuery("select a, ccp " + // - "from Account a " + // - "join treat(a.payments as CreditCardPayment) ccp " + // - "where length(ccp.cardNumber) between 16 and 20"); - assertQuery("select c, ccp " + // - "from Call c " + // - "join treat(c.payment as CreditCardPayment) ccp " + // - "where length(ccp.cardNumber) between 16 and 20"); - assertQuery("select longest.duration " + // - "from Phone p " + // - "left join lateral (" + // - "select c.duration as duration " + // - " from p.calls c" + // - " order by c.duration desc" + // - " limit 1 " + // - " ) longest " + // - "where p.number = :phoneNumber"); - assertQuery("select ph " + // - "from Phone ph " + // - "where ph.person.address = :address "); - assertQuery("select ph " + // - "from Phone ph " + // - "join ph.person pr " + // - "where pr.address = :address "); - assertQuery("select ph " + // - "from Phone ph " + // - "where ph.person.address = :address " + // - " and ph.person.createdOn > :timestamp"); - assertQuery("select ph " + // - "from Phone ph " + // - "inner join ph.person pr " + // - "where pr.address = :address " + // - " and pr.createdOn > :timestamp"); - assertQuery("select ph " + // - "from Person pr " + // - "join pr.phones ph " + // - "join ph.calls c " + // - "where pr.address = :address " + // - " and c.duration > :duration"); - assertQuery("select ch " + // - "from Phone ph " + // - "join ph.callHistory ch " + // - "where ph.id = :id "); - assertQuery("select value(ch) " + // - "from Phone ph " + // - "join ph.callHistory ch " + // - "where ph.id = :id "); - assertQuery("select key(ch) " + // - "from Phone ph " + // - "join ph.callHistory ch " + // - "where ph.id = :id "); - assertQuery("select key(ch) " + // - "from Phone ph " + // - "join ph.callHistory ch " + // - "where ph.id = :id "); - assertQuery("select entry (ch) " + // - "from Phone ph " + // - "join ph.callHistory ch " + // - "where ph.id = :id "); - assertQuery("select sum(ch.duration) " + // - "from Person pr " + // - "join pr.phones ph " + // - "join ph.callHistory ch " + // - "where ph.id = :id " + // - " and index(ph) = :phoneIndex"); - assertQuery("select value(ph.callHistory) " + // - "from Phone ph " + // - "where ph.id = :id "); - assertQuery("select key(ph.callHistory) " + // - "from Phone ph " + // - "where ph.id = :id "); - assertQuery("select p " + // - "from Person p " + // - "where p.phones[0].type = LAND_LINE"); - assertQuery("select p " + // - "from Person p " + // - "where p.addresses['HOME'] = :address"); - assertQuery("select pr " + // - "from Person pr " + // - "where pr.phones[max(indices(pr.phones))].type = 'LAND_LINE'"); - assertQuery("select p.name, p.nickName " + // - "from Person p "); - assertQuery("select p.name as name, p.nickName as nickName " + // - "from Person p "); - assertQuery("select new org.hibernate.userguide.hql.CallStatistics(count(c), " + // - " sum(c.duration), " + // - " min(c.duration), " + // - " max(c.duration), " + // - " avg(c.duration)" + // - ") " + // - "from Call c "); - assertQuery("select new map(p.number as phoneNumber, " + // - " sum(c.duration) as totalDuration, " + // - " avg(c.duration) as averageDuration) " + // - "from Call c " + // - "join c.phone p " + // - "group by p.number "); - assertQuery("select new list(p.number," + // - " c.duration) " + // - "from Call c " + // - "join c.phone p "); - assertQuery("select distinct p.lastName " + // - "from Person p"); - assertQuery("select " + // - " count(c), " + // - " sum(c.duration), " + // - " min(c.duration), " + // - " max(c.duration), " + // - " avg(c.duration) " + // - "from Call c "); - assertQuery("select count(distinct c.phone) " + // - "from Call c "); - assertQuery("select p.number, count(c) " + // - "from Call c " + // - "join c.phone p " + // - "group by p.number"); - assertQuery("select p " + // - "from Phone p " + // - "where max(elements(p.calls)) = :call"); - assertQuery("select p " + // - "from Phone p " + // - "where min(elements(p.calls)) = :call"); - assertQuery("select p " + // - "from Person p " + // - "where max(indices(p.phones)) = 0"); - assertQuery("select count(c) filter (where c.duration < 30) " + // - "from Call c "); - assertQuery("select p.number, count(c) filter (where c.duration < 30) " + // - "from Call c " + // - "join c.phone p " + // - "group by p.number"); - assertQuery("select sum(c.duration) " + // - "from Call c "); - assertQuery("select p.name, sum(c.duration) " + // - "from Call c " + // - "join c.phone ph " + // - "join ph.person p " + // - "group by p.name"); - assertQuery("select p, sum(c.duration) " + // - "from Call c " + // - "join c.phone ph " + // - "join ph.person p " + // - "group by p"); - assertQuery("select p.name, sum(c.duration) " + // - "from Call c " + // - "join c.phone ph " + // - "join ph.person p " + // - "group by p.name " + // - "having sum(c.duration) > 1000"); - assertQuery("select p.name from Person p " + // - "union " + // - "select p.nickName from Person p where p.nickName is not null"); - assertQuery("select p " + // - "from Person p " + // - "order by p.name"); - assertQuery("select p.name, sum(c.duration) as total " + // - "from Call c " + // - "join c.phone ph " + // - "join ph.person p " + // - "group by p.name " + // - "order by total"); - assertQuery("select c " + // - "from Call c " + // - "join c.phone p " + // - "order by p.number " + // - "limit 50"); - assertQuery("select c " + // - "from Call c " + // - "join c.phone p " + // - "order by p.number " + // - "fetch first 50 rows only"); - assertQuery("select p " + // - "from Phone p " + // - "join fetch p.calls " + // - "order by p " + // - "limit 50"); - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java deleted file mode 100644 index a346c8c39e..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright 2024-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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -/** - * Test to verify compliance of {@link JpqlParser} with standard SQL. Other than {@link JpqlSpecificationTests} tests in - * this class check that the parser follows a lenient approach and does not error on well known concepts like numeric - * suffix. - * - * @author Christoph Strobl - * @author Mark Paluch - */ -class JpqlComplianceTests { - - private static String parseWithoutChanges(String query) { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); - - return QueryRenderer.render(new JpqlQueryRenderer().visit(parser.getContext())); - } - - private void assertQuery(String query) { - - String slimmedDownQuery = reduceWhitespace(query); - assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); - } - - private String reduceWhitespace(String original) { - - return original // - .replaceAll("[ \\t\\n]{1,}", " ") // - .trim(); - } - - @Test - void selectQueries() { - - assertQuery("Select e FROM Employee e WHERE e.salary > 100000"); - assertQuery("Select e FROM Employee e WHERE e.id = :id"); - assertQuery("Select MAX(e.salary) FROM Employee e"); - assertQuery("Select e.firstName FROM Employee e"); - assertQuery("Select e.firstName, e.lastName FROM Employee e"); - } - - @Test - void selectClause() { - - assertQuery("SELECT COUNT(e) FROM Employee e"); - assertQuery("SELECT MAX(e.salary) FROM Employee e"); - assertQuery("SELECT NEW com.acme.reports.EmpReport(e.firstName, e.lastName, e.salary) FROM Employee e"); - } - - @Test - void fromClause() { - - assertQuery("SELECT e FROM Employee e"); - assertQuery("SELECT e, a FROM Employee e, MailingAddress a WHERE e.address = a.address"); - assertQuery("SELECT e FROM com.acme.Employee e"); - } - - @Test - void join() { - - assertQuery("SELECT e FROM Employee e JOIN e.address a WHERE a.city = :city"); - assertQuery("SELECT e FROM Employee e JOIN e.projects p JOIN e.projects p2 WHERE p.name = :p1 AND p2.name = :p2"); - } - - @Test - void joinFetch() { - - assertQuery("SELECT e FROM Employee e JOIN FETCH e.address"); - assertQuery("SELECT e FROM Employee e JOIN FETCH e.address a ORDER BY a.city"); - assertQuery("SELECT e FROM Employee e JOIN FETCH e.address AS a ORDER BY a.city"); - } - - @Test - void leftJoin() { - assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); - } - - @Test // GH-3277 - void numericLiterals() { - - assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); - assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); - assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14"); - assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); - assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); - } - - @Test // GH-3308 - void newWithStrings() { - assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); - } - - @Test - void orderByClause() { - - assertQuery("SELECT e FROM Employee e ORDER BY e.lastName ASC, e.firstName ASC"); // Typo in EQL document - assertQuery("SELECT e FROM Employee e LEFT JOIN e.manager m ORDER BY m.lastName NULLS FIRST"); - assertQuery("SELECT e FROM Employee e ORDER BY e.address"); - } - - @Test - void groupByClause() { - - assertQuery("SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city"); - assertQuery("SELECT e, COUNT(p) FROM Employee e LEFT JOIN e.projects p GROUP BY e"); - } - - @Test - void havingClause() { - assertQuery( - "SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city HAVING AVG(e.salary) > 100000"); - } - - @Test // GH-3136 - void union() { - - assertQuery(""" - SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1 - UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 - """); - assertQuery(""" - SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 - INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 - """); - assertQuery(""" - SELECT e FROM Employee e - EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary - """); - } - - @Test - void whereClause() { - // TBD - } - - @Test - void updateQueries() { - assertQuery("UPDATE Employee e SET e.salary = 60000 WHERE e.salary = 50000"); - } - - @Test - void deleteQueries() { - assertQuery("DELETE FROM Employee e WHERE e.department IS NULL"); - } - - @Test - void literals() { - - assertQuery("SELECT e FROM Employee e WHERE e.name = 'Bob'"); - assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); - assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); - assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); - assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); - assertQuery("SELECT e FROM Employee e WHERE e.active = TRUE"); - assertQuery("SELECT e FROM Employee e WHERE e.startDate = {d'2012-01-03'}"); - assertQuery("SELECT e FROM Employee e WHERE e.startTime = {t'09:00:00'}"); - assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}"); - assertQuery("SELECT e FROM Employee e WHERE e.gender = org.acme.Gender.MALE"); - assertQuery("UPDATE Employee e SET e.manager = NULL WHERE e.manager = :manager"); - } - - @Test - void functionsInSelect() { - - assertQuery("SELECT e.salary - 1000 FROM Employee e"); - assertQuery("SELECT e.salary + 1000 FROM Employee e"); - assertQuery("SELECT e.salary * 2 FROM Employee e"); - assertQuery("SELECT e.salary * 2.0 FROM Employee e"); - assertQuery("SELECT e.salary / 2 FROM Employee e"); - assertQuery("SELECT e.salary / 2.0 FROM Employee e"); - assertQuery("SELECT ABS(e.salary - e.manager.salary) FROM Employee e"); - assertQuery( - "select e from Employee e where case e.firstName when 'Bob' then 'Robert' when 'Jill' then 'Gillian' else '' end = 'Robert'"); - assertQuery( - "select case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end from Employee e where e.firstName = 'Bob' or e.firstName = 'Jill'"); - assertQuery( - "select e from Employee e where case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end = 'Robert'"); - assertQuery("SELECT COALESCE(e.salary, 0) FROM Employee e"); - assertQuery("SELECT CONCAT(e.firstName, ' ', e.lastName) FROM Employee e"); - assertQuery("SELECT e.name, CURRENT_DATE FROM Employee e"); - assertQuery("SELECT e.name, CURRENT_TIME FROM Employee e"); - assertQuery("SELECT e.name, CURRENT_TIMESTAMP FROM Employee e"); - assertQuery("SELECT LENGTH(e.lastName) FROM Employee e"); - assertQuery("SELECT LOWER(e.lastName) FROM Employee e"); - assertQuery("SELECT MOD(e.hoursWorked, 8) FROM Employee e"); - assertQuery("SELECT NULLIF(e.salary, 0) FROM Employee e"); - assertQuery("SELECT SQRT(o.RESULT) FROM Output o"); - assertQuery("SELECT SUBSTRING(e.lastName, 0, 2) FROM Employee e"); - assertQuery( - "SELECT TRIM(TRAILING FROM e.lastName), TRIM(e.lastName), TRIM(LEADING '-' FROM e.lastName) FROM Employee e"); - assertQuery("SELECT UPPER(e.lastName) FROM Employee e"); - assertQuery("SELECT CAST(e.salary NUMERIC(10, 2)) FROM Employee e"); - assertQuery("SELECT EXTRACT(YEAR FROM e.startDate) FROM Employee e"); - } - - @Test - void functionsInWhere() { - - assertQuery("SELECT e FROM Employee e WHERE e.salary - 1000 > 0"); - assertQuery("SELECT e FROM Employee e WHERE e.salary + 1000 > 0"); - assertQuery("SELECT e FROM Employee e WHERE e.salary * 2 > 0"); - assertQuery("SELECT e FROM Employee e WHERE e.salary * 2.0 > 0.0"); - assertQuery("SELECT e FROM Employee e WHERE e.salary / 2 > 0"); - assertQuery("SELECT e FROM Employee e WHERE e.salary / 2.0 > 0.0"); - assertQuery("SELECT e FROM Employee e WHERE ABS(e.salary - e.manager.salary) > 0"); - assertQuery("SELECT e FROM Employee e WHERE COALESCE(e.salary, 0) > 0"); - assertQuery("SELECT e FROM Employee e WHERE CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE CURRENT_DATE > CURRENT_TIME"); - assertQuery("SELECT e FROM Employee e WHERE CURRENT_TIME > CURRENT_TIMESTAMP"); - assertQuery("SELECT e FROM Employee e WHERE LENGTH(e.lastName) > 0"); - assertQuery("SELECT e FROM Employee e WHERE LOWER(e.lastName) = 'bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked, 8) > 0"); - assertQuery("SELECT e FROM Employee e WHERE SQRT(o.RESULT) > 0.0"); - assertQuery("SELECT e FROM Employee e WHERE SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE TRIM(TRAILING FROM e.lastName) = 'Bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE TRIM(e.lastName) = 'Bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE TRIM(LEADING '-' FROM e.lastName) = 'Bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE UPPER(e.lastName) = 'BILBO'"); - assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); - assertQuery("SELECT e FROM Employee e WHERE EXTRACT(YEAR FROM e.startDate) = '2023'"); - } - - @Test - void specialOperators() { - - assertQuery("SELECT toDo FROM Employee e JOIN e.toDoList toDo WHERE INDEX(toDo) = 1"); - assertQuery("SELECT p FROM Employee e JOIN e.priorities p WHERE KEY(p) = 'high'"); - assertQuery("SELECT e FROM Employee e WHERE SIZE(e.managedEmployees) < 2"); - assertQuery("SELECT e FROM Employee e WHERE e.managedEmployees IS EMPTY"); - assertQuery("SELECT e FROM Employee e WHERE 'write code' MEMBER OF e.responsibilities"); - assertQuery("SELECT p FROM Project p WHERE TYPE(p) = LargeProject"); - - /** - * NOTE: The following query has been altered to properly align with EclipseLink test code despite NOT matching - * their ref docs. See https://github.com/eclipse-ee4j/eclipselink/issues/1949 for more details. - */ - assertQuery("SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) p WHERE p.budget > 1000000"); - - assertQuery("SELECT p FROM Phone p WHERE FUNCTION('TO_NUMBER', p.areaCode) > 613"); - } - - @Test // GH-3314 - void isNullAndIsNotNull() { - - assertQuery("SELECT e FROM Employee e WHERE (e.active IS null OR e.active = true)"); - assertQuery("SELECT e FROM Employee e WHERE (e.active IS NULL OR e.active = true)"); - assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT null OR e.active = true)"); - assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)"); - } - - @Test // GH-3496 - void lateralShouldBeAValidParameter() { - - assertQuery("select e from Employee e where e.lateral = :_lateral"); - assertQuery("select te from TestEntity te where te.lateral = :lateral"); - } - - @Test // GH-3136 - void intersect() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 - INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 - """); - } - - @Test // GH-3136 - void except() { - - assertQuery(""" - SELECT e FROM Employee e - EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary - """); - } - - @ParameterizedTest // GH-3136 - @ValueSource(strings = { "STRING", "INTEGER", "FLOAT", "DOUBLE" }) - void cast(String targetType) { - assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); - } - - @ParameterizedTest // GH-3136 - @ValueSource(strings = { "LEFT", "RIGHT" }) - void leftRightStringFunctions(String keyword) { - assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); - } - - @Test // GH-3136 - void replaceStringFunctions() { - assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e"); - assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); - } - - @Test // GH-3136 - void stringConcatWithPipes() { - assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); - } - -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java index 4681161836..1ec46fef2d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java @@ -70,6 +70,266 @@ private String reduceWhitespace(String original) { .trim(); } + @Test + void selectQueries() { + + assertQuery("Select e FROM Employee e WHERE e.salary > 100000"); + assertQuery("Select e FROM Employee e WHERE e.id = :id"); + assertQuery("Select MAX(e.salary) FROM Employee e"); + assertQuery("Select e.firstName FROM Employee e"); + assertQuery("Select e.firstName, e.lastName FROM Employee e"); + } + + @Test + void selectClause() { + + assertQuery("SELECT COUNT(e) FROM Employee e"); + assertQuery("SELECT MAX(e.salary) FROM Employee e"); + assertQuery("SELECT NEW com.acme.reports.EmpReport(e.firstName, e.lastName, e.salary) FROM Employee e"); + } + + @Test + void fromClause() { + + assertQuery("SELECT e FROM Employee e"); + assertQuery("SELECT e, a FROM Employee e, MailingAddress a WHERE e.address = a.address"); + assertQuery("SELECT e FROM com.acme.Employee e"); + } + + @Test + void join() { + + assertQuery("SELECT e FROM Employee e JOIN e.address a WHERE a.city = :city"); + assertQuery("SELECT e FROM Employee e JOIN e.projects p JOIN e.projects p2 WHERE p.name = :p1 AND p2.name = :p2"); + } + + @Test + void joinFetch() { + + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address a ORDER BY a.city"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address AS a ORDER BY a.city"); + } + + @Test + void leftJoin() { + assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); + } + + @Test // GH-3277 + void numericLiterals() { + + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + } + + @Test // GH-3308 + void newWithStrings() { + assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); + } + + @Test + void orderByClause() { + + assertQuery("SELECT e FROM Employee e ORDER BY e.lastName ASC, e.firstName ASC"); // Typo in EQL document + assertQuery("SELECT e FROM Employee e LEFT JOIN e.manager m ORDER BY m.lastName NULLS FIRST"); + assertQuery("SELECT e FROM Employee e ORDER BY e.address"); + } + + @Test + void groupByClause() { + + assertQuery("SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city"); + assertQuery("SELECT e, COUNT(p) FROM Employee e LEFT JOIN e.projects p GROUP BY e"); + } + + @Test + void havingClause() { + assertQuery( + "SELECT AVG(e.salary), e.address.city FROM Employee e GROUP BY e.address.city HAVING AVG(e.salary) > 100000"); + } + + @Test // GH-3136 + void union() { + + assertQuery(""" + SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city1 + UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 + """); + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @Test + void whereClause() { + // TBD + } + + @Test + void updateQueries() { + assertQuery("UPDATE Employee e SET e.salary = 60000 WHERE e.salary = 50000"); + } + + @Test + void deleteQueries() { + assertQuery("DELETE FROM Employee e WHERE e.department IS NULL"); + } + + @Test + void literals() { + + assertQuery("SELECT e FROM Employee e WHERE e.name = 'Bob'"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + assertQuery("SELECT e FROM Employee e WHERE e.active = TRUE"); + assertQuery("SELECT e FROM Employee e WHERE e.startDate = {d'2012-01-03'}"); + assertQuery("SELECT e FROM Employee e WHERE e.startTime = {t'09:00:00'}"); + assertQuery("SELECT e FROM Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}"); + assertQuery("SELECT e FROM Employee e WHERE e.gender = org.acme.Gender.MALE"); + assertQuery("UPDATE Employee e SET e.manager = NULL WHERE e.manager = :manager"); + } + + @Test + void functionsInSelect() { + + assertQuery("SELECT e.salary - 1000 FROM Employee e"); + assertQuery("SELECT e.salary + 1000 FROM Employee e"); + assertQuery("SELECT e.salary * 2 FROM Employee e"); + assertQuery("SELECT e.salary * 2.0 FROM Employee e"); + assertQuery("SELECT e.salary / 2 FROM Employee e"); + assertQuery("SELECT e.salary / 2.0 FROM Employee e"); + assertQuery("SELECT ABS(e.salary - e.manager.salary) FROM Employee e"); + assertQuery( + "select e from Employee e where case e.firstName when 'Bob' then 'Robert' when 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery( + "select case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end from Employee e where e.firstName = 'Bob' or e.firstName = 'Jill'"); + assertQuery( + "select e from Employee e where case when e.firstName = 'Bob' then 'Robert' when e.firstName = 'Jill' then 'Gillian' else '' end = 'Robert'"); + assertQuery("SELECT COALESCE(e.salary, 0) FROM Employee e"); + assertQuery("SELECT CONCAT(e.firstName, ' ', e.lastName) FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_DATE FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIME FROM Employee e"); + assertQuery("SELECT e.name, CURRENT_TIMESTAMP FROM Employee e"); + assertQuery("SELECT LENGTH(e.lastName) FROM Employee e"); + assertQuery("SELECT LOWER(e.lastName) FROM Employee e"); + assertQuery("SELECT MOD(e.hoursWorked, 8) FROM Employee e"); + assertQuery("SELECT NULLIF(e.salary, 0) FROM Employee e"); + assertQuery("SELECT SQRT(o.RESULT) FROM Output o"); + assertQuery("SELECT SUBSTRING(e.lastName, 0, 2) FROM Employee e"); + assertQuery( + "SELECT TRIM(TRAILING FROM e.lastName), TRIM(e.lastName), TRIM(LEADING '-' FROM e.lastName) FROM Employee e"); + assertQuery("SELECT UPPER(e.lastName) FROM Employee e"); + assertQuery("SELECT CAST(e.salary NUMERIC(10, 2)) FROM Employee e"); + assertQuery("SELECT EXTRACT(YEAR FROM e.startDate) FROM Employee e"); + } + + @Test + void functionsInWhere() { + + assertQuery("SELECT e FROM Employee e WHERE e.salary - 1000 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary + 1000 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary * 2 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary * 2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary / 2 > 0"); + assertQuery("SELECT e FROM Employee e WHERE e.salary / 2.0 > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE ABS(e.salary - e.manager.salary) > 0"); + assertQuery("SELECT e FROM Employee e WHERE COALESCE(e.salary, 0) > 0"); + assertQuery("SELECT e FROM Employee e WHERE CONCAT(e.firstName, ' ', e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_DATE > CURRENT_TIME"); + assertQuery("SELECT e FROM Employee e WHERE CURRENT_TIME > CURRENT_TIMESTAMP"); + assertQuery("SELECT e FROM Employee e WHERE LENGTH(e.lastName) > 0"); + assertQuery("SELECT e FROM Employee e WHERE LOWER(e.lastName) = 'bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked, 8) > 0"); + assertQuery("SELECT e FROM Employee e WHERE SQRT(o.RESULT) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(TRAILING FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE TRIM(LEADING '-' FROM e.lastName) = 'Bilbo'"); + assertQuery("SELECT e FROM Employee e WHERE UPPER(e.lastName) = 'BILBO'"); + assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); + assertQuery("SELECT e FROM Employee e WHERE EXTRACT(YEAR FROM e.startDate) = '2023'"); + } + + @Test + void specialOperators() { + + assertQuery("SELECT toDo FROM Employee e JOIN e.toDoList toDo WHERE INDEX(toDo) = 1"); + assertQuery("SELECT p FROM Employee e JOIN e.priorities p WHERE KEY(p) = 'high'"); + assertQuery("SELECT e FROM Employee e WHERE SIZE(e.managedEmployees) < 2"); + assertQuery("SELECT e FROM Employee e WHERE e.managedEmployees IS EMPTY"); + assertQuery("SELECT e FROM Employee e WHERE 'write code' MEMBER OF e.responsibilities"); + assertQuery("SELECT p FROM Project p WHERE TYPE(p) = LargeProject"); + + /** + * NOTE: The following query has been altered to properly align with EclipseLink test code despite NOT matching + * their ref docs. See https://github.com/eclipse-ee4j/eclipselink/issues/1949 for more details. + */ + assertQuery("SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) p WHERE p.budget > 1000000"); + + assertQuery("SELECT p FROM Phone p WHERE FUNCTION('TO_NUMBER', p.areaCode) > 613"); + } + + @Test // GH-3314 + void isNullAndIsNotNull() { + + assertQuery("SELECT e FROM Employee e WHERE (e.active IS null OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NULL OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT null OR e.active = true)"); + assertQuery("SELECT e FROM Employee e WHERE (e.active IS NOT NULL OR e.active = true)"); + } + + @Test // GH-3136 + void intersect() { + + assertQuery(""" + SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode1 + INTERSECT SELECT e FROM Employee e JOIN e.phones p WHERE p.areaCode = :areaCode2 + """); + } + + @Test // GH-3136 + void except() { + + assertQuery(""" + SELECT e FROM Employee e + EXCEPT SELECT e FROM Employee e WHERE e.salary > e.manager.salary + """); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = { "STRING", "INTEGER", "FLOAT", "DOUBLE" }) + void cast(String targetType) { + assertQuery("SELECT CAST(e.salary AS %s) FROM Employee e".formatted(targetType)); + } + + @ParameterizedTest // GH-3136 + @ValueSource(strings = { "LEFT", "RIGHT" }) + void leftRightStringFunctions(String keyword) { + assertQuery("SELECT %s(e.name, 3) FROM Employee e".formatted(keyword)); + } + + @Test // GH-3136 + void replaceStringFunctions() { + assertQuery("SELECT REPLACE(e.name, 'o', 'a') FROM Employee e"); + assertQuery("SELECT REPLACE(e.name, ' ', '_') FROM Employee e"); + } + + @Test // GH-3136 + void stringConcatWithPipes() { + assertQuery("SELECT e.firstname || e.lastname AS name FROM Employee e"); + } + /** * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example */ @@ -339,6 +599,38 @@ OR TREAT(e AS Contractor).hours > 100 """); } + @Test // GH-3136 + void substring() { + + assertQuery("select substring(c.number, 1, 2) " + // + "from Call c"); + + assertQuery("select substring(c.number, 1) " + // + "from Call c"); + } + + @Test // GH-3136 + void currentDateFunctions() { + + assertQuery("select CURRENT_DATE " + // + "from Call c "); + + assertQuery("select CURRENT_TIME " + // + "from Call c "); + + assertQuery("select CURRENT_TIMESTAMP " + // + "from Call c "); + + assertQuery("select LOCAL_DATE " + // + "from Call c "); + + assertQuery("select LOCAL_TIME " + // + "from Call c "); + + assertQuery("select LOCAL_DATETIME " + // + "from Call c "); + } + @Test void pathExpressionsNamedParametersExample() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java deleted file mode 100644 index 566bfb8801..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java +++ /dev/null @@ -1,941 +0,0 @@ -/* - * Copyright 2022-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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; - -/** - * Tests built around examples of JPQL found in the JPA spec - * https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc
- *
- * IMPORTANT: Purely verifies the parser without any transformations. - * - * @author Greg Turnquist - * @since 3.1 - */ -class JpqlSpecificationTests { - - private static final String SPEC_FAULT = "Disabled due to spec fault> "; - - /** - * Parse the query using {@link HqlParser} then run it through the query-preserving {@link HqlQueryRenderer}. - */ - private static String parseWithoutChanges(String query) { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); - - return TokenRenderer.render(new JpqlQueryRenderer().visit(parser.getContext())); - } - - private void assertQuery(String query) { - - String slimmedDownQuery = reduceWhitespace(query); - assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); - } - - private String reduceWhitespace(String original) { - - return original // - .replaceAll("[ \\t\\n]{1,}", " ") // - .trim(); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - */ - @Test - void joinExample1() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order AS o JOIN o.lineItems AS l - WHERE l.shipped = FALSE - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#example - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#identification-variables - */ - @Test - void joinExample2() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l JOIN l.product p - WHERE p.productType = 'office_supplies' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#range-variable-declarations - */ - @Test - void rangeVariableDeclarations() { - - assertQuery(""" - SELECT DISTINCT o1 - FROM Order o1, Order o2 - WHERE o1.quantity > o2.quantity AND - o2.customer.lastname = 'Smith' AND - o2.customer.firstname = 'John' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample1() { - - assertQuery(""" - SELECT i.name, VALUE(p) - FROM Item i JOIN i.photos p - WHERE KEY(p) LIKE '%egret' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample2() { - - assertQuery(""" - SELECT i.name, p - FROM Item i JOIN i.photos p - WHERE KEY(p) LIKE '%egret' - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample3() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo.phones p - """); - } - - /** - * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#path-expressions - */ - @Test - void pathExpressionsExample4() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo c JOIN c.phones p - WHERE e.contactInfo.address.zipcode = '95054' - """); - } - - @Test - void pathExpressionSyntaxExample1() { - - assertQuery(""" - SELECT DISTINCT l.product - FROM Order AS o JOIN o.lineItems l - """); - } - - @Test - void joinsExample1() { - - assertQuery(""" - SELECT c FROM Customer c, Employee e WHERE c.hatsize = e.shoesize - """); - } - - @Test - void joinsExample2() { - - assertQuery(""" - SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1 - """); - } - - @Test - void joinsInnerExample() { - - assertQuery(""" - SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1 - """); - } - - @Test - void joinsInExample() { - - assertQuery(""" - SELECT OBJECT(c) FROM Customer c, IN(c.orders) o WHERE c.status = 1 - """); - } - - @Test - void doubleJoinExample() { - - assertQuery(""" - SELECT p.vendor - FROM Employee e JOIN e.contactInfo c JOIN c.phones p - WHERE c.address.zipcode = '95054' - """); - } - - @Test - void leftJoinExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - GROUP BY s.name - """); - } - - @Test - void leftJoinOnExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - ON p.status = 'inStock' - GROUP BY s.name - """); - } - - @Test - void leftJoinWhereExample() { - - assertQuery(""" - SELECT s.name, COUNT(p) - FROM Suppliers s LEFT JOIN s.products p - WHERE p.status = 'inStock' - GROUP BY s.name - """); - } - - @Test - void leftJoinFetchExample() { - - assertQuery(""" - SELECT d - FROM Department d LEFT JOIN FETCH d.employees - WHERE d.deptno = 1 - """); - } - - @Test - void collectionMemberExample() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.product.productType = 'office_supplies' - """); - } - - @Test - void collectionMemberInExample() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o, IN(o.lineItems) l - WHERE l.product.productType = 'office_supplies' - """); - } - - @Test - void fromClauseExample() { - - assertQuery(""" - SELECT o - FROM Order AS o JOIN o.lineItems l JOIN l.product p - """); - } - - @Test - void fromClauseDowncastingExample1() { - - assertQuery(""" - SELECT b.name, b.ISBN - FROM Order o JOIN TREAT(o.product AS Book) b - """); - } - - @Test - void fromClauseDowncastingExample2() { - - assertQuery(""" - SELECT e FROM Employee e JOIN TREAT(e.projects AS LargeProject) lp - WHERE lp.budget > 1000 - """); - } - - /** - * @see #fromClauseDowncastingExample3fixed() - */ - @Test - @Disabled(SPEC_FAULT + "Use double-quotes when it should be using single-quotes for a string literal") - void fromClauseDowncastingExample3_SPEC_BUG() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.projects p - WHERE TREAT(p AS LargeProject).budget > 1000 - OR TREAT(p AS SmallProject).name LIKE 'Persist%' - OR p.description LIKE "cost overrun" - """); - } - - @Test - void fromClauseDowncastingExample3fixed() { - - assertQuery(""" - SELECT e FROM Employee e JOIN e.projects p - WHERE TREAT(p AS LargeProject).budget > 1000 - OR TREAT(p AS SmallProject).name LIKE 'Persist%' - OR p.description LIKE 'cost overrun' - """); - } - - @Test - void fromClauseDowncastingExample4() { - - assertQuery(""" - SELECT e FROM Employee e - WHERE TREAT(e AS Exempt).vacationDays > 10 - OR TREAT(e AS Contractor).hours > 100 - """); - } - - @Test // GH-3136 - void substring() { - - assertQuery("select substring(c.number, 1, 2) " + // - "from Call c"); - - assertQuery("select substring(c.number, 1) " + // - "from Call c"); - } - - @Test // GH-3136 - void currentDateFunctions() { - - assertQuery("select CURRENT_DATE " + // - "from Call c "); - - assertQuery("select CURRENT_TIME " + // - "from Call c "); - - assertQuery("select CURRENT_TIMESTAMP " + // - "from Call c "); - - assertQuery("select LOCAL_DATE " + // - "from Call c "); - - assertQuery("select LOCAL_TIME " + // - "from Call c "); - - assertQuery("select LOCAL_DATETIME " + // - "from Call c "); - } - - @Test - void pathExpressionsNamedParametersExample() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE c.status = :stat - """); - } - - @Test - void betweenExpressionsExample() { - - assertQuery(""" - SELECT t - FROM CreditCard c JOIN c.transactionHistory t - WHERE c.holder.name = 'John Doe' AND INDEX(t) BETWEEN 0 AND 9 - """); - } - - @Test - void isEmptyExample() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void memberOfExample() { - - assertQuery(""" - SELECT p - FROM Person p - WHERE 'Joe' MEMBER OF p.nicknames - """); - } - - @Test - void existsSubSelectExample1() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - - @Test - void allExample() { - - assertQuery(""" - SELECT emp - FROM Employee emp - WHERE emp.salary > ALL (SELECT m.salary - FROM Manager m - WHERE m.department = emp.department) - """); - } - - @Test - void existsSubSelectExample2() { - - assertQuery(""" - SELECT DISTINCT emp - FROM Employee emp - WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) - """); - } - - @Test - void subselectNumericComparisonExample1() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE (SELECT AVG(o.price) FROM c.orders o) > 100 - """); - } - - @Test - void subselectNumericComparisonExample2() { - - assertQuery(""" - SELECT goodCustomer - FROM Customer goodCustomer - WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) / 2.0 FROM Customer c) - """); - } - - @Test - void indexExample() { - - assertQuery(""" - SELECT w.name - FROM Course c JOIN c.studentWaitlist w - WHERE c.name = 'Calculus' - AND INDEX(w) = 0 - """); - } - - /** - * @see #functionInvocationExampleWithCorrection() - */ - @Test - @Disabled(SPEC_FAULT + "FUNCTION calls needs a comparator") - void functionInvocationExample_SPEC_BUG() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) - """); - } - - @Test - void functionInvocationExampleWithCorrection() { - - assertQuery(""" - SELECT c - FROM Customer c - WHERE FUNCTION('hasGoodCredit', c.balance, c.creditLimit) = TRUE - """); - } - - @Test - void updateCaseExample1() { - - assertQuery(""" - UPDATE Employee e - SET e.salary = - CASE WHEN e.rating = 1 THEN e.salary * 1.1 - WHEN e.rating = 2 THEN e.salary * 1.05 - ELSE e.salary * 1.01 - END - """); - } - - @Test - void updateCaseExample2() { - - assertQuery(""" - UPDATE Employee e - SET e.salary = - CASE e.rating WHEN 1 THEN e.salary * 1.1 - WHEN 2 THEN e.salary * 1.05 - ELSE e.salary * 1.01 - END - """); - } - - @Test - void selectCaseExample1() { - - assertQuery(""" - SELECT e.name, - CASE TYPE(e) WHEN Exempt THEN 'Exempt' - WHEN Contractor THEN 'Contractor' - WHEN Intern THEN 'Intern' - ELSE 'NonExempt' - END - FROM Employee e - WHERE e.dept.name = 'Engineering' - """); - } - - @Test - void selectCaseExample2() { - - assertQuery(""" - SELECT e.name, - f.name, - CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' - WHEN f.annualMiles > 25000 THEN 'Gold ' - ELSE '' - END, - 'Frequent Flyer') - FROM Employee e JOIN e.frequentFlierPlan f - """); - } - - @Test - void theRest() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (Exempt, Contractor) - """); - } - - @Test - void theRest2() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN (:empType1, :empType2) - """); - } - - @Test - void theRest3() { - - assertQuery(""" - SELECT e - FROM Employee e - WHERE TYPE(e) IN :empTypes - """); - } - - @Test - void theRest4() { - - assertQuery(""" - SELECT TYPE(e) - FROM Employee e - WHERE TYPE(e) <> Exempt - """); - } - - @Test - void theRest5() { - - assertQuery(""" - SELECT c.status, AVG(c.filledOrderCount), COUNT(c) - FROM Customer c - GROUP BY c.status - HAVING c.status IN (1, 2) - """); - } - - @Test - void theRest6() { - - assertQuery(""" - SELECT c.country, COUNT(c) - FROM Customer c - GROUP BY c.country - HAVING COUNT(c) > 30 - """); - } - - @Test - void theRest7() { - - assertQuery(""" - SELECT c, COUNT(o) - FROM Customer c JOIN c.orders o - GROUP BY c - HAVING COUNT(o) >= 5 - """); - } - - @Test - void theRest8() { - - assertQuery(""" - SELECT c.id, c.status - FROM Customer c JOIN c.orders o - WHERE o.count > 100 - """); - } - - @Test - void theRest9() { - - assertQuery(""" - SELECT v.location.street, KEY(i).title, VALUE(i) - FROM VideoStore v JOIN v.videoInventory i - WHERE v.location.zipcode = '94301' AND VALUE(i) > 0 - """); - } - - @Test - void theRest10() { - - assertQuery(""" - SELECT o.lineItems FROM Order AS o - """); - } - - @Test - void theRest11() { - - assertQuery(""" - SELECT c, COUNT(l) AS itemCount - FROM Customer c JOIN c.Orders o JOIN o.lineItems l - WHERE c.address.state = 'CA' - GROUP BY c - ORDER BY itemCount - """); - } - - @Test - void theRest12() { - - assertQuery(""" - SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) - FROM Customer c JOIN c.orders o - WHERE o.count > 100 - """); - } - - @Test - void theRest13() { - - assertQuery(""" - SELECT e.address AS addr - FROM Employee e - """); - } - - @Test - void theRest14() { - - assertQuery(""" - SELECT AVG(o.quantity) FROM Order o - """); - } - - @Test - void theRest15() { - - assertQuery(""" - SELECT SUM(l.price) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - """); - } - - @Test - void theRest16() { - - assertQuery(""" - SELECT COUNT(o) FROM Order o - """); - } - - @Test - void theRest17() { - - assertQuery(""" - SELECT COUNT(l.price) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - """); - } - - @Test - void theRest18() { - - assertQuery(""" - SELECT COUNT(l) - FROM Order o JOIN o.lineItems l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' AND l.price IS NOT NULL - """); - } - - @Test - void theRest19() { - - assertQuery(""" - SELECT o - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - ORDER BY o.quantity DESC, o.totalcost - """); - } - - @Test - void theRest20() { - - assertQuery(""" - SELECT o.quantity, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - ORDER BY o.quantity, a.zipcode - """); - } - - @Test - void theRest21() { - - assertQuery(""" - SELECT o.quantity, o.cost * 1.08 AS taxedCost, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' AND a.county = 'Santa Clara' - ORDER BY o.quantity, taxedCost, a.zipcode - """); - } - - @Test - void theRest22() { - - assertQuery(""" - SELECT AVG(o.quantity) as q, a.zipcode - FROM Customer c JOIN c.orders o JOIN c.address a - WHERE a.state = 'CA' - GROUP BY a.zipcode - ORDER BY q DESC - """); - } - - @Test - void theRest23() { - - assertQuery(""" - SELECT p.product_name - FROM Order o JOIN o.lineItems l JOIN l.product p JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - ORDER BY p.price - """); - } - - /** - * This query is specifically dubbed illegal in the spec. It may actually be failing for a different reason. - */ - @Test - void theRest24() { - - assertThatExceptionOfType(BadJpqlGrammarException.class).isThrownBy(() -> { - assertQuery(""" - SELECT p.product_name - FROM Order o, IN(o.lineItems) l JOIN o.customer c - WHERE c.lastname = 'Smith' AND c.firstname = 'John' - ORDER BY o.quantity - """); - }); - } - - @Test - void theRest25() { - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - """); - } - - @Test - void theRest26() { - - assertQuery(""" - DELETE - FROM Customer c - WHERE c.status = 'inactive' - AND c.orders IS EMPTY - """); - } - - @Test - void theRest27() { - - assertQuery(""" - UPDATE Customer c - SET c.status = 'outstanding' - WHERE c.balance < 10000 - """); - } - - @Test - void theRest28() { - - assertQuery(""" - UPDATE Employee e - SET e.address.building = 22 - WHERE e.address.building = 14 - AND e.address.city = 'Santa Clara' - AND e.project = 'Jakarta EE' - """); - } - - @Test - void theRest29() { - - assertQuery(""" - SELECT o - FROM Order o - """); - } - - @Test - void theRest30() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.shippingAddress.state = 'CA' - """); - } - - @Test - void theRest31() { - - assertQuery(""" - SELECT DISTINCT o.shippingAddress.state - FROM Order o - """); - } - - @Test - void theRest32() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - """); - } - - @Test - void theRest33() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS NOT EMPTY - """); - } - - @Test - void theRest34() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.lineItems IS EMPTY - """); - } - - @Test - void theRest35() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.shipped = FALSE - """); - } - - @Test - void theRest36() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE - NOT (o.shippingAddress.state = o.billingAddress.state AND - o.shippingAddress.city = o.billingAddress.city AND - o.shippingAddress.street = o.billingAddress.street) - """); - } - - @Test - void theRest37() { - - assertQuery(""" - SELECT o - FROM Order o - WHERE o.shippingAddress <> o.billingAddress - """); - } - - @Test - void theRest38() { - - assertQuery(""" - SELECT DISTINCT o - FROM Order o JOIN o.lineItems l - WHERE l.product.name = ?1 - """); - } -} 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 3113627c8e..163a91dd95 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 @@ -80,7 +80,7 @@ void allowsShortJpaSyntax() { @MethodSource("detectsAliasWithUCorrectlySource") void detectsAliasWithUCorrectly(DeclaredQuery query, String alias) { - assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax.") + assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax") .doesNotStartWithIgnoringCase("from"); assertThat(getEnhancer(query).detectAlias()).isEqualTo(alias); From 75079c519785635a4a21120c6cc472bd352132a6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Jan 2025 14:35:49 +0100 Subject: [PATCH 31/94] Remove OpenJPA leftovers. Remove unused tests, simplify findAllById(Iterable) implementation. Closes #3741 --- spring-data-jpa/pom.xml | 1 - .../support/SimpleJpaRepository.java | 58 +++---------- .../EclipseLinkMetamodelIntegrationTests.java | 2 +- .../OpenJpaMetamodelIntegrationTests.java | 42 --------- ...raphRepositoryMethodsIntegrationTests.java | 24 ----- .../OpenJpaNamespaceUserRepositoryTests.java | 87 ------------------- ...enJpaParentRepositoryIntegrationTests.java | 27 ------ ...itoryWithCompositeKeyIntegrationTests.java | 33 ------- ...penJpaStoredProcedureIntegrationTests.java | 35 -------- .../OpenJpaUserRepositoryFinderTests.java | 33 ------- .../SimpleJpaParameterBindingTests.java | 4 +- .../jpa/repository/UserRepositoryTests.java | 7 -- .../query/OpenJpaJpa21UtilsTests.java | 26 ------ ...meterMetadataProviderIntegrationTests.java | 27 ------ .../OpenJpaQueryUtilsIntegrationTests.java | 26 ------ .../jpa/repository/sample/UserRepository.java | 7 -- .../support/OpenJpaJpaRepositoryTests.java | 33 ------- ...odelEntityInformationIntegrationTests.java | 62 ------------- .../support/OpenJpaProxyIdAccessorTests.java | 32 ------- .../test/resources/META-INF/persistence.xml | 18 ---- .../src/test/resources/openjpa.xml | 25 ------ 21 files changed, 14 insertions(+), 595 deletions(-) delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/OpenJpaMetamodelIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaEntityGraphRepositoryMethodsIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaNamespaceUserRepositoryTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaParentRepositoryIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaRepositoryWithCompositeKeyIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaStoredProcedureIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaUserRepositoryFinderTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaParameterMetadataProviderIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaQueryUtilsIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaJpaRepositoryTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaMetamodelEntityInformationIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaProxyIdAccessorTests.java delete mode 100644 spring-data-jpa/src/test/resources/openjpa.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 19ed8b44a9..a890bccf13 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -290,7 +290,6 @@ **/*UnitTests.java - **/OpenJpa* **/EclipseLink* **/MySql* **/Postgres* diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index dbbdea2d15..aad76c29ca 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -19,20 +19,17 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.LockModeType; -import jakarta.persistence.Parameter; import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; -import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Selection; -import java.io.Serial; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -262,7 +259,7 @@ public void deleteAllByIdInBatch(Iterable ids) { /* * Some JPA providers require {@code ids} to be a {@link Collection} so we must convert if it's not already. */ - Collection idCollection = toCollection(ids); + Collection idCollection = toCollection(ids); query.setParameter("ids", idCollection); applyQueryHints(query); @@ -426,10 +423,14 @@ public List findAllById(Iterable ids) { Collection idCollection = toCollection(ids); - ByIdsSpecification specification = new ByIdsSpecification<>(entityInformation); - TypedQuery query = getQuery(specification, Sort.unsorted()); + TypedQuery query = getQuery((root, q, criteriaBuilder) -> { - return query.setParameter(specification.parameter, idCollection).getResultList(); + Path path = root.get(entityInformation.getIdAttribute()); + return path.in(idCollection); + + }, Sort.unsorted()); + + return query.getResultList(); } @Override @@ -1083,37 +1084,6 @@ private static long executeCountQuery(TypedQuery query) { return total; } - /** - * Specification that gives access to the {@link Parameter} instance used to bind the ids for - * {@link SimpleJpaRepository#findAllById(Iterable)}. Workaround for OpenJPA not binding collections to in-clauses - * correctly when using by-name binding. - * - * @author Oliver Gierke - * @see OPENJPA-2018 - */ - @SuppressWarnings("rawtypes") - private static final class ByIdsSpecification implements Specification { - - private static final @Serial long serialVersionUID = 1L; - - private final JpaEntityInformation entityInformation; - - @Nullable ParameterExpression> parameter; - - ByIdsSpecification(JpaEntityInformation entityInformation) { - this.entityInformation = entityInformation; - } - - @Override - @SuppressWarnings("unchecked") - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { - - Path path = root.get(entityInformation.getIdAttribute()); - parameter = (ParameterExpression>) (ParameterExpression) cb.parameter(Collection.class); - return path.in(parameter); - } - } - /** * {@link Specification} that gives access to the {@link Predicate} instance representing the values contained in the * {@link Example}. @@ -1122,12 +1092,8 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild * @author Christoph Strobl * @since 1.10 */ - private static class ExampleSpecification implements Specification { - - private static final @Serial long serialVersionUID = 1L; - - private final Example example; - private final EscapeCharacter escapeCharacter; + private record ExampleSpecification(Example example, + EscapeCharacter escapeCharacter) implements Specification { /** * Creates new {@link ExampleSpecification}. @@ -1135,13 +1101,11 @@ private static class ExampleSpecification implements Specification { * @param example the example to base the specification of. Must not be {@literal null}. * @param escapeCharacter the escape character to use for like expressions. Must not be {@literal null}. */ - ExampleSpecification(Example example, EscapeCharacter escapeCharacter) { + private ExampleSpecification { Assert.notNull(example, EXAMPLE_MUST_NOT_BE_NULL); Assert.notNull(escapeCharacter, "EscapeCharacter must not be null"); - this.example = example; - this.escapeCharacter = escapeCharacter; } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java index e3cf795046..d62094bbf8 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java @@ -20,7 +20,7 @@ import org.springframework.test.context.ContextConfiguration; /** - * Metamodel tests using OpenJPA. + * Metamodel tests using Eclipselink. * * @author Oliver Gierke */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/OpenJpaMetamodelIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/OpenJpaMetamodelIntegrationTests.java deleted file mode 100644 index 16983f0f88..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/infrastructure/OpenJpaMetamodelIntegrationTests.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2013-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.infrastructure; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ContextConfiguration; - -/** - * Metamodel tests using OpenJPA. - * - * @author Oliver Gierke - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaMetamodelIntegrationTests extends MetamodelIntegrationTests { - - @Test - @Disabled - @Override - void canAccessParametersByIndexForNativeQueries() {} - - /** - * TODO: Remove once https://issues.apache.org/jira/browse/OPENJPA-2618 is fixed. - */ - @Test - @Disabled - @Override - void doesNotExposeAliasForTupleIfNoneDefined() {} -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaEntityGraphRepositoryMethodsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaEntityGraphRepositoryMethodsIntegrationTests.java deleted file mode 100644 index c42ae99579..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaEntityGraphRepositoryMethodsIntegrationTests.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2014-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; - -import org.springframework.test.context.ContextConfiguration; - -/** - * @author Oliver Gierke - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaEntityGraphRepositoryMethodsIntegrationTests extends EntityGraphRepositoryMethodsIntegrationTests {} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaNamespaceUserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaNamespaceUserRepositoryTests.java deleted file mode 100644 index a69fb9e35c..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaNamespaceUserRepositoryTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.ParameterExpression; -import jakarta.persistence.criteria.Root; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.data.jpa.domain.sample.User; -import org.springframework.data.jpa.repository.sample.UserRepository; -import org.springframework.test.context.ContextConfiguration; - -/** - * Testcase to run {@link UserRepository} integration tests on top of OpenJPA. - * - * @author Oliver Gierke - * @author Jens Schauder - * @author Krzysztof Krason - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaNamespaceUserRepositoryTests extends NamespaceUserRepositoryTests { - - @PersistenceContext EntityManager em; - - @Test - void checkQueryValidationWithOpenJpa() { - - assertThatThrownBy(() -> em.createQuery("something absurd")).isInstanceOf(RuntimeException.class); - assertThatThrownBy(() -> em.createNamedQuery("not available")).isInstanceOf(RuntimeException.class); - } - - /** - * Test case for https://issues.apache.org/jira/browse/OPENJPA-2018 - */ - @SuppressWarnings({ "rawtypes" }) - @Test - @Disabled - void queryUsingIn() { - - flushTestUsers(); - - CriteriaBuilder builder = em.getCriteriaBuilder(); - - CriteriaQuery criteriaQuery = builder.createQuery(User.class); - Root root = criteriaQuery.from(User.class); - ParameterExpression parameter = builder.parameter(Collection.class); - criteriaQuery.where(root. get("id").in(parameter)); - - TypedQuery query = em.createQuery(criteriaQuery); - query.setParameter(parameter, Arrays.asList(1, 2)); - - List resultList = query.getResultList(); - assertThat(resultList).hasSize(2); - } - - /** - * Temporarily ignored until openjpa works with hsqldb 2.x. - */ - @Override - void shouldFindUsersInNativeQueryWithPagination() {} -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaParentRepositoryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaParentRepositoryIntegrationTests.java deleted file mode 100644 index d94ed598c0..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaParentRepositoryIntegrationTests.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2013-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; - -import org.junit.jupiter.api.Disabled; -import org.springframework.test.context.ContextConfiguration; - -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaParentRepositoryIntegrationTests extends ParentRepositoryIntegrationTests { - - @Override - @Disabled - void testWithJoin() {} -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaRepositoryWithCompositeKeyIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaRepositoryWithCompositeKeyIntegrationTests.java deleted file mode 100644 index c6acc17b33..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaRepositoryWithCompositeKeyIntegrationTests.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016-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; - -import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ContextConfiguration; - -/** - * Testcase to run {@link RepositoryWithIdClassKeyTests} integration tests on top of OpenJPA. - * - * @author Mark Paluch - */ -@ContextConfiguration -class OpenJpaRepositoryWithCompositeKeyIntegrationTests extends RepositoryWithIdClassKeyTests { - - @ImportResource({ "classpath:infrastructure.xml", "classpath:openjpa.xml" }) - static class TestConfig extends Config { - - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaStoredProcedureIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaStoredProcedureIntegrationTests.java deleted file mode 100644 index 6984b99e27..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaStoredProcedureIntegrationTests.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2015-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; - -import org.junit.jupiter.api.Disabled; -import org.springframework.context.annotation.ImportResource; -import org.springframework.test.context.ContextConfiguration; - -/** - * Test case to run {@link StoredProcedureIntegrationTests} integration tests on top of OpenJpa. This is currently not - * supported since, the OpenJPA tests need to be executed with hsqldb1 which doesn't supported stored procedures. - * - * @author Thomas Darimont - * @author Oliver Gierke - */ -@Disabled -@ContextConfiguration(classes = { StoredProcedureIntegrationTests.Config.class }) -class OpenJpaStoredProcedureIntegrationTests extends StoredProcedureIntegrationTests { - - @ImportResource({ "classpath:infrastructure.xml", "classpath:openjpa.xml" }) - static class TestConfig extends Config {} -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaUserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaUserRepositoryFinderTests.java deleted file mode 100644 index d1e1b01f66..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OpenJpaUserRepositoryFinderTests.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2011-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; - -import org.junit.jupiter.api.Disabled; -import org.springframework.test.context.ContextConfiguration; - -/** - * Ignores some test cases using IN queries as long as we wait for fix for - * https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477. - * - * @author Oliver Gierke - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaUserRepositoryFinderTests extends UserRepositoryFinderTests { - - @Disabled - @Override - void findsByLastnameIgnoringCaseLike() {} -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/SimpleJpaParameterBindingTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/SimpleJpaParameterBindingTests.java index efe754ad7b..bad8461741 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/SimpleJpaParameterBindingTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/SimpleJpaParameterBindingTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -45,7 +46,6 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration({ "classpath:application-context.xml" // , "classpath:eclipselink.xml" -// , "classpath:openjpa.xml" }) @Transactional class SimpleJpaParameterBindingTests { 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 478cff1ab9..b51a78c058 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 @@ -804,9 +804,6 @@ void executesFinderWithFalseKeywordCorrectly() { assertThat(repository.findByActiveFalse()).containsOnly(firstUser); } - /** - * Ignored until the query declaration is supported by OpenJPA. - */ @Test void executesAnnotatedCollectionMethodCorrectly() { @@ -1619,11 +1616,7 @@ void deleteByShouldReturnEmptyListInCaseNoEntityHasBeenRemovedAndReturnTypeIsCol assertThat(repository.deleteByLastname("dorfuaeB")).isEmpty(); } - /** - * @see OPENJPA-2484 - */ @Test // DATAJPA-505 - @Disabled void findBinaryDataByIdJpaQl() throws Exception { byte[] data = "Woho!!".getBytes("UTF-8"); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java deleted file mode 100644 index 4c5cac42e1..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2017-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.test.context.ContextConfiguration; - -/** - * @author Christoph Strobl - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaJpa21UtilsTests extends Jpa21UtilsTests { - -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaParameterMetadataProviderIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaParameterMetadataProviderIntegrationTests.java deleted file mode 100644 index 7517a2a7e1..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaParameterMetadataProviderIntegrationTests.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2015-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.test.context.ContextConfiguration; - -/** - * OpenJpa-specific tests for {@link ParameterMetadataProvider}. - * - * @author Oliver Gierke - * @soundtrack Elephants Crossing - We are (Irrelephant) - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaParameterMetadataProviderIntegrationTests extends ParameterMetadataProviderIntegrationTests {} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaQueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaQueryUtilsIntegrationTests.java deleted file mode 100644 index fd8f1cb634..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaQueryUtilsIntegrationTests.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2013-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.test.context.ContextConfiguration; - -/** - * @author Oliver Gierke - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaQueryUtilsIntegrationTests extends QueryUtilsIntegrationTests { - -} 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 419e757bc0..9e0a4a8941 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 @@ -298,13 +298,6 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 List deleteByLastname(String lastname); - /** - * @see OPENJPA-2484 - */ - // DATAJPA-505 - // @Query(value = "select u.binaryData from User u where u.id = :id") - // byte[] findBinaryDataByIdJpaQl(@Param("id") Integer id); - /** * Explicitly mapped to a procedure with name "plus1inout" in database. */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaJpaRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaJpaRepositoryTests.java deleted file mode 100644 index dd8a85fce2..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaJpaRepositoryTests.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2013-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.support; - -import org.junit.jupiter.api.Disabled; -import org.springframework.test.context.ContextConfiguration; - -/** - * Integration tests to execute {@link JpaRepositoryTests} against OpenJpa. - * - * @author Oliver Gierke - */ -@ContextConfiguration("classpath:openjpa.xml") -class OpenJpaJpaRepositoryTests extends JpaRepositoryTests { - - @Override - @Disabled - void testCrudOperationsForCompoundKeyEntity() { - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaMetamodelEntityInformationIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaMetamodelEntityInformationIntegrationTests.java deleted file mode 100644 index 5c0a0600dd..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaMetamodelEntityInformationIntegrationTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2013-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.support; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * OpenJpa execution for {@link JpaMetamodelEntityInformationIntegrationTests}. - * - * @author Oliver Gierke - * @author Greg Turnquist - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration({ "classpath:infrastructure.xml", "classpath:openjpa.xml" }) -class OpenJpaMetamodelEntityInformationIntegrationTests extends JpaMetamodelEntityInformationIntegrationTests { - - @Override - String getMetadadataPersistenceUnitName() { - return "metadata_oj"; - } - - /** - * Re-activate test. - */ - @Test - void reactivatedDetectsIdTypeForMappedSuperclass() { - super.detectsIdTypeForMappedSuperclass(); - } - - /** - * Ignore as it fails with weird {@link NoClassDefFoundError}. - */ - @Override - @Disabled - void findsIdClassOnMappedSuperclass() {} - - /** - * Re-activate test for DATAJPA-820. - */ - @Test - @Override - void detectsVersionPropertyOnMappedSuperClass() { - super.detectsVersionPropertyOnMappedSuperClass(); - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaProxyIdAccessorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaProxyIdAccessorTests.java deleted file mode 100644 index 54372525c8..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/OpenJpaProxyIdAccessorTests.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2014-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.support; - -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportResource; -import org.springframework.data.jpa.provider.PersistenceProviderIntegrationTests; -import org.springframework.test.context.ContextConfiguration; - -/** - * @author Oliver Gierke - */ -@ContextConfiguration -class OpenJpaProxyIdAccessorTests extends PersistenceProviderIntegrationTests { - - @Configuration - @ImportResource("classpath:openjpa.xml") - static class Config {} -} diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence.xml b/spring-data-jpa/src/test/resources/META-INF/persistence.xml index 35a8715991..8fffadb357 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence.xml @@ -157,24 +157,6 @@ - - org.apache.openjpa.persistence.PersistenceProviderImpl - org.springframework.data.jpa.domain.sample.CustomAbstractPersistable - org.springframework.data.jpa.domain.sample.MailMessage - org.springframework.data.jpa.domain.sample.MailSender - org.springframework.data.jpa.domain.sample.MailUser - org.springframework.data.jpa.domain.sample.User - org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample - org.springframework.data.jpa.domain.sample.Dummy - true - - - - - - - - org.hibernate.jpa.HibernatePersistenceProvider org.springframework.data.jpa.domain.sample.CustomAbstractPersistable diff --git a/spring-data-jpa/src/test/resources/openjpa.xml b/spring-data-jpa/src/test/resources/openjpa.xml deleted file mode 100644 index eaca2061cd..0000000000 --- a/spring-data-jpa/src/test/resources/openjpa.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - none - - - - - From 017257b30b5f82ba232dfa1dcd340cc61f5fd80f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 13 Jan 2025 11:51:05 +0100 Subject: [PATCH 32/94] Upgrade to Hibernate 7.0 Beta3. Also, upgrade to Antlr 4.13.2 and extend XML metadata due to changes in how Hibernate now handles model metadata. Closes #3723 --- pom.xml | 4 ++-- .../src/test/resources/META-INF/persistence.xml | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 87f0b99aa7..4334372575 100755 --- a/pom.xml +++ b/pom.xml @@ -27,10 +27,10 @@ - 4.13.0 + 4.13.2 5.0.0-B05 5.0.0-SNAPSHOT - 7.0.0.Beta1 + 7.0.0.Beta3 7.0.0-SNAPSHOT 2.7.4

2.3.232

diff --git a/spring-data-jpa/src/test/resources/META-INF/persistence.xml b/spring-data-jpa/src/test/resources/META-INF/persistence.xml index 8fffadb357..a12c866d21 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence.xml @@ -4,6 +4,7 @@ xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_2.xsd" version="3.2"> + META-INF/orm.xml org.springframework.data.jpa.domain.AbstractPersistable org.springframework.data.jpa.domain.AbstractAuditable org.springframework.data.jpa.domain.sample.AbstractAnnotatedAuditable @@ -69,6 +70,7 @@ org.springframework.data.jpa.domain.sample.MailMessage org.springframework.data.jpa.domain.sample.MailSender org.springframework.data.jpa.domain.sample.MailUser + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.domain.sample.User org.springframework.data.jpa.domain.sample.Dummy true @@ -78,6 +80,7 @@ org.springframework.data.jpa.domain.sample.MailMessage org.springframework.data.jpa.domain.sample.MailSender org.springframework.data.jpa.domain.sample.MailUser + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.domain.sample.User org.springframework.data.jpa.repository.cdi.Person org.springframework.data.jpa.domain.sample.Dummy @@ -95,6 +98,7 @@ org.springframework.data.jpa.domain.sample.User + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.repository.query.QueryUtilsIntegrationTests$Merchant org.springframework.data.jpa.repository.query.QueryUtilsIntegrationTests$Address org.springframework.data.jpa.repository.query.QueryUtilsIntegrationTests$Employee @@ -122,6 +126,7 @@ org.springframework.data.jpa.domain.sample.MailSender org.springframework.data.jpa.domain.sample.MailUser org.springframework.data.jpa.domain.sample.User + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithNestedIdClass org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithIdClass @@ -140,6 +145,7 @@ org.springframework.data.jpa.domain.sample.MailSender org.springframework.data.jpa.domain.sample.MailUser org.springframework.data.jpa.domain.sample.User + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithNestedIdClass org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithIdClass @@ -164,6 +170,7 @@ org.springframework.data.jpa.domain.sample.MailSender org.springframework.data.jpa.domain.sample.MailUser org.springframework.data.jpa.domain.sample.User + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithNestedIdClass org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithIdClass @@ -181,6 +188,7 @@ org.springframework.data.jpa.domain.sample.MailMessage org.springframework.data.jpa.domain.sample.MailSender org.springframework.data.jpa.domain.sample.MailUser + org.springframework.data.jpa.domain.sample.Role org.springframework.data.jpa.domain.sample.User org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$Sample org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformationIntegrationTests$EntityWithNestedIdClass From 597f9296b01ce1ec543dfc7ebfaae30b0ce18e34 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 15 Jan 2025 10:41:54 +0100 Subject: [PATCH 33/94] Polishing. Fix since versions. See: #3521 Original Pull Request: #3578 --- .../data/jpa/repository/JpaSpecificationExecutor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index 65e352105f..ee715b24d2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -182,7 +182,7 @@ default boolean exists(PredicateSpecification spec) { * * @param spec the {@link UpdateSpecification} to use for the update query must not be {@literal null}. * @return the number of entities deleted. - * @since xxx + * @since 4.0 */ long update(UpdateSpecification spec); @@ -221,7 +221,7 @@ default long delete(PredicateSpecification spec) { * @param spec must not be null. * @param queryFunction the query function defining projection, sorting, and the result type * @return all entities matching the given Example. - * @since xxx + * @since 4.0 */ default R findBy(PredicateSpecification spec, Function, R> queryFunction) { From 398b2ee8f1e2f08899f58bb324925f9a1ce019ca Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 22 Jan 2025 14:15:39 +0100 Subject: [PATCH 34/94] =?UTF-8?q?Document=20that=20fluent=20`findBy(?= =?UTF-8?q?=E2=80=A6)`=20queries=20must=20return=20a=20result.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3294 --- .../data/jpa/repository/JpaSpecificationExecutor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index ee715b24d2..b09aec12f3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -217,6 +217,10 @@ default long delete(PredicateSpecification spec) { /** * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query * and its result type. + *

+ * The query object used with {@code queryFunction} is only valid inside the {@code findBy(…)} method call. This + * requires the query function to return a query result and not the {@link FluentQuery} object itself to ensure the + * query is executed inside the {@code findBy(…)} method. * * @param spec must not be null. * @param queryFunction the query function defining projection, sorting, and the result type From 3987faad998d43975cdcaa7a6f1220eae3316deb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 22 Jan 2025 14:17:33 +0100 Subject: [PATCH 35/94] Remove deprecated QuerydslJpaRepository. Closes #3683 --- .../support/QuerydslJpaRepository.java | 228 ------------ .../support/QuerydslJpaRepositoryTests.java | 341 ------------------ 2 files changed, 569 deletions(-) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java deleted file mode 100644 index 129d56f6e9..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepository.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * 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.support; - -import java.io.Serializable; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.LockModeType; - -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.querydsl.EntityPathResolver; -import org.springframework.data.querydsl.QSort; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; -import org.springframework.data.querydsl.SimpleEntityPathResolver; -import org.springframework.data.repository.query.FluentQuery; -import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -import com.querydsl.core.NonUniqueResultException; -import com.querydsl.core.types.EntityPath; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.PathBuilder; -import com.querydsl.jpa.JPQLQuery; -import com.querydsl.jpa.impl.AbstractJPAQuery; - -/** - * QueryDsl specific extension of {@link SimpleJpaRepository} which adds implementation for - * {@link QuerydslPredicateExecutor}. - * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Mark Paluch - * @author Jocelyn Ntakpe - * @author Christoph Strobl - * @author Jens Schauder - * @author Greg Turnquist - * @author Yanming Zhou - * @deprecated Instead of this class use {@link QuerydslJpaPredicateExecutor} - */ -@Deprecated -public class QuerydslJpaRepository extends SimpleJpaRepository - implements QuerydslPredicateExecutor { - - private final EntityPath path; - private final PathBuilder builder; - private final Querydsl querydsl; - private final EntityManager entityManager; - - /** - * Creates a new {@link QuerydslJpaRepository} from the given domain class and {@link EntityManager}. This will use - * the {@link SimpleEntityPathResolver} to translate the given domain class into an {@link EntityPath}. - * - * @param entityInformation must not be {@literal null}. - * @param entityManager must not be {@literal null}. - */ - public QuerydslJpaRepository(JpaEntityInformation entityInformation, EntityManager entityManager) { - this(entityInformation, entityManager, SimpleEntityPathResolver.INSTANCE); - } - - /** - * Creates a new {@link QuerydslJpaRepository} from the given domain class and {@link EntityManager} and uses the - * given {@link EntityPathResolver} to translate the domain class into an {@link EntityPath}. - * - * @param entityInformation must not be {@literal null}. - * @param entityManager must not be {@literal null}. - * @param resolver must not be {@literal null}. - */ - public QuerydslJpaRepository(JpaEntityInformation entityInformation, EntityManager entityManager, - EntityPathResolver resolver) { - - super(entityInformation, entityManager); - - this.path = resolver.createPath(entityInformation.getJavaType()); - this.builder = new PathBuilder<>(path.getType(), path.getMetadata()); - this.querydsl = new Querydsl(entityManager, builder); - this.entityManager = entityManager; - } - - @Override - public Optional findOne(Predicate predicate) { - - try { - return Optional.ofNullable(createQuery(predicate).select(path).limit(2).fetchOne()); - } catch (NonUniqueResultException ex) { - throw new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex); - } - } - - @Override - public List findAll(Predicate predicate) { - return createQuery(predicate).select(path).fetch(); - } - - @Override - public List findAll(Predicate predicate, OrderSpecifier... orders) { - return executeSorted(createQuery(predicate).select(path), orders); - } - - @Override - public List findAll(Predicate predicate, Sort sort) { - - Assert.notNull(sort, "Sort must not be null"); - - return executeSorted(createQuery(predicate).select(path), sort); - } - - @Override - public List findAll(OrderSpecifier... orders) { - - Assert.notNull(orders, "Order specifiers must not be null"); - - return executeSorted(createQuery(new Predicate[0]).select(path), orders); - } - - @Override - public Page findAll(Predicate predicate, Pageable pageable) { - - Assert.notNull(pageable, "Pageable must not be null"); - - final JPQLQuery countQuery = createCountQuery(predicate); - JPQLQuery query = querydsl.applyPagination(pageable, createQuery(predicate).select(path)); - - return PageableExecutionUtils.getPage(query.fetch(), pageable, countQuery::fetchCount); - } - - @Override - public R findBy(Predicate predicate, - Function, R> queryFunction) { - throw new UnsupportedOperationException( - "Fluent Query API support for Querydsl is only found in QuerydslJpaPredicateExecutor."); - } - - @Override - public long count(Predicate predicate) { - return createQuery(predicate).fetchCount(); - } - - @Override - public boolean exists(Predicate predicate) { - return createQuery(predicate).fetchCount() > 0; - } - - /** - * Creates a new {@link JPQLQuery} for the given {@link Predicate}. - * - * @param predicate - * @return the Querydsl {@link JPQLQuery}. - */ - protected JPQLQuery createQuery(Predicate... predicate) { - - AbstractJPAQuery query = doCreateQuery(getQueryHints().withFetchGraphs(entityManager), predicate); - - CrudMethodMetadata metadata = getRepositoryMethodMetadata(); - - if (metadata == null) { - return query; - } - - LockModeType type = metadata.getLockModeType(); - return type == null ? query : query.setLockMode(type); - } - - /** - * Creates a new {@link JPQLQuery} count query for the given {@link Predicate}. - * - * @param predicate, can be {@literal null}. - * @return the Querydsl count {@link JPQLQuery}. - */ - protected JPQLQuery createCountQuery(@Nullable Predicate... predicate) { - return doCreateQuery(getQueryHints(), predicate); - } - - private AbstractJPAQuery doCreateQuery(QueryHints hints, @Nullable Predicate... predicate) { - - AbstractJPAQuery query = querydsl.createQuery(path); - - if (predicate != null) { - query = query.where(predicate); - } - - hints.forEach(query::setHint); - - return query; - } - - /** - * Executes the given {@link JPQLQuery} after applying the given {@link OrderSpecifier}s. - * - * @param query must not be {@literal null}. - * @param orders must not be {@literal null}. - * @return - */ - private List executeSorted(JPQLQuery query, OrderSpecifier... orders) { - return executeSorted(query, new QSort(orders)); - } - - /** - * Executes the given {@link JPQLQuery} after applying the given {@link Sort}. - * - * @param query must not be {@literal null}. - * @param sort must not be {@literal null}. - * @return - */ - private List executeSorted(JPQLQuery query, Sort sort) { - return querydsl.applySorting(sort, query).fetch(); - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java deleted file mode 100644 index ece657841b..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaRepositoryTests.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * 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.support; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; - -import java.sql.Date; -import java.time.LocalDate; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -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.sample.Address; -import org.springframework.data.jpa.domain.sample.QUser; -import org.springframework.data.jpa.domain.sample.Role; -import org.springframework.data.jpa.domain.sample.User; -import org.springframework.data.querydsl.QPageRequest; -import org.springframework.data.querydsl.QSort; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Transactional; - -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.PathBuilder; -import com.querydsl.core.types.dsl.PathBuilderFactory; - -/** - * Integration test for {@link QuerydslJpaRepository}. - * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Mark Paluch - * @author Christoph Strobl - * @author Malte Mauelshagen - * @author Greg Turnquist - * @author Krzysztof Krason - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration({ "classpath:infrastructure.xml" }) -@Transactional -class QuerydslJpaRepositoryTests { - - @PersistenceContext EntityManager em; - - private QuerydslJpaRepository repository; - private QUser user = new QUser("user"); - private User dave; - private User carter; - private User oliver; - private Role adminRole; - - @BeforeEach - void setUp() { - - JpaEntityInformation information = new JpaMetamodelEntityInformation<>(User.class, em.getMetamodel(), - em.getEntityManagerFactory().getPersistenceUnitUtil()); - - repository = new QuerydslJpaRepository<>(information, em); - dave = repository.save(new User("Dave", "Matthews", "dave@matthews.com")); - carter = repository.save(new User("Carter", "Beauford", "carter@beauford.com")); - oliver = repository.save(new User("Oliver", "matthews", "oliver@matthews.com")); - adminRole = em.merge(new Role("admin")); - } - - @Test - void executesPredicatesCorrectly() { - - BooleanExpression isCalledDave = user.firstname.eq("Dave"); - BooleanExpression isBeauford = user.lastname.eq("Beauford"); - - List result = repository.findAll(isCalledDave.or(isBeauford)); - - assertThat(result).containsExactlyInAnyOrder(carter, dave); - } - - @Test - void executesStringBasedPredicatesCorrectly() { - - PathBuilder builder = new PathBuilderFactory().create(User.class); - - BooleanExpression isCalledDave = builder.getString("firstname").eq("Dave"); - BooleanExpression isBeauford = builder.getString("lastname").eq("Beauford"); - - List result = repository.findAll(isCalledDave.or(isBeauford)); - - assertThat(result).containsExactlyInAnyOrder(carter, dave); - } - - @Test // DATAJPA-243 - void considersSortingProvidedThroughPageable() { - - Predicate lastnameContainsE = user.lastname.contains("e"); - - Page result = repository.findAll(lastnameContainsE, PageRequest.of(0, 1, Direction.ASC, "lastname")); - - assertThat(result).containsExactly(carter); - - result = repository.findAll(lastnameContainsE, PageRequest.of(0, 2, Direction.DESC, "lastname")); - - assertThat(result).containsExactly(oliver, dave); - } - - @Test // DATAJPA-296 - void appliesIgnoreCaseOrdering() { - - Sort sort = Sort.by(new Order(Direction.DESC, "lastname").ignoreCase(), new Order(Direction.ASC, "firstname")); - - Page result = repository.findAll(user.lastname.contains("e"), PageRequest.of(0, 2, sort)); - - assertThat(result.getContent()).containsExactly(dave, oliver); - } - - @Test // DATAJPA-427 - void findBySpecificationWithSortByPluralAssociationPropertyInPageableShouldUseSortNullValuesLast() { - - oliver.getColleagues().add(dave); - dave.getColleagues().add(oliver); - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "colleagues.firstname"))); - - assertThat(page.getContent()).hasSize(3).contains(oliver, dave, carter); - } - - @Test // DATAJPA-427 - void findBySpecificationWithSortBySingularAssociationPropertyInPageableShouldUseSortNullValuesLast() { - - oliver.setManager(dave); - dave.setManager(carter); - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "manager.firstname"))); - - assertThat(page.getContent()).hasSize(3).contains(dave, oliver, carter); - } - - @Test // DATAJPA-427 - void findBySpecificationWithSortBySingularPropertyInPageableShouldUseSortNullValuesFirst() { - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "firstname"))); - - assertThat(page.getContent()).containsExactly(carter, dave, oliver); - } - - @Test // DATAJPA-427 - void findBySpecificationWithSortByOrderIgnoreCaseBySingularPropertyInPageableShouldUseSortNullValuesFirst() { - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - PageRequest.of(0, 10, Sort.by(new Order(Sort.Direction.ASC, "firstname").ignoreCase()))); - - assertThat(page.getContent()).containsExactly(carter, dave, oliver); - } - - @Test // DATAJPA-427 - void findBySpecificationWithSortByNestedEmbeddedPropertyInPageableShouldUseSortNullValuesFirst() { - - oliver.setAddress(new Address("Germany", "Saarbrücken", "HaveItYourWay", "123")); - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "address.streetName"))); - - assertThat(page.getContent()).containsExactly(dave, carter, oliver); - } - - @Test // DATAJPA-12 - void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequestAndQSort() { - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - QPageRequest.of(0, 10, new QSort(user.firstname.asc()))); - - assertThat(page.getContent()).containsExactly(carter, dave, oliver); - } - - @Test // DATAJPA-12 - void findBySpecificationWithSortByQueryDslOrderSpecifierWithQPageRequest() { - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), QPageRequest.of(0, 10, user.firstname.asc())); - - assertThat(page.getContent()).containsExactly(carter, dave, oliver); - } - - @Test // DATAJPA-12 - void findBySpecificationWithSortByQueryDslOrderSpecifierForAssociationShouldGenerateLeftJoinWithQPageRequest() { - - oliver.setManager(dave); - dave.setManager(carter); - - QUser user = QUser.user; - - Page page = repository.findAll(user.firstname.isNotNull(), - QPageRequest.of(0, 10, user.manager.firstname.asc())); - - assertThat(page.getContent()).containsExactly(carter, dave, oliver); - } - - @Test // DATAJPA-491 - void sortByNestedAssociationPropertyWithSpecificationAndSortInPageable() { - - oliver.setManager(dave); - dave.getRoles().add(adminRole); - - Page page = repository.findAll(PageRequest.of(0, 10, Sort.by(Direction.ASC, "manager.roles.name"))); - - assertThat(page.getContent()).hasSize(3); - assertThat(page.getContent().get(0)).isEqualTo(dave); - } - - @Test // DATAJPA-500, DATAJPA-635 - void sortByNestedEmbeddedAttribute() { - - carter.setAddress(new Address("U", "Z", "Y", "41")); - dave.setAddress(new Address("U", "A", "Y", "41")); - oliver.setAddress(new Address("G", "D", "X", "42")); - - List users = repository.findAll(QUser.user.address.streetName.asc()); - - assertThat(users).hasSize(3).contains(dave, oliver, carter); - } - - @Test // DATAJPA-566, DATAJPA-635 - void shouldSupportSortByOperatorWithDateExpressions() { - - carter.setDateOfBirth(Date.valueOf(LocalDate.of(2000, 2, 1))); - dave.setDateOfBirth(Date.valueOf(LocalDate.of(2000, 1, 1))); - oliver.setDateOfBirth(Date.valueOf(LocalDate.of(2003, 5, 1))); - - List users = repository.findAll(QUser.user.dateOfBirth.yearMonth().asc()); - - assertThat(users).containsExactly(dave, carter, oliver); - } - - @Test // DATAJPA-665 - void shouldSupportExistsWithPredicate() { - - assertThat(repository.exists(user.firstname.eq("Dave"))).isTrue(); - assertThat(repository.exists(user.firstname.eq("Unknown"))).isFalse(); - assertThat(repository.exists((Predicate) null)).isTrue(); - } - - @Test // DATAJPA-679 - void shouldSupportFindAllWithPredicateAndSort() { - - List users = repository.findAll(user.dateOfBirth.isNull(), Sort.by(Direction.ASC, "firstname")); - - assertThat(users).contains(carter, dave, oliver); - } - - @Test // DATAJPA-585 - void worksWithUnpagedPageable() { - assertThat(repository.findAll(user.dateOfBirth.isNull(), Pageable.unpaged()).getContent()).hasSize(3); - } - - @Test // DATAJPA-912 - void pageableQueryReportsTotalFromResult() { - - Page firstPage = repository.findAll(user.dateOfBirth.isNull(), PageRequest.of(0, 10)); - assertThat(firstPage.getContent()).hasSize(3); - assertThat(firstPage.getTotalElements()).isEqualTo(3L); - - Page secondPage = repository.findAll(user.dateOfBirth.isNull(), PageRequest.of(1, 2)); - assertThat(secondPage.getContent()).hasSize(1); - assertThat(secondPage.getTotalElements()).isEqualTo(3L); - } - - @Test // DATAJPA-912 - void pageableQueryReportsTotalFromCount() { - - Page firstPage = repository.findAll(user.dateOfBirth.isNull(), PageRequest.of(0, 3)); - assertThat(firstPage.getContent()).hasSize(3); - assertThat(firstPage.getTotalElements()).isEqualTo(3L); - - Page secondPage = repository.findAll(user.dateOfBirth.isNull(), PageRequest.of(10, 10)); - assertThat(secondPage.getContent()).isEmpty(); - assertThat(secondPage.getTotalElements()).isEqualTo(3L); - } - - @Test // DATAJPA-1115 - void findOneWithPredicateReturnsResultCorrectly() { - assertThat(repository.findOne(user.eq(dave))).contains(dave); - } - - @Test // DATAJPA-1115 - void findOneWithPredicateReturnsOptionalEmptyWhenNoDataFound() { - assertThat(repository.findOne(user.firstname.eq("batman"))).isNotPresent(); - } - - @Test // DATAJPA-1115 - void findOneWithPredicateThrowsExceptionForNonUniqueResults() { - - assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) - .isThrownBy(() -> repository.findOne(user.emailAddress.contains("com"))); - } - - @Test // GH-2294 - void findByFluentQuery() { - - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> repository.findBy(user.firstname.eq("Dave"), q -> q.sortBy(Sort.by("firstname")).all())); - } -} From 64459416f3b2b720374c9e26f7e5eb91627c2fc2 Mon Sep 17 00:00:00 2001 From: Joshua Chen <27291761@qq.com> Date: Sat, 28 Dec 2024 15:02:46 +0800 Subject: [PATCH 36/94] =?UTF-8?q?Support=20custom=20countSpec=20in=20Simpl?= =?UTF-8?q?eJpaRepository.findAll(=E2=80=A6).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3727 --- .../data/jpa/repository/JpaSpecificationExecutor.java | 1 + .../springframework/data/jpa/repository/UserRepositoryTests.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index b09aec12f3..1d9c5cb8b7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -37,6 +37,7 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.UpdateSpecification; import org.springframework.data.repository.query.FluentQuery; +import org.springframework.lang.Nullable; /** * Interface to allow execution of {@link Specification}s based on the JPA criteria API. 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 b51a78c058..9bc8bded0a 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 @@ -47,7 +47,6 @@ import org.assertj.core.api.SoftAssertions; import org.hibernate.LazyInitializationException; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; From 56382cf8dc8adf9685f578f532bc68a566508bb8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 23 Jan 2025 11:26:38 +0100 Subject: [PATCH 37/94] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SpecificationFluentQuery to include specification-related overloads. Also, add slice(…) terminal method to obtain a slice only without running a count query. See #3727 --- .../repository/JpaSpecificationExecutor.java | 20 +++++++++- .../FetchableFluentQueryBySpecification.java | 20 ++++++++++ .../jpa/repository/UserRepositoryTests.java | 38 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index 1d9c5cb8b7..3712be9561 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -19,6 +19,8 @@ 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; @@ -31,6 +33,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; 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; @@ -229,7 +232,7 @@ default long delete(PredicateSpecification spec) { * @since 4.0 */ default R findBy(PredicateSpecification spec, - Function, R> queryFunction) { + Function, R> queryFunction) { return findBy(Specification.where(spec), queryFunction); } @@ -275,6 +278,21 @@ default SpecificationFluentQuery project(String... properties) { @Override SpecificationFluentQuery project(Collection properties); + /** + * Get a page of matching elements for {@link Pageable} and provide a custom {@link Specification count + * specification}. + * + * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be + * {@literal null}. The given {@link Pageable} will override any previously specified {@link Sort sort} if + * the {@link Sort} object is not {@link Sort#isUnsorted()}. Any potentially specified {@link #limit(int)} + * will be overridden by {@link Pageable#getPageSize()}. + * @param countSpec specification used to count results. + * @return + */ + default Page page(Pageable pageable, PredicateSpecification countSpec) { + return page(pageable, Specification.where(countSpec)); + } + /** * Get a page of matching elements for {@link Pageable} and provide a custom {@link Specification count * specification}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java index 68b4eb2582..a1c91b9148 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java @@ -226,6 +226,26 @@ private TypedQuery createSortedAndProjectedQuery(Sort sort) { private Slice readSlice(Pageable pageable) { + TypedQuery pagedQuery = createSortedAndProjectedQuery(); + + if (pageable.isPaged()) { + pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable)); + pagedQuery.setMaxResults(pageable.getPageSize() + 1); + } + + List resultList = pagedQuery.getResultList(); + boolean hasNext = resultList.size() > pageable.getPageSize(); + if (hasNext) { + resultList = resultList.subList(0, pageable.getPageSize()); + } + + List slice = convert(resultList); + + return new SliceImpl<>(slice, pageable, hasNext); + } + + private Slice readSlice(Pageable pageable, @Nullable Specification countSpec) { + TypedQuery pagedQuery = createSortedAndProjectedQuery(pageable.getSort()); if (pageable.isPaged()) { 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 9bc8bded0a..94f69747f8 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 @@ -2801,6 +2801,44 @@ void findByFluentSpecificationPageCustomCountSpec() { assertThat(page0.getTotalElements()).isEqualTo(3L); } + @Test // GH-2274 + void findByFluentSpecificationSlice() { + + flushTestUsers(); + + Slice slice = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).slice(PageRequest.of(0, 2))); + + assertThat(slice).isNotInstanceOf(Page.class); + assertThat(slice.getContent()).containsExactly(thirdUser, firstUser); + assertThat(slice.hasNext()).isTrue(); + + slice = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).slice(PageRequest.of(0, 3))); + + assertThat(slice).isNotInstanceOf(Page.class); + assertThat(slice).hasSize(3); + assertThat(slice.hasNext()).isFalse(); + } + + @Test // GH-3727 + void findByFluentSpecificationPageCustomCountSpec() { + + flushTestUsers(); + + Page page0 = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2), (root, query, criteriaBuilder) -> null)); + + assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); + assertThat(page0.getTotalElements()).isEqualTo(4L); + + page0 = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2))); + + assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); + assertThat(page0.getTotalElements()).isEqualTo(3L); + } + @Test // GH-2274, GH-3716 void findByFluentSpecificationWithInterfaceBasedProjection() { From fbbb2002cebe68b3e9ea368a84505941812ca6ed Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:47:59 +0100 Subject: [PATCH 38/94] Prepare 4.0 M1 (2025.1.0). See #3680 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 4334372575..35b6eb6a3f 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 @@ -38,7 +38,7 @@ 5.0 9.1.0 42.7.4 - 4.0.0-SNAPSHOT + 4.0.0-M1 0.10.3 org.hibernate @@ -248,20 +248,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 10569fab97ed4443fac89b4cd4fc1e44c192188f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:48:55 +0100 Subject: [PATCH 39/94] Release version 4.0 M1 (2025.1.0). See #3680 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 35b6eb6a3f..1707adf41f 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..261619b3dd 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-SNAPSHOT + 4.0.0-M1 org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..11c9deffdd 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index a890bccf13..5e00201219 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-SNAPSHOT + 4.0.0-M1 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 ../pom.xml From e5f5b5f42618aa64039348a0b0814704fee596d1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:09 +0100 Subject: [PATCH 40/94] Prepare next development iteration. See #3680 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 1707adf41f..35b6eb6a3f 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 261619b3dd..0bdf2c8e7e 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-M1 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 11c9deffdd..af5244a230 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 5e00201219..a890bccf13 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-M1 + 4.0.0-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT ../pom.xml From eef3df1077ddedece6ee87cd58c4fef8fc334a76 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:11 +0100 Subject: [PATCH 41/94] After release cleanups. See #3680 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 35b6eb6a3f..4334372575 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT @@ -38,7 +38,7 @@ 5.0 9.1.0 42.7.4 - 4.0.0-M1 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -248,8 +248,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From 7b8b35fe02c5bf0284b36fff9604c39f5d74b762 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Mon, 27 Jan 2025 15:54:33 +0100 Subject: [PATCH 42/94] Add visitor to build an order expression from a JPQL order specification. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now parse JpaSort.unsafe(…) expressions using our Query Parser and translate the parsed tree into a CriteriaQuery Expression except for CAST, TREAT and subqueries. Closes #3172 Original pull request: #3187 --- .../query/HqlOrderExpressionVisitor.java | 740 ++++++++++++++++++ .../HqlOrderExpressionVisitorUnitTests.java | 241 ++++++ 2 files changed, 981 insertions(+) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java new file mode 100644 index 0000000000..ade370c0f0 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java @@ -0,0 +1,740 @@ +/* + * 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 jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.LocalDateTimeField; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.TemporalField; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.temporal.Temporal; +import java.util.Collection; +import java.util.HexFormat; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.util.Assert; + +/** + * Parses the content of {@link JpaSort#unsafe(String...)} as an HQL {@literal sortExpression} and renders that into a + * JPA Criteria {@link Expression}. + * + * @author Greg Turnquist + * @author Mark Paluch + * @since 4.0 + */ +@SuppressWarnings("ConstantValue") +class HqlOrderExpressionVisitor extends HqlBaseVisitor> { + + private final CriteriaBuilder cb; + private final Path from; + private static String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe"; + + HqlOrderExpressionVisitor(CriteriaBuilder cb, Path from) { + this.cb = cb; + this.from = from; + } + + /** + * Extract the {@link org.springframework.data.jpa.domain.JpaSort.JpaOrder}'s property and parse it as an HQL + * {@literal sortExpression}. + * + * @param jpaOrder + * @return criteriaExpression + */ + Expression createCriteriaExpression(Sort.Order jpaOrder) { + + String orderByProperty = jpaOrder.getProperty(); + HqlLexer lexer = new HqlLexer(CharStreams.fromString(orderByProperty)); + HqlParser parser = new HqlParser(new CommonTokenStream(lexer)); + + JpaQueryEnhancer.configureParser(orderByProperty, "ORDER BY expression", lexer, parser); + + HqlParser.SortExpressionContext ctx = parser.sortExpression(); + + if (ctx == null) { + throw new IllegalArgumentException("No sort expression provided"); + } + + return visitRequired(ctx); + } + + @Override + public Expression visitSortExpression(HqlParser.SortExpressionContext ctx) { + + if (ctx.identifier() != null) { + HqlParser.IdentifierContext identifier = ctx.identifier(); + + return from.get(getString(identifier)); + } else if (ctx.INTEGER_LITERAL() != null) { + return cb.literal(Integer.valueOf(ctx.INTEGER_LITERAL().getText())); + } else if (ctx.expression() != null) { + return visitRequired(ctx.expression()); + } else { + return null; + } + } + + @Override + @SuppressWarnings("rawtypes") + public Expression visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) { + + Expression left = visitRequired(ctx.expression(0)); + Expression right = visitRequired(ctx.expression(1)); + String op = ctx.op.getText(); + + if (op.equals("=")) { + return cb.equal(left, right); + } else if (op.equals(">")) { + return cb.greaterThan(left, right); + } else if (op.equals(">=")) { + return cb.greaterThanOrEqualTo(left, right); + } else if (op.equals("<")) { + return cb.lessThan(left, right); + } else if (op.equals("<=")) { + return cb.lessThanOrEqualTo(left, right); + } else if (op.equals("<>") || op.equals("!=") || op.equals("^=")) { + return cb.notEqual(left, right); + } else { + throw new UnsupportedOperationException("Unsupported comparison operator: " + op); + } + } + + @Override + @SuppressWarnings("rawtypes") + public Expression visitBetweenExpression(HqlParser.BetweenExpressionContext ctx) { + + Expression condition = visitRequired(ctx.expression(0)); + Expression lower = visitRequired(ctx.expression(1)); + Expression upper = visitRequired(ctx.expression(2)); + + if (ctx.NOT() == null) { + return cb.between(condition, lower, upper); + } else { + return cb.between(condition, lower, upper).not(); + } + } + + @SuppressWarnings("unchecked") + @Override + public Expression visitIsBooleanPredicate(HqlParser.IsBooleanPredicateContext ctx) { + + Expression condition = visitRequired(ctx.expression()); + + if (ctx.NULL() != null) { + if (ctx.NOT() == null) { + return cb.isNull(condition); + } else { + return cb.isNotNull(condition); + } + } + + if (ctx.EMPTY() != null) { + if (ctx.NOT() == null) { + return cb.isEmpty((Expression>) condition); + } else { + return cb.isNotEmpty((Expression>) condition); + } + } + + if (ctx.TRUE() != null) { + if (ctx.NOT() == null) { + return cb.isTrue((Expression) condition); + } else { + return cb.isFalse((Expression) condition); + } + } + + if (ctx.FALSE() != null) { + if (ctx.NOT() == null) { + return cb.isFalse((Expression) condition); + } else { + return cb.isTrue((Expression) condition); + } + } + + return null; + } + + @Override + public Expression visitStringPatternMatching(HqlParser.StringPatternMatchingContext ctx) { + Expression condition = visitRequired(ctx.expression(0)); + Expression match = visitRequired(ctx.expression(1)); + Expression escape = ctx.ESCAPE() != null ? charLiteralOf(ctx.ESCAPE()) : null; + + if (ctx.LIKE() != null) { + if (ctx.NOT() == null) { + return escape == null // + ? cb.like(condition, match) // + : cb.like(condition, match, escape); + } else { + return escape == null // + ? cb.notLike(condition, match) // + : cb.notLike(condition, match, escape); + } + } else { + HibernateCriteriaBuilder hcb = (HibernateCriteriaBuilder) cb; + if (ctx.NOT() == null) { + return escape == null // + ? hcb.ilike(condition, match) // + : hcb.ilike(condition, match, escape); + } else { + return escape == null // + ? hcb.notIlike(condition, match) // + : hcb.notIlike(condition, match, escape); + } + } + } + + @Override + public Expression visitInExpression(HqlParser.InExpressionContext ctx) { + + if (ctx.inList().simplePath() != null) { + throw new UnsupportedOperationException( + String.format(UNSUPPORTED_TEMPLATE, "IN clause with ELEMENTS or INDICES argument")); + } else if (ctx.inList().subquery() != null) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "IN clause with a subquery")); + } else if (ctx.inList().parameter() != null) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "IN clause with a parameter")); + } + + CriteriaBuilder.In in = cb.in(visit(ctx.expression())); + + ctx.inList().expressionOrPredicate() + .forEach(expressionOrPredicateContext -> in.value(visit(expressionOrPredicateContext))); + + if (ctx.NOT() == null) { + return in; + } + return in.not(); + + } + + @Override + public Expression visitGenericFunction(HqlParser.GenericFunctionContext ctx) { + + String functionName = ctx.genericFunctionName().getText(); + + if (ctx.genericFunctionArguments() == null) { + return cb.function(functionName, Object.class); + } + + Expression[] arguments = ctx.genericFunctionArguments().expressionOrPredicate().stream() // + .map(expressionOrPredicateContext -> visitRequired(expressionOrPredicateContext)) // + .toArray(Expression[]::new); + return cb.function(functionName, Object.class, arguments); + + } + + @Override + public Expression visitCastFunction(HqlParser.CastFunctionContext ctx) { + throw new UnsupportedOperationException("Sorting using CAST ist not supported"); + } + + @Override + public Expression visitTreatedNavigablePath(HqlParser.TreatedNavigablePathContext ctx) { + throw new UnsupportedOperationException("Sorting using TREAT ist not supported"); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Expression visitExtractFunction(HqlParser.ExtractFunctionContext ctx) { + + Expression expr = visitRequired(ctx.expression()); + TemporalField temporalField = ctx.extractField() != null ? getTemporalField(ctx.extractField()) + : getTemporalField(ctx.datetimeField()); + + return cb.extract(temporalField, expr); + } + + private TemporalField getTemporalField(HqlParser.DatetimeFieldContext ctx) { + + if (ctx.YEAR() != null) { + return LocalDateTimeField.YEAR; + } + + if (ctx.MONTH() != null) { + return LocalDateTimeField.MONTH; + } + + if (ctx.QUARTER() != null) { + return LocalDateTimeField.QUARTER; + } + + if (ctx.WEEK() != null) { + return LocalDateTimeField.WEEK; + } + + if (ctx.DAY() != null) { + return LocalDateTimeField.DAY; + } + + if (ctx.HOUR() != null) { + return LocalDateTimeField.HOUR; + } + + if (ctx.MINUTE() != null) { + return LocalDateTimeField.MINUTE; + } + + if (ctx.SECOND() != null) { + return LocalDateTimeField.SECOND; + } + + throw new UnsupportedOperationException("Unsupported extract field: " + ctx.getText()); + } + + private TemporalField getTemporalField(HqlParser.ExtractFieldContext ctx) { + + if (ctx.dateOrTimeField() != null) { + + if (ctx.dateOrTimeField().DATE() != null) { + return LocalDateTimeField.DATE; + } + + if (ctx.dateOrTimeField().TIME() != null) { + return LocalDateTimeField.DATE; + } + } else if (ctx.datetimeField() != null) { + + if (ctx.datetimeField().YEAR() != null) { + return LocalDateTimeField.YEAR; + } + + if (ctx.datetimeField().MONTH() != null) { + return LocalDateTimeField.MONTH; + } + + if (ctx.datetimeField().QUARTER() != null) { + return LocalDateTimeField.QUARTER; + } + + if (ctx.datetimeField().WEEK() != null) { + return LocalDateTimeField.WEEK; + } + + if (ctx.datetimeField().DAY() != null) { + return LocalDateTimeField.DAY; + } + + if (ctx.datetimeField().HOUR() != null) { + return LocalDateTimeField.HOUR; + } + + if (ctx.datetimeField().MINUTE() != null) { + return LocalDateTimeField.MINUTE; + } + + if (ctx.datetimeField().SECOND() != null) { + return LocalDateTimeField.SECOND; + } + } else if (ctx.weekField() != null) { + + if (ctx.weekField().WEEK() != null) { + return LocalDateTimeField.WEEK; + } + + if (ctx.weekField().MONTH() != null) { + return LocalDateTimeField.MONTH; + } + + if (ctx.weekField().YEAR() != null) { + return LocalDateTimeField.YEAR; + } + } + + throw new UnsupportedOperationException("Unsupported extract field: " + ctx.getText()); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Expression visitTruncFunction(HqlParser.TruncFunctionContext ctx) { + + Expression expr = visitRequired(ctx.expression().get(0)); + + if (ctx.datetimeField() != null) { + TemporalField temporalField = getTemporalField(ctx.datetimeField()); + + return cb.function("trunc", Object.class, expr, cb.literal(temporalField)); + } else if (ctx.expression().size() > 1) { + + return cb.function("trunc", Object.class, expr, visitRequired(ctx.expression().get(1))); + } + + return cb.function("trunc", Object.class, expr); + } + + @Override + public Expression visitTrimFunction(HqlParser.TrimFunctionContext ctx) { + + CriteriaBuilder.Trimspec trimSpec = null; + + HqlParser.TrimSpecificationContext tsc = ctx.trimSpecification(); + + if (tsc.LEADING() != null) { + trimSpec = CriteriaBuilder.Trimspec.LEADING; + } else if (tsc.TRAILING() != null) { + trimSpec = CriteriaBuilder.Trimspec.TRAILING; + } else if (tsc.BOTH() != null) { + trimSpec = CriteriaBuilder.Trimspec.BOTH; + } + + Expression stringLiteral = charLiteralOf(ctx.trimCharacter().STRING_LITERAL()); + Expression expression = visitRequired(ctx.expression()); + + if (trimSpec != null) { + return stringLiteral != null // + ? cb.trim(trimSpec, stringLiteral, expression) // + : cb.trim(trimSpec, expression); + } else { + return stringLiteral != null // + ? cb.trim(stringLiteral, expression) // + : cb.trim(expression); + } + } + + @Override + public Expression visitSubstringFunction(HqlParser.SubstringFunctionContext ctx) { + + Expression start = visitRequired(ctx.substringFunctionStartArgument().expression()); + + if (ctx.substringFunctionLengthArgument() != null) { + Expression length = visitRequired(ctx.substringFunctionLengthArgument().expression()); + return cb.substring(visitRequired(ctx.expression()), start, length); + } + + return cb.substring(visitRequired(ctx.expression()), start); + } + + @Override + public Expression visitLiteral(HqlParser.LiteralContext ctx) { + + if (ctx.booleanLiteral() != null) { + return visitRequired(ctx.booleanLiteral()); + } else if (ctx.JAVA_STRING_LITERAL() != null) { + return literalOf(ctx.JAVA_STRING_LITERAL()); + } else if (ctx.STRING_LITERAL() != null) { + return literalOf(ctx.STRING_LITERAL()); + } else if (ctx.numericLiteral() != null) { + return visitRequired(ctx.numericLiteral()); + } else if (ctx.temporalLiteral() != null) { + return visitRequired(ctx.temporalLiteral()); + } else if (ctx.binaryLiteral() != null) { + return visitRequired(ctx.binaryLiteral()); + } else { + return null; + } + } + + private Expression literalOf(TerminalNode node) { + + String text = node.getText(); + return cb.literal(unquoteStringLiteral(text)); + } + + private Expression charLiteralOf(TerminalNode node) { + + String text = node.getText(); + return cb.literal(text.charAt(0)); + } + + @Override + public Expression visitBooleanLiteral(HqlParser.BooleanLiteralContext ctx) { + if (ctx.TRUE() != null) { + return cb.literal(true); + } else { + return cb.literal(false); + } + } + + @Override + public Expression visitNumericLiteral(HqlParser.NumericLiteralContext ctx) { + return cb.literal(getLiteralValue(ctx)); + } + + private Number getLiteralValue(HqlParser.NumericLiteralContext ctx) { + + if (ctx.INTEGER_LITERAL() != null) { + return Integer.valueOf(getDecimals(ctx.INTEGER_LITERAL())); + } else if (ctx.LONG_LITERAL() != null) { + return Long.valueOf(getDecimals(ctx.LONG_LITERAL())); + } else if (ctx.FLOAT_LITERAL() != null) { + return Float.valueOf(getDecimals(ctx.FLOAT_LITERAL())); + } else if (ctx.DOUBLE_LITERAL() != null) { + return Double.valueOf(getDecimals(ctx.DOUBLE_LITERAL())); + } else if (ctx.BIG_INTEGER_LITERAL() != null) { + return new BigInteger(getDecimals(ctx.BIG_INTEGER_LITERAL())); + } else if (ctx.BIG_DECIMAL_LITERAL() != null) { + return new BigDecimal(getDecimals(ctx.BIG_DECIMAL_LITERAL())); + } else if (ctx.HEX_LITERAL() != null) { + return HexFormat.fromHexDigits(ctx.HEX_LITERAL().toString().substring(2)); + } + + throw new UnsupportedOperationException("Unsupported literal: " + ctx.getText()); + } + + static String getDecimals(TerminalNode input) { + + String text = input.getText(); + StringBuilder result = new StringBuilder(text.length()); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (Character.isDigit(c) || c == '-' || c == '+' || c == '.') { + result.append(c); + } + } + + return result.toString(); + } + + @Override + public Expression visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) { + + if (ctx.offsetDateTimeLiteral() != null) { + return visit(ctx.offsetDateTimeLiteral()); + } else if (ctx.localDateTimeLiteral() != null) { + return visit(ctx.localDateTimeLiteral()); + } else if (ctx.zonedDateTimeLiteral() != null) { + return visit(ctx.zonedDateTimeLiteral()); + } + + return null; + } + + @Override + public Expression visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) { + return visit(ctx.expression()); + } + + @Override + public Expression visitTupleExpression(HqlParser.TupleExpressionContext ctx) { + return (Expression) cb + .tuple(ctx.expressionOrPredicate().stream().map(this::visitRequired).toArray(Expression[]::new)); + } + + @Override + public Expression visitSubqueryExpression(HqlParser.SubqueryExpressionContext ctx) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a subquery argument")); + } + + @Override + public Expression visitMultiplicationExpression(HqlParser.MultiplicationExpressionContext ctx) { + + Expression left = visitRequired(ctx.expression(0)); + Expression right = visitRequired(ctx.expression(1)); + + if (ctx.op.getText().equals("*")) { + return cb.prod(left, right); + } else { + return cb.quot(left, right); + } + } + + @Override + public Expression visitAdditionExpression(HqlParser.AdditionExpressionContext ctx) { + + Expression left = visitRequired(ctx.expression(0)); + Expression right = visitRequired(ctx.expression(1)); + + if (ctx.op.getText().equals("+")) { + return cb.sum(left, right); + } else { + return cb.diff(left, right); + } + } + + @Override + public Expression visitHqlConcatenationExpression(HqlParser.HqlConcatenationExpressionContext ctx) { + + Expression left = visitRequired(ctx.expression(0)); + Expression right = visitRequired(ctx.expression(1)); + + return cb.concat(left, right); + } + + @Override + public Expression visitSimplePath(HqlParser.SimplePathContext ctx) { + return QueryUtils.toExpressionRecursively((From) from, PropertyPath.from(ctx.getText(), from.getJavaType())); + } + + String getString(HqlParser.IdentifierContext context) { + + HqlParser.NakedIdentifierContext ni = context.nakedIdentifier(); + + String text = context.getText(); + if (ni != null) { + if (ni.QUOTED_IDENTIFIER() != null) { + text = unquoteIdentifier(ni.getText()); + } + } + return text; + } + + @Override + public Expression visitCaseList(HqlParser.CaseListContext ctx) { + if (ctx.simpleCaseExpression() != null) { + return visit(ctx.simpleCaseExpression()); + } else { + return visit(ctx.searchedCaseExpression()); + } + } + + @Override + public Expression visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) { + CriteriaBuilder.SimpleCase simpleCase = cb.selectCase(visit(ctx.expressionOrPredicate(0))); + ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> { + simpleCase.when( // + visitRequired(caseWhenExpressionClauseContext.expression()), // + visitRequired(caseWhenExpressionClauseContext.expressionOrPredicate())); + }); + if (ctx.expressionOrPredicate().size() == 2) { + simpleCase.otherwise(visitRequired(ctx.expressionOrPredicate(1))); + } + return simpleCase; + } + + @Override + public Expression visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) { + CriteriaBuilder.Case searchedCase = cb.selectCase(); + ctx.caseWhenPredicateClause().forEach(caseWhenPredicateClauseContext -> { + searchedCase.when( // + visitRequired(caseWhenPredicateClauseContext.predicate()), // + visit(caseWhenPredicateClauseContext.expressionOrPredicate())); + }); + if (ctx.expressionOrPredicate() != null) { + searchedCase.otherwise(visit(ctx.expressionOrPredicate())); + } + return searchedCase; + } + + @Override + public Expression visitParameter(HqlParser.ParameterContext ctx) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a parameter argument")); + } + + @SuppressWarnings("unchecked") + private Expression visitRequired(ParseTree ctx) { + + Expression expression = visit(ctx); + + if (expression == null) { + throw new UnsupportedOperationException("No result for expression: " + ctx.getText()); + } + + return (Expression) expression; + } + + private static String unquoteIdentifier(String text) { + + int end = text.length() - 1; + assert text.charAt(0) == '`' && text.charAt(end) == '`'; + // Unquote a parsed quoted identifier and handle escape sequences + final StringBuilder sb = new StringBuilder(text.length() - 2); + for (int i = 1; i < end; i++) { + char c = text.charAt(i); + switch (c) { + case '\\': + if (i + 1 < end) { + char nextChar = text.charAt(++i); + switch (nextChar) { + case 'b': + c = '\b'; + break; + case 't': + c = '\t'; + break; + case 'n': + c = '\n'; + break; + case 'f': + c = '\f'; + break; + case 'r': + c = '\r'; + break; + case '\\': + c = '\\'; + break; + case '\'': + c = '\''; + break; + case '"': + c = '"'; + break; + case '`': + c = '`'; + break; + case 'u': + c = (char) Integer.parseInt(text.substring(i + 1, i + 5), 16); + i += 4; + break; + default: + sb.append('\\'); + c = nextChar; + break; + } + } + break; + default: + break; + } + sb.append(c); + } + return sb.toString(); + } + + private static String unquoteStringLiteral(String text) { + + int end = text.length() - 1; + char delimiter = text.charAt(0); + Assert.isTrue(delimiter == text.charAt(end), "Quoted identifier does not end with the same delimiter"); + + // Unescape the parsed literal + final StringBuilder sb = new StringBuilder(text.length() - 2); + for (int i = 1; i < end; i++) { + char c = text.charAt(i); + switch (c) { + case '\'': + if (delimiter == '\'') { + i++; + } + break; + case '"': + if (delimiter == '"') { + i++; + } + break; + default: + break; + } + sb.append(c); + } + return sb.toString(); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java new file mode 100644 index 0000000000..cff6bea21e --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java @@ -0,0 +1,241 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Nulls; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Selection; + +import java.util.Locale; + +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Verify that {@link JpaSort#unsafe(String...)} works properly with Hibernate via {@link HqlOrderExpressionVisitor}. + * + * @author Greg Turnquist + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration("classpath:application-context.xml") +@Transactional +class HqlOrderExpressionVisitorUnitTests { + + @PersistenceContext EntityManager em; + + @Test + void genericFunctions() { + + assertThat(renderOrderBy(JpaSort.unsafe("LENGTH(firstname)"), "u")) + .startsWithIgnoringCase("order by character_length(u.firstname) asc"); + assertThat(renderOrderBy(JpaSort.unsafe("char_length(firstname)"), "u")) + .startsWithIgnoringCase("order by char_length(u.firstname) asc"); + + assertThat(renderOrderBy(JpaSort.unsafe("nlssort(firstname, 'NLS_SORT = XGERMAN_DIN_AI')"), "u")) + .startsWithIgnoringCase("order by nlssort(u.firstname, 'NLS_SORT = XGERMAN_DIN_AI')"); + } + + @Test // GH-3172 + void cast() { + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> renderOrderBy(JpaSort.unsafe("cast(emailAddress as date)"), "u")); + } + + @Test // GH-3172 + void extract() { + + assertThat(renderOrderBy(JpaSort.unsafe("EXTRACT(DAY FROM createdAt)"), "u")) + .startsWithIgnoringCase("order by extract(day from u.createdAt)"); + + assertThat(renderOrderBy(JpaSort.unsafe("WEEK(createdAt)"), "u")) + .startsWithIgnoringCase("order by extract(week from u.createdAt)"); + } + + @Test // GH-3172 + void trunc() { + assertThat(renderOrderBy(JpaSort.unsafe("TRUNC(age)"), "u")).startsWithIgnoringCase("order by trunc(u.age)"); + } + + @Test // GH-3172 + void upperLower() { + assertThat(renderOrderBy(JpaSort.unsafe("upper(firstname)"), "u")) + .startsWithIgnoringCase("order by upper(u.firstname)"); + assertThat(renderOrderBy(JpaSort.unsafe("lower(firstname)"), "u")) + .startsWithIgnoringCase("order by lower(u.firstname)"); + } + + @Test // GH-3172 + void substring() { + assertThat(renderOrderBy(JpaSort.unsafe("substring(emailAddress, 0, 3)"), "u")) + .startsWithIgnoringCase("order by substring(u.emailAddress, 0, 3) asc"); + assertThat(renderOrderBy(JpaSort.unsafe("substring(emailAddress, 0)"), "u")) + .startsWithIgnoringCase("order by substring(u.emailAddress, 0) asc"); + } + + @Test // GH-3172 + void repeat() { + assertThat(renderOrderBy(JpaSort.unsafe("repeat('a', 5)"), "u")) + .startsWithIgnoringCase("order by repeat('a', 5) asc"); + } + + @Test // GH-3172 + void literals() { + + assertThat(renderOrderBy(JpaSort.unsafe("age + 1"), "u")).startsWithIgnoringCase("order by u.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1l"), "u")).startsWithIgnoringCase("order by u.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1L"), "u")).startsWithIgnoringCase("order by u.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1"), "u")).startsWithIgnoringCase("order by u.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1f"), "u")).startsWithIgnoringCase("order by u.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1bi"), "u")).startsWithIgnoringCase("order by u.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1bd"), "u")).startsWithIgnoringCase("order by u.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 0x12"), "u")).startsWithIgnoringCase("order by u.age + 18"); + } + + @Test // GH-3172 + void arithmetic() { + + // Hibernate representation bugs, should be sum(u.age) + assertThat(renderOrderBy(JpaSort.unsafe("sum(age)"), "u")).startsWithIgnoringCase("order by sum()"); + assertThat(renderOrderBy(JpaSort.unsafe("min(age)"), "u")).startsWithIgnoringCase("order by min()"); + assertThat(renderOrderBy(JpaSort.unsafe("max(age)"), "u")).startsWithIgnoringCase("order by max()"); + + assertThat(renderOrderBy(JpaSort.unsafe("age"), "u")).startsWithIgnoringCase("order by u.age"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1"), "u")).startsWithIgnoringCase("order by u.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("ABS(age) + 1"), "u")).startsWithIgnoringCase("order by abs(u.age) + 1"); + + assertThat(renderOrderBy(JpaSort.unsafe("neg(active)"), "u")).startsWithIgnoringCase("order by neg(u.active)"); + assertThat(renderOrderBy(JpaSort.unsafe("abs(age)"), "u")).startsWithIgnoringCase("order by abs(u.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("ceiling(age)"), "u")).startsWithIgnoringCase("order by ceiling(u.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("floor(age)"), "u")).startsWithIgnoringCase("order by floor(u.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("round(age)"), "u")).startsWithIgnoringCase("order by round(u.age)"); + + assertThat(renderOrderBy(JpaSort.unsafe("prod(age, 1)"), "u")).startsWithIgnoringCase("order by prod(u.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("prod(age, age)"), "u")) + .startsWithIgnoringCase("order by prod(u.age, u.age)"); + + assertThat(renderOrderBy(JpaSort.unsafe("diff(age, 1)"), "u")).startsWithIgnoringCase("order by diff(u.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("quot(age, 1)"), "u")).startsWithIgnoringCase("order by quot(u.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("mod(age, 1)"), "u")).startsWithIgnoringCase("order by mod(u.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("sqrt(age)"), "u")).startsWithIgnoringCase("order by sqrt(u.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("exp(age)"), "u")).startsWithIgnoringCase("order by exp(u.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("ln(age)"), "u")).startsWithIgnoringCase("order by ln(u.age)"); + } + + @Test // GH-3172 + @Disabled("HHH-19075") + void trim() { + assertThat(renderOrderBy(JpaSort.unsafe("trim(leading '.' from lastname)"), "u")) + .startsWithIgnoringCase("order by repeat('a', 5) asc"); + } + + @Test // GH-3172 + void groupedExpression() { + assertThat(renderOrderBy(JpaSort.unsafe("(lastname)"), "u")).startsWithIgnoringCase("order by u.lastname"); + } + + @Test // GH-3172 + void tupleExpression() { + assertThat(renderOrderBy(JpaSort.unsafe("(firstname, lastname)"), "u")) + .startsWithIgnoringCase("order by u.firstname, u.lastname"); + } + + @Test // GH-3172 + void concat() { + assertThat(renderOrderBy(JpaSort.unsafe("firstname || lastname"), "u")) + .startsWithIgnoringCase("order by concat(u.firstname, u.lastname)"); + } + + @Test // GH-3172 + void pathBased() { + + String query = renderQuery(JpaSort.unsafe("manager.firstname"), "u"); + + assertThat(query).contains("from org.springframework.data.jpa.domain.sample.User u left join u.manager"); + assertThat(query).contains(".firstname asc nulls last"); + } + + @Test // GH-3172 + void caseSwitch() { + + assertThat(renderOrderBy(JpaSort.unsafe("case firstname when 'Oliver' then 'A' else firstname end"), "u")) + .startsWithIgnoringCase("order by case u.firstname when 'Oliver' then 'A' else u.firstname end"); + + assertThat(renderOrderBy( + JpaSort.unsafe("case firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else firstname end"), "u")) + .startsWithIgnoringCase( + "order by case u.firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else u.firstname end"); + + assertThat(renderOrderBy(JpaSort.unsafe("case when age < 31 then 'A' else firstname end"), "u")) + .startsWithIgnoringCase("order by case when u.age < 31 then 'A' else u.firstname end"); + + assertThat( + renderOrderBy(JpaSort.unsafe("case when firstname not in ('Oliver', 'Dave') then 'A' else firstname end"), "u")) + .startsWithIgnoringCase( + "order by case when u.firstname not in ('Oliver', 'Dave') then 'A' else u.firstname end"); + } + + private String renderOrderBy(JpaSort sort, String alias) { + + String query = renderQuery(sort, alias); + + String lowerCase = query.toLowerCase(Locale.ROOT); + int index = lowerCase.indexOf("order by"); + + if (index != -1) { + return query.substring(index); + } + + return ""; + } + + CriteriaQuery createQuery(JpaSort sort, String alias) { + + CriteriaQuery query = em.getCriteriaBuilder().createQuery(User.class); + Selection from = query.from(User.class).alias(alias); + HqlOrderExpressionVisitor extractor = new HqlOrderExpressionVisitor(em.getCriteriaBuilder(), (Path) from); + + Expression expression = extractor.createCriteriaExpression(sort.stream().findFirst().get()); + return query.select(from).orderBy(em.getCriteriaBuilder().asc(expression, Nulls.NONE)); + } + + @SuppressWarnings("rawtypes") + String renderQuery(JpaSort sort, String alias) { + + CriteriaQuery q = createQuery(sort, alias); + SqmSelectStatement s = (SqmSelectStatement) q; + + StringBuilder builder = new StringBuilder(); + s.appendHqlString(builder); + + return builder.toString(); + } +} From 020f4c238f9bda73aebdb151a4784c19dc78a18e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 28 Jan 2025 10:06:52 +0100 Subject: [PATCH 43/94] Polishing. Refine temporal literal handling. Update documentation. See #3172 Original pull request: #3187 --- .../data/jpa/domain/JpaSort.java | 39 +- .../query/HqlOrderExpressionVisitor.java | 341 ++++++++++++------ .../data/jpa/repository/query/QueryUtils.java | 11 +- .../jpa/repository/UserRepositoryTests.java | 33 ++ .../HqlOrderExpressionVisitorUnitTests.java | 30 +- .../modules/ROOT/pages/jpa/query-methods.adoc | 11 + 6 files changed, 339 insertions(+), 126 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java index 771b5361a6..89e4f35bfa 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java @@ -18,10 +18,6 @@ import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.PluralAttribute; -import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - import java.io.Serial; import java.util.ArrayList; import java.util.Arrays; @@ -29,8 +25,15 @@ import java.util.Collections; import java.util.List; +import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + /** - * Sort option for queries that wraps JPA meta-model {@link Attribute}s for sorting. + * Sort option for queries that wraps JPA metamodel {@link Attribute}s for sorting. + *

+ * {@link JpaSort#unsafe} accepts unsafe sort expressions, i. e. the String provided is not necessarily a property but + * can be an arbitrary expression piped into the query execution. * * @author Thomas Darimont * @author Oliver Gierke @@ -44,7 +47,7 @@ public class JpaSort extends Sort { @Serial private static final long serialVersionUID = 1L; private JpaSort(Direction direction, List> paths) { - this(Collections.emptyList(), direction, paths); + this(Collections. emptyList(), direction, paths); } private JpaSort(List orders, @Nullable Direction direction, List> paths) { @@ -76,7 +79,7 @@ public static JpaSort of(JpaSort.Path... paths) { /** * Creates a new {@link JpaSort} for the given direction and attributes. * - * @param direction the sorting direction. + * @param direction the sorting direction. * @param attributes must not be {@literal null} or empty. */ public static JpaSort of(Direction direction, Attribute... attributes) { @@ -87,7 +90,7 @@ public static JpaSort of(Direction direction, Attribute... attributes) { * Creates a new {@link JpaSort} for the given direction and {@link Path}s. * * @param direction the sorting direction. - * @param paths must not be {@literal null} or empty. + * @param paths must not be {@literal null} or empty. */ public static JpaSort of(Direction direction, Path... paths) { return new JpaSort(direction, Arrays.asList(paths)); @@ -96,7 +99,7 @@ public static JpaSort of(Direction direction, Path... paths) { /** * Returns a new {@link JpaSort} with the given sorting criteria added to the current one. * - * @param direction can be {@literal null}. + * @param direction can be {@literal null}. * @param attributes must not be {@literal null}. * @return */ @@ -111,7 +114,7 @@ public JpaSort and(@Nullable Direction direction, Attribute... attributes) * Returns a new {@link JpaSort} with the given sorting criteria added to the current one. * * @param direction can be {@literal null}. - * @param paths must not be {@literal null}. + * @param paths must not be {@literal null}. * @return */ public JpaSort and(@Nullable Direction direction, Path... paths) { @@ -130,7 +133,7 @@ public JpaSort and(@Nullable Direction direction, Path... paths) { /** * Returns a new {@link JpaSort} with the given sorting criteria added to the current one. * - * @param direction can be {@literal null}. + * @param direction can be {@literal null}. * @param properties must not be {@literal null} or empty. * @return */ @@ -148,7 +151,7 @@ public JpaSort andUnsafe(@Nullable Direction direction, String... properties) { orders.add(new JpaOrder(direction, property)); } - return new JpaSort(orders, direction, Collections.>emptyList()); + return new JpaSort(orders, direction, Collections.> emptyList()); } /** @@ -219,7 +222,7 @@ public static JpaSort unsafe(String... properties) { /** * Creates new unsafe {@link JpaSort} based on given {@link Direction} and properties. * - * @param direction must not be {@literal null}. + * @param direction must not be {@literal null}. * @param properties must not be {@literal null} or empty. * @return */ @@ -235,7 +238,7 @@ public static JpaSort unsafe(Direction direction, String... properties) { /** * Creates new unsafe {@link JpaSort} based on given {@link Direction} and properties. * - * @param direction must not be {@literal null}. + * @param direction must not be {@literal null}. * @param properties must not be {@literal null} or empty. * @return */ @@ -327,7 +330,7 @@ public static class JpaOrder extends Order { * {@link Sort#DEFAULT_DIRECTION} * * @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}. - * @param property must not be {@literal null}. + * @param property must not be {@literal null}. */ private JpaOrder(@Nullable Direction direction, String property) { this(direction, property, NullHandling.NATIVE); @@ -337,8 +340,8 @@ private JpaOrder(@Nullable Direction direction, String property) { * Creates a new {@link Order} instance. if order is {@literal null} then order defaults to * {@link Sort#DEFAULT_DIRECTION}. * - * @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}. - * @param property must not be {@literal null}. + * @param direction can be {@literal null}, will default to {@link Sort#DEFAULT_DIRECTION}. + * @param property must not be {@literal null}. * @param nullHandlingHint can be {@literal null}, will default to {@link NullHandling#NATIVE}. */ private JpaOrder(@Nullable Direction direction, String property, NullHandling nullHandlingHint) { @@ -346,7 +349,7 @@ private JpaOrder(@Nullable Direction direction, String property, NullHandling nu } private JpaOrder(@Nullable Direction direction, String property, boolean ignoreCase, NullHandling nullHandling, - boolean unsafe) { + boolean unsafe) { super(direction, property, ignoreCase, nullHandling); this.unsafe = unsafe; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java index ade370c0f0..e5915f19e3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jpa.repository.query; +import static java.time.format.DateTimeFormatter.*; + import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.From; @@ -24,9 +26,18 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.Temporal; import java.util.Collection; import java.util.HexFormat; +import java.util.Locale; +import java.util.function.BiFunction; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; @@ -47,24 +58,48 @@ * @author Mark Paluch * @since 4.0 */ -@SuppressWarnings("ConstantValue") +@SuppressWarnings({ "unchecked", "rawtypes", "ConstantValue" }) class HqlOrderExpressionVisitor extends HqlBaseVisitor> { + private static final DateTimeFormatter DATE_TIME = new DateTimeFormatterBuilder().parseCaseInsensitive() + .append(ISO_LOCAL_DATE).optionalStart().appendLiteral(' ').optionalEnd().optionalStart().appendLiteral('T') + .optionalEnd().append(ISO_LOCAL_TIME).optionalStart().appendLiteral(' ').optionalEnd().optionalStart() + .appendZoneOrOffsetId().optionalEnd().toFormatter(); + + private static final DateTimeFormatter DATE_TIME_FORMATTER_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd", + Locale.ENGLISH); + + private static final DateTimeFormatter DATE_TIME_FORMATTER_TIME = DateTimeFormatter.ofPattern("HH:mm:ss", + Locale.ENGLISH); + + private static final String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe(…)"; + private final CriteriaBuilder cb; private final Path from; - private static String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe"; + private final BiFunction, PropertyPath, Expression> expressionFactory; - HqlOrderExpressionVisitor(CriteriaBuilder cb, Path from) { + /** + * @param cb criteria builder. + * @param from from path (i.e. root entity). + * @param expressionFactory factory to create expressions such as + * {@link QueryUtils#toExpressionRecursively(From, PropertyPath)}. + */ + HqlOrderExpressionVisitor(CriteriaBuilder cb, Path from, + BiFunction, PropertyPath, Expression> expressionFactory) { this.cb = cb; this.from = from; + this.expressionFactory = expressionFactory; } /** * Extract the {@link org.springframework.data.jpa.domain.JpaSort.JpaOrder}'s property and parse it as an HQL * {@literal sortExpression}. * - * @param jpaOrder + * @param jpaOrder must not be {@literal null}. * @return criteriaExpression + * @throws IllegalArgumentException thrown if the order yields no sort expression. + * @throws UnsupportedOperationException thrown if the order contains an unsupported expression. + * @throws BadJpqlGrammarException thrown if the order contains a syntax errors. */ Expression createCriteriaExpression(Sort.Order jpaOrder) { @@ -100,32 +135,24 @@ public Expression visitSortExpression(HqlParser.SortExpressionContext ctx) { } @Override - @SuppressWarnings("rawtypes") public Expression visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) { Expression left = visitRequired(ctx.expression(0)); Expression right = visitRequired(ctx.expression(1)); String op = ctx.op.getText(); - if (op.equals("=")) { - return cb.equal(left, right); - } else if (op.equals(">")) { - return cb.greaterThan(left, right); - } else if (op.equals(">=")) { - return cb.greaterThanOrEqualTo(left, right); - } else if (op.equals("<")) { - return cb.lessThan(left, right); - } else if (op.equals("<=")) { - return cb.lessThanOrEqualTo(left, right); - } else if (op.equals("<>") || op.equals("!=") || op.equals("^=")) { - return cb.notEqual(left, right); - } else { - throw new UnsupportedOperationException("Unsupported comparison operator: " + op); - } + return switch (op) { + case "=" -> cb.equal(left, right); + case ">" -> cb.greaterThan(left, right); + case ">=" -> cb.greaterThanOrEqualTo(left, right); + case "<" -> cb.lessThan(left, right); + case "<=" -> cb.lessThanOrEqualTo(left, right); + case "<>", "!=", "^=" -> cb.notEqual(left, right); + default -> throw new UnsupportedOperationException("Unsupported comparison operator: " + op); + }; } @Override - @SuppressWarnings("rawtypes") public Expression visitBetweenExpression(HqlParser.BetweenExpressionContext ctx) { Expression condition = visitRequired(ctx.expression(0)); @@ -244,7 +271,7 @@ public Expression visitGenericFunction(HqlParser.GenericFunctionContext ctx) } Expression[] arguments = ctx.genericFunctionArguments().expressionOrPredicate().stream() // - .map(expressionOrPredicateContext -> visitRequired(expressionOrPredicateContext)) // + .map(this::visitRequired) // .toArray(Expression[]::new); return cb.function(functionName, Object.class, arguments); @@ -371,7 +398,6 @@ public Expression visitExtractFunction(HqlParser.ExtractFunctionContext ctx) } @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) public Expression visitTruncFunction(HqlParser.TruncFunctionContext ctx) { Expression expr = visitRequired(ctx.expression().get(0)); @@ -497,21 +523,6 @@ private Number getLiteralValue(HqlParser.NumericLiteralContext ctx) { throw new UnsupportedOperationException("Unsupported literal: " + ctx.getText()); } - static String getDecimals(TerminalNode input) { - - String text = input.getText(); - StringBuilder result = new StringBuilder(text.length()); - - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (Character.isDigit(c) || c == '-' || c == '+' || c == '.') { - result.append(c); - } - } - - return result.toString(); - } - @Override public Expression visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) { @@ -526,6 +537,97 @@ public Expression visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) return null; } + @Override + public Expression visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ctx) { + + if (ctx.time() != null) { + return visitRequired(ctx.time()); + } + + return cb.literal(DATE_TIME_FORMATTER_TIME.parse(unquoteTemporal(ctx.genericTemporalLiteralText()))); + } + + @Override + public Expression visitDate(HqlParser.DateContext ctx) { + return cb.literal(LocalDate.from(DATE_TIME_FORMATTER_DATE.parse(unquoteTemporal(ctx)))); + } + + @Override + public Expression visitTime(HqlParser.TimeContext ctx) { + return cb.literal(LocalTime.from(DATE_TIME_FORMATTER_TIME.parse(unquoteTemporal(ctx)))); + } + + @Override + public Expression visitJdbcDateLiteral(HqlParser.JdbcDateLiteralContext ctx) { + + if (ctx.date() != null) { + return visitRequired(ctx.date()); + } + + return cb + .literal(LocalDate.from(DATE_TIME_FORMATTER_DATE.parse(unquoteTemporal(ctx.genericTemporalLiteralText())))); + } + + @Override + public Expression visitJdbcTimestampLiteral(HqlParser.JdbcTimestampLiteralContext ctx) { + + if (ctx.dateTime() != null) { + return visitRequired(ctx.dateTime()); + } + + return cb.literal(LocalDateTime.from(DATE_TIME.parse(unquoteTemporal(ctx.genericTemporalLiteralText())))); + } + + @Override + public Expression visitLocalDateTime(HqlParser.LocalDateTimeContext ctx) { + return cb.literal(LocalDateTime.from(DATE_TIME.parse(unquoteTemporal(ctx.getText())))); + } + + @Override + public Expression visitZonedDateTime(HqlParser.ZonedDateTimeContext ctx) { + return cb.literal(ZonedDateTime.parse(ctx.getText())); + } + + @Override + public Expression visitOffsetDateTime(HqlParser.OffsetDateTimeContext ctx) { + return cb.literal(OffsetDateTime.parse(ctx.getText())); + } + + @Override + public Expression visitOffsetDateTimeWithMinutes(HqlParser.OffsetDateTimeWithMinutesContext ctx) { + return cb.literal(OffsetDateTime.parse(ctx.getText())); + } + + @Override + public Expression visitLocalDateTimeLiteral(HqlParser.LocalDateTimeLiteralContext ctx) { + return visitRequired(ctx.localDateTime()); + } + + @Override + public Expression visitZonedDateTimeLiteral(HqlParser.ZonedDateTimeLiteralContext ctx) { + return visitRequired(ctx.zonedDateTime()); + } + + @Override + public Expression visitOffsetDateTimeLiteral(HqlParser.OffsetDateTimeLiteralContext ctx) { + return visitRequired(ctx.offsetDateTime() != null ? ctx.offsetDateTime() : ctx.offsetDateTimeWithMinutes()); + } + + @Override + public Expression visitDateLiteral(HqlParser.DateLiteralContext ctx) { + return visitRequired(ctx.date()); + } + + @Override + public Expression visitTimeLiteral(HqlParser.TimeLiteralContext ctx) { + return visitRequired(ctx.time()); + } + + @Override + public Expression visitDateTime(HqlParser.DateTimeContext ctx) { + return super.visitDateTime(ctx); + } + @Override public Expression visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) { return visit(ctx.expression()); @@ -579,56 +681,47 @@ public Expression visitHqlConcatenationExpression(HqlParser.HqlConcatenationE @Override public Expression visitSimplePath(HqlParser.SimplePathContext ctx) { - return QueryUtils.toExpressionRecursively((From) from, PropertyPath.from(ctx.getText(), from.getJavaType())); - } - - String getString(HqlParser.IdentifierContext context) { - - HqlParser.NakedIdentifierContext ni = context.nakedIdentifier(); - - String text = context.getText(); - if (ni != null) { - if (ni.QUOTED_IDENTIFIER() != null) { - text = unquoteIdentifier(ni.getText()); - } - } - return text; + return expressionFactory.apply((From) from, PropertyPath.from(ctx.getText(), from.getJavaType())); } @Override public Expression visitCaseList(HqlParser.CaseListContext ctx) { - if (ctx.simpleCaseExpression() != null) { - return visit(ctx.simpleCaseExpression()); - } else { - return visit(ctx.searchedCaseExpression()); - } + return visit(ctx.simpleCaseExpression() != null ? ctx.simpleCaseExpression() : ctx.searchedCaseExpression()); } @Override public Expression visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) { + CriteriaBuilder.SimpleCase simpleCase = cb.selectCase(visit(ctx.expressionOrPredicate(0))); + ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> { simpleCase.when( // visitRequired(caseWhenExpressionClauseContext.expression()), // visitRequired(caseWhenExpressionClauseContext.expressionOrPredicate())); }); + if (ctx.expressionOrPredicate().size() == 2) { simpleCase.otherwise(visitRequired(ctx.expressionOrPredicate(1))); } + return simpleCase; } @Override public Expression visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) { + CriteriaBuilder.Case searchedCase = cb.selectCase(); + ctx.caseWhenPredicateClause().forEach(caseWhenPredicateClauseContext -> { searchedCase.when( // visitRequired(caseWhenPredicateClauseContext.predicate()), // visit(caseWhenPredicateClauseContext.expressionOrPredicate())); }); + if (ctx.expressionOrPredicate() != null) { searchedCase.otherwise(visit(ctx.expressionOrPredicate())); } + return searchedCase; } @@ -637,7 +730,6 @@ public Expression visitParameter(HqlParser.ParameterContext ctx) { throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a parameter argument")); } - @SuppressWarnings("unchecked") private Expression visitRequired(ParseTree ctx) { Expression expression = visit(ctx); @@ -649,59 +741,98 @@ private Expression visitRequired(ParseTree ctx) { return (Expression) expression; } + private String getString(HqlParser.IdentifierContext context) { + + HqlParser.NakedIdentifierContext ni = context.nakedIdentifier(); + + String text = context.getText(); + if (ni != null) { + if (ni.QUOTED_IDENTIFIER() != null) { + text = unquoteIdentifier(ni.getText()); + } + } + return text; + } + + private static String getDecimals(TerminalNode input) { + + String text = input.getText(); + StringBuilder result = new StringBuilder(text.length()); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (Character.isDigit(c) || c == '-' || c == '+' || c == '.') { + result.append(c); + } + } + + return result.toString(); + } + + private static String unquoteTemporal(ParseTree node) { + return unquoteTemporal(node.getText()); + } + + private static String unquoteTemporal(String temporal) { + if (temporal.startsWith("'") && temporal.endsWith("'")) { + temporal = temporal.substring(1, temporal.length() - 1); + } + return temporal; + } + private static String unquoteIdentifier(String text) { int end = text.length() - 1; - assert text.charAt(0) == '`' && text.charAt(end) == '`'; + + Assert.isTrue(text.charAt(0) == '`' && text.charAt(end) == '`', + "Quoted identifier does not end with the same delimiter"); + // Unquote a parsed quoted identifier and handle escape sequences - final StringBuilder sb = new StringBuilder(text.length() - 2); + StringBuilder sb = new StringBuilder(text.length() - 2); for (int i = 1; i < end; i++) { + char c = text.charAt(i); - switch (c) { - case '\\': - if (i + 1 < end) { - char nextChar = text.charAt(++i); - switch (nextChar) { - case 'b': - c = '\b'; - break; - case 't': - c = '\t'; - break; - case 'n': - c = '\n'; - break; - case 'f': - c = '\f'; - break; - case 'r': - c = '\r'; - break; - case '\\': - c = '\\'; - break; - case '\'': - c = '\''; - break; - case '"': - c = '"'; - break; - case '`': - c = '`'; - break; - case 'u': - c = (char) Integer.parseInt(text.substring(i + 1, i + 5), 16); - i += 4; - break; - default: - sb.append('\\'); - c = nextChar; - break; - } + if (c == '\\') { + if (i + 1 < end) { + char nextChar = text.charAt(++i); + switch (nextChar) { + case 'b': + c = '\b'; + break; + case 't': + c = '\t'; + break; + case 'n': + c = '\n'; + break; + case 'f': + c = '\f'; + break; + case 'r': + c = '\r'; + break; + case '\\': + c = '\\'; + break; + case '\'': + c = '\''; + break; + case '"': + c = '"'; + break; + case '`': + c = '`'; + break; + case 'u': + c = (char) Integer.parseInt(text.substring(i + 1, i + 5), 16); + i += 4; + break; + default: + sb.append('\\'); + c = nextChar; + break; } - break; - default: - break; + } } sb.append(c); } @@ -715,7 +846,7 @@ private static String unquoteStringLiteral(String text) { Assert.isTrue(delimiter == text.charAt(end), "Quoted identifier does not end with the same delimiter"); // Unescape the parsed literal - final StringBuilder sb = new StringBuilder(text.length() - 2); + StringBuilder sb = new StringBuilder(text.length() - 2); for (int i = 1; i < end; i++) { char c = text.charAt(i); switch (c) { 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 e51d305e0b..bbb638eda0 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 @@ -725,8 +725,15 @@ public static String getProjection(String query) { @SuppressWarnings("unchecked") private static jakarta.persistence.criteria.Order toJpaOrder(Order order, From from, CriteriaBuilder cb) { - PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType()); - Expression expression = toExpressionRecursively(from, property); + Expression expression; + + if (order instanceof JpaOrder jpaOrder && jpaOrder.isUnsafe()) { + expression = new HqlOrderExpressionVisitor(cb, from, QueryUtils::toExpressionRecursively) + .createCriteriaExpression(order); + } else { + PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType()); + expression = toExpressionRecursively(from, property); + } Nulls nulls = toNulls(order.getNullHandling()); 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 94f69747f8..ed49e479bb 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 @@ -60,6 +60,7 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.DeleteSpecification; +import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.domain.PredicateSpecification; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.UpdateSpecification; @@ -3228,6 +3229,38 @@ void handlesColonsFollowedByIntegerInStringLiteral() { assertThat(users).extracting(User::getId).containsExactly(expected.getId()); } + @Test // GH-3172 + void specificationShouldApplyUnsafeSort() { + + flushTestUsers(); + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + PredicateSpecification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + + List result = repository.findBy(spec, q -> q.sortBy(JpaSort.unsafe("LENGTH(firstname)")).all()); + + assertThat(result).containsExactly(thirdUser, firstUser); + } + + @Test // GH-3172 + void findAllShouldApplyUnsafeSort() { + + flushTestUsers(); + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + assertThat( + repository.findAll(JpaSort.unsafe("case when firstname ilike 'O%' escape '^' then 'A' else firstname end"))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + } + @Test // DATAJPA-1233, GH-3756 void handlesCountQueriesWithLessParametersSingleParam() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java index cff6bea21e..98ac54ca79 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java @@ -120,6 +120,33 @@ void literals() { assertThat(renderOrderBy(JpaSort.unsafe("age + 0x12"), "u")).startsWithIgnoringCase("order by u.age + 18"); } + @Test // GH-3172 + void temporalLiterals() { + + // JDBC + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2024-01-01 12:34:56'}"), "u")) + .startsWithIgnoringCase("order by u.createdAt + 2024-01-01T12:34:56"); + + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2012-01-03 09:00:00.000000001'}"), "u")) + .startsWithIgnoringCase("order by u.createdAt + 2012-01-03T09:00:00.000000001"); + + // Hibernate NPE + assertThatNullPointerException().isThrownBy(() -> renderOrderBy(JpaSort.unsafe("createdAt + {t '12:34:56'}"), "u")); + + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d '2024-01-01'}"), "u")) + .startsWithIgnoringCase("order by u.createdAt + 2024-01-01"); + + // JPQL + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts 2024-01-01 12:34:56}"), "u")) + .startsWithIgnoringCase("order by u.createdAt + 2024-01-01T12:34:56"); + + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {t 12:34:56}"), "u")) + .startsWithIgnoringCase("order by u.createdAt + 12:34:56"); + + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d 2024-01-01}"), "u")) + .startsWithIgnoringCase("order by u.createdAt + 2024-01-01"); + } + @Test // GH-3172 void arithmetic() { @@ -221,7 +248,8 @@ CriteriaQuery createQuery(JpaSort sort, String alias) { CriteriaQuery query = em.getCriteriaBuilder().createQuery(User.class); Selection from = query.from(User.class).alias(alias); - HqlOrderExpressionVisitor extractor = new HqlOrderExpressionVisitor(em.getCriteriaBuilder(), (Path) from); + HqlOrderExpressionVisitor extractor = new HqlOrderExpressionVisitor(em.getCriteriaBuilder(), (Path) from, + QueryUtils::toExpressionRecursively); Expression expression = extractor.createCriteriaExpression(sort.stream().findFirst().get()); return query.select(from).orderBy(em.getCriteriaBuilder().asc(expression, Nulls.NONE)); diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 116676fba7..8dd229918c 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -383,6 +383,17 @@ Throws Exception. <4> Valid `Sort` expression pointing to aliased function. ==== +=== JpaSort.unsafe(…) limitations + +`JpaSort.unsafe(…)` operates in two modes: + +* When used with derived Queries or String-based Queries, the order string is appended to the query. +* When used with Query by Example or Specifications (that use `CriteriaQuery`), order expressions are parsed and added to the `CriteriaQuery` as expressions. +Query expressions can contain function calls, various clauses (such as `CASE WHEN`, arithmetic expressions) or property paths. +Order translation does not support subquery expressions, `TREAT` and `CAST`.` + +[[jpa.query-methods.paging]] + [[jpa.query-methods.scroll]] == Scrolling Large Query Results From 2590e6b838a4f31963336fde97d3732368874de4 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Sun, 28 Apr 2024 09:36:37 +0800 Subject: [PATCH 44/94] Polishing. Add missing `@FunctionalInterface` to Specification interfaces. `SpecificationUnitTests` shouldn't be `Serializable`. Closes #3452 --- .../data/jpa/domain/PredicateSpecification.java | 1 + .../data/jpa/domain/SpecificationUnitTests.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index dc17edbfc4..5ed394ad1d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -40,6 +40,7 @@ * @author Mark Paluch * @since 4.0 */ +@FunctionalInterface public interface PredicateSpecification extends Serializable { /** diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index fc1bc5431c..c8c7228433 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -88,7 +88,7 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation" }) + @SuppressWarnings({ "unchecked", "deprecation"}) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -103,7 +103,7 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation" }) + @SuppressWarnings({ "unchecked", "deprecation"}) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); From 06ad2b08c3ab3abffd03fcf79fb9496f2b85a4be Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 17 Feb 2025 11:22:46 +0100 Subject: [PATCH 45/94] Migrate to JSpecify annotations for nullability constraints. Closes #3745 Original pull request: #3781 --- .../v4/tool/templates/codegen/Java/Java.stg | 35 ++++++-- .../repository/config/package-info.java | 2 +- .../EnversRevisionRepositoryFactoryBean.java | 5 +- .../repository/support/package-info.java | 2 +- spring-data-jpa/pom.xml | 85 +++++++++++++++++++ .../QueryByExamplePredicateBuilder.java | 9 +- .../data/jpa/convert/package-info.java | 2 +- .../convert/threeten/Jsr310JpaConverters.java | 45 ++++------ .../jpa/convert/threeten/package-info.java | 2 +- .../data/jpa/domain/AbstractAuditable.java | 20 +++-- .../data/jpa/domain/AbstractPersistable.java | 13 ++- .../data/jpa/domain/DeleteSpecification.java | 3 +- .../data/jpa/domain/JpaSort.java | 3 +- .../jpa/domain/PredicateSpecification.java | 3 +- .../data/jpa/domain/Specification.java | 5 +- .../jpa/domain/SpecificationComposition.java | 24 +++--- .../data/jpa/domain/UpdateSpecification.java | 3 +- .../data/jpa/domain/package-info.java | 2 +- .../support/AuditingEntityListener.java | 3 +- .../data/jpa/domain/support/package-info.java | 2 +- .../mapping/JpaMetamodelMappingContext.java | 9 +- .../jpa/mapping/JpaPersistentEntityImpl.java | 5 +- .../mapping/JpaPersistentPropertyImpl.java | 11 ++- .../data/jpa/mapping/package-info.java | 2 +- .../data/jpa/projection/package-info.java | 2 +- .../data/jpa/provider/HibernateUtils.java | 5 +- .../data/jpa/provider/JpaClassUtils.java | 3 +- .../jpa/provider/PersistenceProvider.java | 22 ++--- .../data/jpa/provider/ProxyIdAccessor.java | 2 +- .../data/jpa/provider/QueryComment.java | 2 +- .../data/jpa/provider/QueryExtractor.java | 2 +- .../data/jpa/provider/package-info.java | 2 +- .../repository/JpaSpecificationExecutor.java | 3 +- .../jpa/repository/aot/JpaRuntimeHints.java | 3 +- .../data/jpa/repository/cdi/package-info.java | 2 +- .../config/AuditingBeanDefinitionParser.java | 4 +- ...JpaMetamodelMappingContextFactoryBean.java | 3 +- .../config/JpaRepositoryConfigExtension.java | 13 +-- .../jpa/repository/config/package-info.java | 2 +- .../data/jpa/repository/package-info.java | 2 +- .../repository/query/AbstractJpaQuery.java | 16 ++-- .../query/AbstractStringBasedJpaQuery.java | 7 +- .../query/BadJpqlGrammarException.java | 9 +- .../jpa/repository/query/DeclaredQuery.java | 3 +- .../query/DefaultQueryEnhancer.java | 7 +- .../DtoProjectionTransformerDelegate.java | 2 +- .../repository/query/EmptyDeclaredQuery.java | 4 +- .../query/EmptyQueryTokenStream.java | 6 +- .../query/EqlCountQueryTransformer.java | 5 +- .../query/EqlQueryIntrospector.java | 3 +- .../query/EqlSortedQueryTransformer.java | 3 +- .../jpa/repository/query/EscapeCharacter.java | 5 +- .../query/ExpressionBasedStringQuery.java | 11 ++- ...bernateJpaParametersParameterAccessor.java | 6 +- .../query/HibernateQueryInformation.java | 2 +- .../query/HqlCountQueryTransformer.java | 3 +- .../query/HqlOrderExpressionVisitor.java | 5 +- .../query/HqlQueryIntrospector.java | 3 +- .../query/HqlSortedQueryTransformer.java | 3 +- .../query/JSqlParserQueryEnhancer.java | 39 ++++++--- .../data/jpa/repository/query/Jpa21Utils.java | 12 ++- .../jpa/repository/query/JpaEntityGraph.java | 5 +- .../query/JpaKeysetScrollQueryCreator.java | 11 +-- .../jpa/repository/query/JpaParameters.java | 3 +- .../query/JpaParametersParameterAccessor.java | 6 +- .../jpa/repository/query/JpaQueryCreator.java | 7 +- .../repository/query/JpaQueryEnhancer.java | 6 +- .../repository/query/JpaQueryExecution.java | 19 ++--- .../jpa/repository/query/JpaQueryFactory.java | 3 +- .../query/JpaQueryLookupStrategy.java | 5 +- .../jpa/repository/query/JpaQueryMethod.java | 11 ++- .../query/JpaQueryTransformerSupport.java | 3 +- .../repository/query/JpaResultConverters.java | 7 +- .../query/JpqlCountQueryTransformer.java | 8 +- .../repository/query/JpqlQueryBuilder.java | 12 ++- .../query/JpqlQueryIntrospector.java | 2 +- .../query/JpqlSortedQueryTransformer.java | 3 +- .../data/jpa/repository/query/JpqlUtils.java | 29 ++++--- .../query/KeysetScrollDelegate.java | 16 ++-- .../query/KeysetScrollSpecification.java | 35 +++++--- .../data/jpa/repository/query/Meta.java | 9 +- .../data/jpa/repository/query/NamedQuery.java | 8 +- .../jpa/repository/query/NativeJpaQuery.java | 8 +- .../repository/query/ParameterBinding.java | 29 ++++--- .../query/ParameterMetadataProvider.java | 12 ++- .../repository/query/PartTreeJpaQuery.java | 4 +- .../repository/query/PartTreeQueryCache.java | 3 +- .../repository/query/ProcedureParameter.java | 2 +- .../jpa/repository/query/QueryEnhancer.java | 3 +- .../repository/query/QueryInformation.java | 5 +- .../query/QueryParameterSetter.java | 2 +- .../query/QueryParameterSetterFactory.java | 32 +++---- .../jpa/repository/query/QueryRenderer.java | 36 +++----- .../repository/query/QueryTokenStream.java | 8 +- .../data/jpa/repository/query/QueryUtils.java | 20 +++-- .../jpa/repository/query/SimpleJpaQuery.java | 3 +- .../query/StoredProcedureAttributeSource.java | 6 +- .../query/StoredProcedureAttributes.java | 3 +- .../query/StoredProcedureJpaQuery.java | 3 +- .../jpa/repository/query/StringQuery.java | 16 ++-- .../jpa/repository/query/package-info.java | 2 +- .../support/CrudMethodMetadata.java | 6 +- .../CrudMethodMetadataPostProcessor.java | 43 +++++----- .../repository/support/DefaultQueryHints.java | 21 +++-- .../support/EntityGraphFactory.java | 11 ++- .../FetchableFluentQueryByPredicate.java | 6 +- .../FetchableFluentQueryBySpecification.java | 7 +- .../support/FluentQuerySupport.java | 3 +- .../jpa/repository/support/JakartaTuple.java | 6 +- .../support/JpaEntityInformation.java | 3 +- .../JpaEvaluationContextExtension.java | 3 +- .../JpaMetamodelEntityInformation.java | 14 ++- .../JpaPersistableEntityInformation.java | 6 +- .../support/JpaRepositoryFactory.java | 14 +-- .../support/JpaRepositoryFactoryBean.java | 5 +- .../data/jpa/repository/support/Querydsl.java | 5 +- .../support/QuerydslJpaPredicateExecutor.java | 12 ++- .../support/QuerydslRepositorySupport.java | 9 +- .../support/SimpleJpaRepository.java | 16 ++-- .../support/SpringDataJpaQuery.java | 3 +- .../jpa/repository/support/package-info.java | 2 +- ...hScanningPersistenceUnitPostProcessor.java | 3 +- .../data/jpa/support/package-info.java | 2 +- .../data/jpa/util/HibernateProxyDetector.java | 6 +- .../data/jpa/util/package-info.java | 2 +- .../data/jpa/AntlrVersionTests.java | 2 +- .../data/jpa/domain/JpaSortTests.java | 2 +- .../data/jpa/domain/sample/AuditableUser.java | 3 +- .../domain/sample/UserWithOptionalField.java | 2 +- .../jpa/repository/UserRepositoryTests.java | 2 +- .../AbstractStringBasedJpaQueryUnitTests.java | 2 +- .../query/EqlQueryTransformerTests.java | 2 +- .../query/HqlQueryTransformerTests.java | 5 +- .../jpa/repository/query/Jpa21UtilsTests.java | 8 +- .../query/JpaQueryCreatorTests.java | 11 +-- .../query/JpqlQueryTransformerTests.java | 2 +- .../query/QueryEnhancerFactoryUnitTests.java | 2 +- .../QueryWithNullLikeIntegrationTests.java | 2 +- .../query/SimpleJpaQueryUnitTests.java | 2 +- ...ethodsWithEntityGraphConfigRepository.java | 2 +- .../jpa/repository/sample/UserRepository.java | 3 +- .../support/SimpleJpaRepositoryUnitTests.java | 2 +- 142 files changed, 681 insertions(+), 507 deletions(-) diff --git a/org/antlr/v4/tool/templates/codegen/Java/Java.stg b/org/antlr/v4/tool/templates/codegen/Java/Java.stg index fc455cfa1d..7f1701c00f 100644 --- a/org/antlr/v4/tool/templates/codegen/Java/Java.stg +++ b/org/antlr/v4/tool/templates/codegen/Java/Java.stg @@ -48,14 +48,18 @@ ParserFile(file, parser, namedActions, contextSuperClass) ::= << package ; + import org.antlr.v4.runtime.atn.*; import org.antlr.v4.runtime.dfa.DFA; import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.misc.*; import org.antlr.v4.runtime.tree.*; +import org.jspecify.annotations.NullUnmarked; import java.util.List; import java.util.Iterator; import java.util.ArrayList; +import jakarta.annotation.Generated; + >> @@ -67,11 +71,15 @@ package ;
import org.antlr.v4.runtime.tree.ParseTreeListener; +import org.jspecify.annotations.NullUnmarked; +import jakarta.annotation.Generated; /** * This interface defines a complete listener for a parse tree produced by * {@link }. */ +@NullUnmarked +@Generated("Listener") interface Listener extends ParseTreeListener { ;
- import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ErrorNode; import org.antlr.v4.runtime.tree.TerminalNode; +import org.jspecify.annotations.NullUnmarked; +import jakarta.annotation.Generated; /** * This class provides an empty implementation of {@link Listener}, * which can be extended to create a listener which only needs to handle a subset * of the available methods. */ -@SuppressWarnings("CheckReturnValue") +@NullUnmarked +@Generated("BaseListener") +@SuppressWarnings({ "CheckReturnValue", "NullAway" }) class BaseListener implements Listener { ;
import org.antlr.v4.runtime.tree.ParseTreeVisitor; +import org.jspecify.annotations.NullUnmarked; +import jakarta.annotation.Generated; /** * This interface defines a complete generic visitor for a parse tree produced @@ -171,6 +184,8 @@ import org.antlr.v4.runtime.tree.ParseTreeVisitor; * @param \ The return type of the visit operation. Use {@link Void} for * operations with no return type. */ +@NullUnmarked +@Generated("Visitor") interface Visitor\ extends ParseTreeVisitor\ { ;
import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; +import org.jspecify.annotations.NullUnmarked; +import jakarta.annotation.Generated; /** * This class provides an empty implementation of {@link Visitor}, @@ -203,7 +220,9 @@ import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; * @param \ The return type of the visit operation. Use {@link Void} for * operations with no return type. */ -@SuppressWarnings("CheckReturnValue") +@NullUnmarked +@Generated("BaseVisitor") +@SuppressWarnings({ "CheckReturnValue", "NullAway" }) class BaseVisitor\ extends AbstractParseTreeVisitor\ implements Visitor\ { > Parser_(parser, funcs, atn, sempredFuncs, ctor, superClass) ::= << -@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue"}) +@NullUnmarked +@Generated("") +@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "NullAway"}) class extends { // Customization: Suppress version check // static { RuntimeMetaData.checkVersion("", RuntimeMetaData.VERSION); } @@ -895,12 +916,16 @@ import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.atn.*; import org.antlr.v4.runtime.dfa.DFA; import org.antlr.v4.runtime.misc.*; +import org.jspecify.annotations.NullUnmarked; +import jakarta.annotation.Generated; >> Lexer(lexer, atn, actionFuncs, sempredFuncs, superClass) ::= << -@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue"}) +@NullUnmarked +@Generated("") +@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "NullAway"}) class extends { // Customization: Suppress version check // static { RuntimeMetaData.checkVersion("", RuntimeMetaData.VERSION); } diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/package-info.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/package-info.java index 2e79b25c03..ab7c7b3781 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/package-info.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/package-info.java @@ -1,5 +1,5 @@ /** * Classes for Envers Repositories configuration support. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.envers.repository.config; diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java index 825a1d1a4e..b152bef044 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryFactoryBean.java @@ -21,6 +21,7 @@ import org.hibernate.envers.DefaultRevisionEntity; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; @@ -39,7 +40,7 @@ public class EnversRevisionRepositoryFactoryBean, S, ID, N extends Number & Comparable> extends JpaRepositoryFactoryBean { - private Class revisionEntityClass; + private @Nullable Class revisionEntityClass; /** * Creates a new {@link EnversRevisionRepositoryFactoryBean} for the given repository interface. @@ -81,7 +82,7 @@ private static class RevisionRepositoryFactory revisionEntityClass) { + public RevisionRepositoryFactory(EntityManager entityManager, @Nullable Class revisionEntityClass) { super(entityManager); diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/package-info.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/package-info.java index dd135fdacf..e021667fdb 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/package-info.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/package-info.java @@ -1,5 +1,5 @@ /** * Spring Data JPA specific converter infrastructure. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.envers.repository.support; diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index a890bccf13..13f0b11177 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -465,4 +465,89 @@ + + + nullaway + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.querydsl + querydsl-apt + ${querydsl} + jakarta + + + org.hibernate.orm + hibernate-jpamodelgen + ${hibernate} + + + org.hibernate.orm + hibernate-core + ${hibernate} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh} + + + jakarta.persistence + jakarta.persistence-api + ${jakarta-persistence-api} + + + com.google.errorprone + error_prone_core + ${errorprone} + + + com.uber.nullaway + nullaway + ${nullaway} + + + + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + + compile + + + + -XDcompilePolicy=simple + --should-stop=ifError=FLOW + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract + + + + + java-test-compile + test-compile + + testCompile + + + + + + + + + diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java index 6b2314a2d0..c27d9f8804 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -34,6 +34,8 @@ import java.util.Set; import org.springframework.dao.InvalidDataAccessApiUsageException; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.ExampleMatcher.MatchMode; @@ -41,7 +43,6 @@ import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -81,8 +82,7 @@ public class QueryByExamplePredicateBuilder { * @param example must not be {@literal null}. * @return {@literal null} indicates no {@link Predicate}. */ - @Nullable - public static Predicate getPredicate(Root root, CriteriaBuilder cb, Example example) { + public static @Nullable Predicate getPredicate(Root root, CriteriaBuilder cb, Example example) { return getPredicate(root, cb, example, EscapeCharacter.DEFAULT); } @@ -95,8 +95,7 @@ public static Predicate getPredicate(Root root, CriteriaBuilder cb, Examp * @param escapeCharacter Must not be {@literal null}. * @return {@literal null} indicates no constraints */ - @Nullable - public static Predicate getPredicate(Root root, CriteriaBuilder cb, Example example, + public static @Nullable Predicate getPredicate(Root root, CriteriaBuilder cb, Example example, EscapeCharacter escapeCharacter) { Assert.notNull(root, "Root must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/package-info.java index 8b3213871e..1090103cb7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/package-info.java @@ -1,5 +1,5 @@ /** * Spring Data JPA specific converter infrastructure. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.convert; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConverters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConverters.java index 87aeb9353e..12f29500eb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConverters.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConverters.java @@ -27,6 +27,9 @@ import java.util.Date; import org.springframework.data.convert.Jsr310Converters.DateToLocalDateConverter; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.Jsr310Converters.DateToLocalDateTimeConverter; import org.springframework.data.convert.Jsr310Converters.DateToLocalTimeConverter; import org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConverter; @@ -36,8 +39,6 @@ import org.springframework.data.convert.Jsr310Converters.ZoneIdToStringConverter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; /** @@ -53,81 +54,71 @@ public class Jsr310JpaConverters { @Converter(autoApply = true) - public static class LocalDateConverter implements AttributeConverter { + public static class LocalDateConverter implements AttributeConverter<@Nullable LocalDate, @Nullable Date> { - @Nullable @Override - public Date convertToDatabaseColumn(LocalDate date) { + public @Nullable Date convertToDatabaseColumn(@Nullable LocalDate date) { return date == null ? null : LocalDateToDateConverter.INSTANCE.convert(date); } - @Nullable @Override - public LocalDate convertToEntityAttribute(Date date) { + public @Nullable LocalDate convertToEntityAttribute(@Nullable Date date) { return date == null ? null : DateToLocalDateConverter.INSTANCE.convert(date); } } @Converter(autoApply = true) - public static class LocalTimeConverter implements AttributeConverter { + public static class LocalTimeConverter implements AttributeConverter<@Nullable LocalTime, @Nullable Date> { - @Nullable @Override - public Date convertToDatabaseColumn(LocalTime time) { + public @Nullable Date convertToDatabaseColumn(@Nullable LocalTime time) { return time == null ? null : LocalTimeToDateConverter.INSTANCE.convert(time); } - @Nullable @Override - public LocalTime convertToEntityAttribute(Date date) { + public @Nullable LocalTime convertToEntityAttribute(@Nullable Date date) { return date == null ? null : DateToLocalTimeConverter.INSTANCE.convert(date); } } @Converter(autoApply = true) - public static class LocalDateTimeConverter implements AttributeConverter { + public static class LocalDateTimeConverter implements AttributeConverter<@Nullable LocalDateTime, @Nullable Date> { - @Nullable @Override - public Date convertToDatabaseColumn(LocalDateTime date) { + public @Nullable Date convertToDatabaseColumn(@Nullable LocalDateTime date) { return date == null ? null : LocalDateTimeToDateConverter.INSTANCE.convert(date); } - @Nullable @Override - public LocalDateTime convertToEntityAttribute(Date date) { + public @Nullable LocalDateTime convertToEntityAttribute(@Nullable Date date) { return date == null ? null : DateToLocalDateTimeConverter.INSTANCE.convert(date); } } @Converter(autoApply = true) - public static class InstantConverter implements AttributeConverter { + public static class InstantConverter implements AttributeConverter<@Nullable Instant, @Nullable Timestamp> { - @Nullable @Override - public Timestamp convertToDatabaseColumn(Instant instant) { + public @Nullable Timestamp convertToDatabaseColumn(@Nullable Instant instant) { return instant == null ? null : InstantToTimestampConverter.INSTANCE.convert(instant); } - @Nullable @Override - public Instant convertToEntityAttribute(Timestamp timestamp) { + public @Nullable Instant convertToEntityAttribute(@Nullable Timestamp timestamp) { return timestamp == null ? null : TimestampToInstantConverter.INSTANCE.convert(timestamp); } } @Converter(autoApply = true) - public static class ZoneIdConverter implements AttributeConverter { + public static class ZoneIdConverter implements AttributeConverter<@Nullable ZoneId, @Nullable String> { - @Nullable @Override - public String convertToDatabaseColumn(ZoneId zoneId) { + public @Nullable String convertToDatabaseColumn(@Nullable ZoneId zoneId) { return zoneId == null ? null : ZoneIdToStringConverter.INSTANCE.convert(zoneId); } - @Nullable @Override - public ZoneId convertToEntityAttribute(String zoneId) { + public @Nullable ZoneId convertToEntityAttribute(@Nullable String zoneId) { return zoneId == null ? null : StringToZoneIdConverter.INSTANCE.convert(zoneId); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/package-info.java index 0c00cdf218..716d2fe999 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/threeten/package-info.java @@ -1,5 +1,5 @@ /** * Spring Data JPA specific JSR-310 converters. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.convert.threeten; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java index 21637a9be9..8f93ab0fc6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java @@ -25,7 +25,8 @@ import java.util.Optional; import org.springframework.data.domain.Auditable; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * Abstract base class for auditable entities. Stores the audition values in persistent fields. @@ -37,18 +38,23 @@ * @param the type of the auditing type's identifier. */ @MappedSuperclass +@SuppressWarnings("NullAway") public abstract class AbstractAuditable extends AbstractPersistable implements Auditable { +// @Nullable @ManyToOne // - private @Nullable U createdBy; + private U createdBy; - private @Nullable Instant createdDate; +// @Nullable + private Instant createdDate; +// @Nullable @ManyToOne // - private @Nullable U lastModifiedBy; + private U lastModifiedBy; - private @Nullable Instant lastModifiedDate; +// @Nullable + private Instant lastModifiedDate; @Override public Optional getCreatedBy() { @@ -56,7 +62,7 @@ public Optional getCreatedBy() { } @Override - public void setCreatedBy(U createdBy) { + public void setCreatedBy(@Nullable U createdBy) { this.createdBy = createdBy; } @@ -77,7 +83,7 @@ public Optional getLastModifiedBy() { } @Override - public void setLastModifiedBy(U lastModifiedBy) { + public void setLastModifiedBy(@Nullable U lastModifiedBy) { this.lastModifiedBy = lastModifiedBy; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java index 989eac2cff..0d645c1519 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java @@ -18,13 +18,14 @@ import java.io.Serializable; import jakarta.persistence.GeneratedValue; + +import org.jspecify.annotations.Nullable; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.Transient; import org.springframework.data.domain.Persistable; import org.springframework.data.util.ProxyUtils; -import org.springframework.lang.Nullable; /** * Abstract base class for entities. Allows parameterization of id type, chooses auto-generation and implements @@ -38,12 +39,16 @@ * @param the type of the identifier. */ @MappedSuperclass -public abstract class AbstractPersistable implements Persistable { - @Id @GeneratedValue private @Nullable PK id; +public abstract class AbstractPersistable implements Persistable { @Nullable + @Id @GeneratedValue private PK id; + @Override + @SuppressWarnings("NullAway") + // TODO: Querydsl APT does not like @Nullable + // -> errors with cryptic 'Did not find type @org.jspecify.annotations.Nullable PK' public PK getId() { return id; } @@ -74,7 +79,7 @@ public String toString() { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (null == obj) { return false; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index 3337ae5fb1..bd6911df9c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -25,8 +25,9 @@ import java.util.stream.StreamSupport; import org.springframework.lang.CheckReturnValue; + +import org.jspecify.annotations.Nullable; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java index 89e4f35bfa..4fc0f813aa 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java @@ -26,7 +26,8 @@ import java.util.List; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index 5ed394ad1d..5d9bd51065 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -24,8 +24,9 @@ import java.util.stream.StreamSupport; import org.springframework.lang.CheckReturnValue; + +import org.jspecify.annotations.Nullable; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index b0b44dc0f6..f0c782d7a7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -26,8 +26,9 @@ import java.util.stream.StreamSupport; import org.springframework.lang.CheckReturnValue; + +import org.jspecify.annotations.Nullable; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -231,6 +232,6 @@ static Specification anyOf(Iterable> specifications) { * @return a {@link Predicate}, may be {@literal null}. */ @Nullable - Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder); + Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder criteriaBuilder); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java index 0b6e90014c..5600b40f58 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java @@ -24,7 +24,8 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; /** * Helper class to support specification compositions. @@ -39,7 +40,8 @@ class SpecificationComposition { interface Combiner extends Serializable { - Predicate combine(CriteriaBuilder builder, @Nullable Predicate lhs, @Nullable Predicate rhs); + @Nullable + Predicate combine(CriteriaBuilder builder, Predicate lhs, Predicate rhs); } static Specification composed(@Nullable Specification lhs, @Nullable Specification rhs, @@ -58,12 +60,13 @@ static Specification composed(@Nullable Specification lhs, @Nullable S }; } - @Nullable - private static Predicate toPredicate(@Nullable Specification specification, Root root, + private static @Nullable Predicate toPredicate(@Nullable Specification specification, Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { return specification == null ? null : specification.toPredicate(root, query, builder); } + @Contract("_, _, !null -> new") + @SuppressWarnings("NullAway") static DeleteSpecification composed(@Nullable DeleteSpecification lhs, @Nullable DeleteSpecification rhs, Combiner combiner) { @@ -80,10 +83,10 @@ static DeleteSpecification composed(@Nullable DeleteSpecification lhs, }; } - @Nullable - private static Predicate toPredicate(@Nullable DeleteSpecification specification, Root root, + private static @Nullable Predicate toPredicate(@Nullable DeleteSpecification specification, Root root, @Nullable CriteriaDelete delete, CriteriaBuilder builder) { - return specification == null ? null : specification.toPredicate(root, delete, builder); + + return specification == null || delete == null ? null : specification.toPredicate(root, delete, builder); } static UpdateSpecification composed(@Nullable UpdateSpecification lhs, @Nullable UpdateSpecification rhs, @@ -102,8 +105,8 @@ static UpdateSpecification composed(@Nullable UpdateSpecification lhs, }; } - @Nullable - private static Predicate toPredicate(@Nullable UpdateSpecification specification, Root root, + + private static @Nullable Predicate toPredicate(@Nullable UpdateSpecification specification, Root root, CriteriaUpdate update, CriteriaBuilder builder) { return specification == null ? null : specification.toPredicate(root, update, builder); } @@ -124,8 +127,7 @@ static PredicateSpecification composed(PredicateSpecification lhs, Pre }; } - @Nullable - private static Predicate toPredicate(@Nullable PredicateSpecification specification, Root root, + private static @Nullable Predicate toPredicate(@Nullable PredicateSpecification specification, Root root, CriteriaBuilder builder) { return specification == null ? null : specification.toPredicate(root, builder); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java index 2e9d93b82a..9b4b9f5e4d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -25,8 +25,9 @@ import java.util.stream.StreamSupport; import org.springframework.lang.CheckReturnValue; + +import org.jspecify.annotations.Nullable; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/package-info.java index 46adc19c0a..2ee320ed30 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/package-info.java @@ -1,5 +1,5 @@ /** * JPA specific support classes to implement domain classes. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.domain; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/AuditingEntityListener.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/AuditingEntityListener.java index 9dc73af957..9c0fe493ca 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/AuditingEntityListener.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/AuditingEntityListener.java @@ -19,10 +19,11 @@ import jakarta.persistence.PreUpdate; import org.springframework.beans.factory.ObjectFactory; + +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Configurable; import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.domain.Auditable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/package-info.java index d14b03294c..b18b18cf18 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/support/package-info.java @@ -1,5 +1,5 @@ /** * Implementation classes for auditing with JPA. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.domain.support; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContext.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContext.java index bc5a71c25c..2c0b813370 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContext.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContext.java @@ -22,6 +22,8 @@ import java.util.function.Predicate; import org.springframework.data.jpa.provider.PersistenceProvider; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.context.AbstractMappingContext; @@ -29,7 +31,6 @@ import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -114,8 +115,7 @@ private Metamodels(Set metamodels) { * @param type must not be {@literal null}. * @return */ - @Nullable - public JpaMetamodel getMetamodel(TypeInformation type) { + public @Nullable JpaMetamodel getMetamodel(TypeInformation type) { Metamodel metamodel = getMetamodelFor(type.getType()); @@ -166,8 +166,7 @@ public boolean isMetamodelManagedType(Class type) { * @param type must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - private Metamodel getMetamodelFor(Class type) { + private @Nullable Metamodel getMetamodelFor(Class type) { for (Metamodel model : metamodels) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentEntityImpl.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentEntityImpl.java index 761a1600d0..611c82dff5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentEntityImpl.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentEntityImpl.java @@ -17,6 +17,7 @@ import java.util.Comparator; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Version; import org.springframework.data.jpa.provider.ProxyIdAccessor; import org.springframework.data.jpa.util.JpaMetamodel; @@ -63,7 +64,7 @@ public JpaPersistentEntityImpl(TypeInformation information, ProxyIdAccessor p } @Override - protected JpaPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(JpaPersistentProperty property) { + protected @Nullable JpaPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(JpaPersistentProperty property) { return property.isIdProperty() ? property : null; } @@ -117,7 +118,7 @@ private static class JpaProxyAwareIdentifierAccessor extends IdPropertyIdentifie } @Override - public Object getIdentifier() { + public @Nullable Object getIdentifier() { return proxyIdAccessor.shouldUseAccessorFor(bean) // ? proxyIdAccessor.getIdentifierFrom(bean)// diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java index a63252f8db..38678add2b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java @@ -25,6 +25,8 @@ import java.util.Set; import org.springframework.core.annotation.AnnotationUtils; + +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.AccessType.Type; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.mapping.Association; @@ -34,7 +36,6 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -170,7 +171,7 @@ public boolean isEmbeddable() { } @Override - public TypeInformation getAssociationTargetTypeInformation() { + public @Nullable TypeInformation getAssociationTargetTypeInformation() { if (!isAssociation()) { return null; @@ -193,8 +194,7 @@ public TypeInformation getAssociationTargetTypeInformation() { * * @return */ - @Nullable - private Boolean detectPropertyAccess() { + private @Nullable Boolean detectPropertyAccess() { org.springframework.data.annotation.AccessType accessType = findAnnotation( org.springframework.data.annotation.AccessType.class); @@ -229,8 +229,7 @@ private Boolean detectPropertyAccess() { * * @return */ - @Nullable - private TypeInformation detectAssociationTargetType() { + private @Nullable TypeInformation detectAssociationTargetType() { if (!isAssociation()) { return null; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/package-info.java index 0139f824dc..a16f60cc6f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/package-info.java @@ -1,5 +1,5 @@ /** * JPA specific support classes for the Spring Data mapping subsystem. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.mapping; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java index 4f85f48a62..037c3c5eb3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/projection/package-info.java @@ -1,5 +1,5 @@ /** * JPA specific support projection support. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.projection; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java index 862cb5a1fb..414d8d5952 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java @@ -17,7 +17,7 @@ import org.hibernate.query.Query; import org.hibernate.query.spi.SqmQuery; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility functions to work with Hibernate. Mostly using reflection to make sure common functionality can be executed @@ -41,8 +41,7 @@ private HibernateUtils() {} * @param query * @return */ - @Nullable - public static String getHibernateQuery(Object query) { + public @Nullable static String getHibernateQuery(Object query) { try { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java index f00f4b849d..f6ea036c2b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java @@ -18,8 +18,9 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.Metamodel; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; + +import org.jspecify.annotations.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 2b5e0abbeb..14a8db9dcc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -36,8 +36,9 @@ import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.proxy.HibernateProxy; +import org.jspecify.annotations.Nullable; + import org.springframework.data.util.CloseableIterator; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -68,7 +69,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) { @Override - public String extractQueryString(Query query) { + public @Nullable String extractQueryString(Query query) { return HibernateUtils.getHibernateQuery(query); } @@ -127,9 +128,8 @@ public boolean shouldUseAccessorFor(Object entity) { return false; } - @Nullable @Override - public Object getIdentifierFrom(Object entity) { + public @Nullable Object getIdentifierFrom(Object entity) { return null; } @@ -154,9 +154,8 @@ public String getCommentHintValue(String comment) { */ GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { - @Nullable @Override - public String extractQueryString(Query query) { + public @Nullable String extractQueryString(Query query) { return null; } @@ -170,20 +169,18 @@ public boolean shouldUseAccessorFor(Object entity) { return false; } - @Nullable @Override - public Object getIdentifierFrom(Object entity) { + public @Nullable Object getIdentifierFrom(Object entity) { return null; } - @Nullable @Override - public String getCommentHintKey() { + public @Nullable String getCommentHintKey() { return null; } }; - private static final Class typedParameterValueClass; + private static final @Nullable Class typedParameterValueClass; static { @@ -334,8 +331,7 @@ public boolean canExtractQuery() { * @return the original value or null. * @since 3.0 */ - @Nullable - public static Object unwrapTypedParameterValue(@Nullable Object value) { + public static @Nullable Object unwrapTypedParameterValue(@Nullable Object value) { return typedParameterValueClass != null && typedParameterValueClass.isInstance(value) // ? null // diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/ProxyIdAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/ProxyIdAccessor.java index d999d7490b..e550368876 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/ProxyIdAccessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/ProxyIdAccessor.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.provider; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface for a persistence provider specific accessor of identifiers held in proxies. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryComment.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryComment.java index aa39144da4..aa6c64abaf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryComment.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryComment.java @@ -17,7 +17,7 @@ import jakarta.persistence.Query; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to hide different implementations of query hints that insert comments into a {@link Query}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java index 6bd6f4bace..b9be1da3bf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java @@ -17,7 +17,7 @@ import jakarta.persistence.Query; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to hide different implementations to extract the original JPA query string from a {@link Query}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/package-info.java index 02605bbf3d..87977ed2ce 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/package-info.java @@ -1,5 +1,5 @@ /** * JPA provider-specific utilities. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.provider; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index 3712be9561..ffd6f55529 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -31,6 +31,8 @@ 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; @@ -40,7 +42,6 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.UpdateSpecification; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.lang.Nullable; /** * Interface to allow execution of {@link Specification}s based on the JPA criteria API. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRuntimeHints.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRuntimeHints.java index 80b67fd896..3b00237d29 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRuntimeHints.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRuntimeHints.java @@ -22,6 +22,8 @@ import java.util.List; import org.springframework.aot.hint.ExecutableMode; + +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -36,7 +38,6 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.QuerydslUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/package-info.java index c5fb3792d5..a186187b38 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/cdi/package-info.java @@ -1,5 +1,5 @@ /** * CDI support for Spring Data JPA Repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.repository.cdi; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/AuditingBeanDefinitionParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/AuditingBeanDefinitionParser.java index 8625119632..53ec098e86 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/AuditingBeanDefinitionParser.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/AuditingBeanDefinitionParser.java @@ -17,6 +17,7 @@ import static org.springframework.beans.factory.support.BeanDefinitionBuilder.*; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -45,6 +46,7 @@ public class AuditingBeanDefinitionParser implements BeanDefinitionParser { private final SpringConfiguredBeanDefinitionParser springConfiguredParser = new SpringConfiguredBeanDefinitionParser(); @Override + @SuppressWarnings("NullAway") public BeanDefinition parse(Element element, ParserContext parser) { springConfiguredParser.parse(element, parser); @@ -90,7 +92,7 @@ private static class SpringConfiguredBeanDefinitionParser implements BeanDefinit private static final String BEAN_CONFIGURER_ASPECT_CLASS_NAME = "org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"; @Override - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaMetamodelMappingContextFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaMetamodelMappingContextFactoryBean.java index 2bd8cd5ec8..9ccfa3f038 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaMetamodelMappingContextFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaMetamodelMappingContextFactoryBean.java @@ -23,6 +23,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.FactoryBean; @@ -32,7 +34,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.data.util.StreamUtils; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} to setup {@link JpaMetamodelMappingContext} instances from Spring configuration. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 1bcf8073a8..6366a8d5db 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -34,6 +34,8 @@ import java.util.Set; import org.springframework.aot.generate.GenerationContext; + +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -56,7 +58,6 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -116,7 +117,9 @@ public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSo Optional transactionManagerRef = source.getAttribute("transactionManagerRef"); builder.addPropertyValue("transactionManager", transactionManagerRef.orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); - builder.addPropertyReference("entityManager", entityManagerRefs.get(source)); + if(entityManagerRefs.containsKey(source)) { + builder.addPropertyReference("entityManager", entityManagerRefs.get(source)); + } builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\')); builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME); } @@ -228,13 +231,13 @@ private String registerSharedEntityMangerIfNotAlreadyRegistered(BeanDefinitionRe } @Override - protected ClassLoader getConfigurationInspectionClassLoader(ResourceLoader loader) { + protected @Nullable ClassLoader getConfigurationInspectionClassLoader(ResourceLoader loader) { ClassLoader classLoader = loader.getClassLoader(); return classLoader != null && LazyJvmAgent.isActive(loader.getClassLoader()) - ? new InspectionClassLoader(loader.getClassLoader()) - : loader.getClassLoader(); + ? new InspectionClassLoader(classLoader) + : classLoader; } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/package-info.java index 6e54455cfe..e2186fa63a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/package-info.java @@ -1,5 +1,5 @@ /** * Classes for JPA namespace configuration. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.repository.config; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/package-info.java index 61ce846166..702e410e85 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/package-info.java @@ -1,5 +1,5 @@ /** * Interfaces and annotations for JPA specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.repository; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 641c16190d..851c40a55f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -35,6 +35,8 @@ import java.util.stream.Collectors; import org.springframework.beans.BeanUtils; + +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.convert.converter.Converter; import org.springframework.data.jpa.provider.PersistenceProvider; @@ -55,7 +57,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.Lazy; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -141,9 +142,8 @@ protected JpaMetamodel getMetamodel() { return metamodel; } - @Nullable @Override - public Object execute(Object[] parameters) { + public @Nullable Object execute(Object[] parameters) { return doExecute(getExecution(), parameters); } @@ -152,8 +152,7 @@ public Object execute(Object[] parameters) { * @param values * @return */ - @Nullable - private Object doExecute(JpaQueryExecution execution, Object[] values) { + private @Nullable Object doExecute(JpaQueryExecution execution, Object[] values) { JpaParametersParameterAccessor accessor = obtainParameterAccessor(values); Object result = execution.execute(this, accessor); @@ -193,6 +192,7 @@ protected JpaQueryExecution getExecution() { * @param query * @return */ + @SuppressWarnings("NullAway") protected T applyHints(T query, JpaQueryMethod method) { List hints = method.getHints(); @@ -283,8 +283,7 @@ protected Query createCountQuery(JpaParametersParameterAccessor values) { * @return * @since 2.0.5 */ - @Nullable - protected Class getTypeToRead(ReturnedType returnedType) { + protected @Nullable Class getTypeToRead(ReturnedType returnedType) { if (PersistenceProvider.ECLIPSELINK.equals(provider)) { return null; @@ -525,8 +524,7 @@ public boolean containsValue(Object value) { * @return the value of the backing {@link Tuple} for that key or {@code null}. */ @Override - @Nullable - public Object get(Object key) { + public @Nullable Object get(Object key) { if (!(key instanceof String)) { return null; 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 73ec879ddd..a7b0b04e42 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 @@ -21,6 +21,8 @@ import java.util.Objects; import org.springframework.data.domain.Pageable; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.jpa.repository.QueryRewriter; @@ -28,7 +30,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; import org.springframework.util.StringUtils; @@ -237,7 +238,7 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter { - private volatile String cachedQueryString; + private volatile @Nullable String cachedQueryString; public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { @@ -263,7 +264,7 @@ class CachingQuerySortRewriter implements QuerySortRewriter { private final ConcurrentLruCache queryCache = new ConcurrentLruCache<>(16, AbstractStringBasedJpaQuery.this::applySorting); - private volatile String cachedQueryString; + private volatile @Nullable String cachedQueryString; @Override public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BadJpqlGrammarException.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BadJpqlGrammarException.java index ab3b51b7b3..e731d9f3bc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BadJpqlGrammarException.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BadJpqlGrammarException.java @@ -15,8 +15,9 @@ */ package org.springframework.data.jpa.repository.query; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.lang.Nullable; /** * An exception thrown if the JPQL query is invalid. @@ -29,12 +30,12 @@ public class BadJpqlGrammarException extends InvalidDataAccessResourceUsageExcep private final String jpql; - public BadJpqlGrammarException(String message, String jpql, @Nullable Throwable cause) { + public BadJpqlGrammarException(@Nullable String message, String jpql, @Nullable Throwable cause) { this(message, jpql, "JPQL", cause); } - BadJpqlGrammarException(String message, String grammar, String jpql, @Nullable Throwable cause) { - super(message + "; Bad " + grammar + " grammar [" + jpql + "]", cause); + BadJpqlGrammarException(@Nullable String message, String grammar, String jpql, @Nullable Throwable cause) { + super("%sBad %s grammar [%s]".formatted(message != null ? message + "; " : "", grammar, jpql), cause); this.jpql = jpql; } 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 70bc5c829b..0e6f760ed3 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 @@ -17,9 +17,10 @@ import java.util.List; -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. * 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 8dba004f4b..1fe6236621 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 @@ -18,7 +18,8 @@ import java.util.Set; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * The implementation of the Regex-based {@link QueryEnhancer} using {@link QueryUtils}. @@ -30,7 +31,7 @@ public class DefaultQueryEnhancer implements QueryEnhancer { private final DeclaredQuery query; private final boolean hasConstructorExpression; - private final String alias; + private final @Nullable String alias; private final String projection; private final Set joinAliases; @@ -68,7 +69,7 @@ public boolean hasConstructorExpression() { } @Override - public String detectAlias() { + public @Nullable String detectAlias() { return this.alias; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java index 4593697a4d..d57a83ab99 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java @@ -57,7 +57,7 @@ public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> { QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder(); - prop.append(QueryTokens.token(selectionList.getFirst().value())); + prop.append(QueryTokens.token(selectionList.getRequiredFirst().value())); prop.append(QueryTokens.TOKEN_DOT); prop.append(QueryTokens.token(property)); 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 850c0919a3..95693e8808 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 @@ -18,7 +18,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * NULL-Object pattern implementation for {@link DeclaredQuery}. @@ -44,7 +44,7 @@ public String getQueryString() { } @Override - public String getAlias() { + public @Nullable String getAlias() { return null; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyQueryTokenStream.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyQueryTokenStream.java index db498281fc..1b05738d5e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyQueryTokenStream.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyQueryTokenStream.java @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.Iterator; +import org.jspecify.annotations.Nullable; + /** * Empty QueryTokenStream. * @@ -31,12 +33,12 @@ class EmptyQueryTokenStream implements QueryTokenStream { private EmptyQueryTokenStream() {} @Override - public QueryToken getFirst() { + public @Nullable QueryToken getFirst() { return null; } @Override - public QueryToken getLast() { + public @Nullable QueryToken getLast() { return null; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java index 81b5e9a8f6..2d8e27c167 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java @@ -18,8 +18,9 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; -import org.springframework.lang.Nullable; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query into a @@ -30,7 +31,7 @@ * @author Christoph Strobl * @since 3.4 */ -@SuppressWarnings("ConstantValue") +@SuppressWarnings({ "ConstantValue", "NullAway" }) class EqlCountQueryTransformer extends EqlQueryRenderer { private final @Nullable String countProjection; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java index 0f006f2388..fa7fa5ec8e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java @@ -22,7 +22,8 @@ import java.util.List; import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * {@link ParsedQueryIntrospector} for EQL queries. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java index 50a3019acc..30e9106d22 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java @@ -20,9 +20,10 @@ import java.util.List; import org.springframework.data.domain.Sort; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java index d73680ff62..448b80bad1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A value type encapsulating an escape character for LIKE queries and the actually usage of it in escaping @@ -49,8 +49,7 @@ public static EscapeCharacter of(char escapeCharacter) { * @param value may be {@literal null}. * @return */ - @Nullable - public String escape(@Nullable String value) { + public @Nullable String escape(@Nullable String value) { return value == null // ? null // 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 3007f494ca..a414b52005 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 @@ -18,6 +18,8 @@ import java.util.Objects; import java.util.regex.Pattern; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; import org.springframework.data.expression.ValueExpressionParser; @@ -52,6 +54,12 @@ class ExpressionBasedStringQuery extends StringQuery { private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME; private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE; + private static final Environment DEFAULT_ENVIRONMENT; + + static { + DEFAULT_ENVIRONMENT = new StandardEnvironment(); + } + /** * Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link EntityMetadata}. * @@ -102,7 +110,8 @@ private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEnti ValueExpression expr = parser.parse(query); - String result = Objects.toString(expr.evaluate(ValueEvaluationContext.of(null, evalContext))); + String result = Objects.toString( + expr.evaluate(ValueEvaluationContext.of(DEFAULT_ENVIRONMENT, evalContext))); if (result == null) { return query; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java index 6020c50fa1..af1c4fa0ec 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java @@ -21,10 +21,11 @@ import org.hibernate.query.TypedParameterValue; import org.hibernate.type.BasicType; import org.hibernate.type.BasicTypeRegistry; +import org.jspecify.annotations.Nullable; + import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.lang.Nullable; /** * {@link org.springframework.data.repository.query.ParameterAccessor} based on an {@link Parameters} instance. In @@ -62,9 +63,8 @@ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAcce } @Override - @Nullable @SuppressWarnings("unchecked") - public Object getValue(Parameter parameter) { + public @Nullable Object getValue(Parameter parameter) { Object value = super.getValue(parameter.getIndex()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java index 755dade914..405fa08660 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java @@ -17,7 +17,7 @@ import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Hibernate-specific query details capturing common table expression details. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java index c85ec88bda..b048b6601d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java @@ -18,9 +18,10 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; import org.springframework.data.jpa.repository.query.HqlParser.SelectClauseContext; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; -import org.springframework.lang.Nullable; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query into a diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java index e5915f19e3..1d73d078f3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitor.java @@ -45,6 +45,7 @@ import org.antlr.v4.runtime.tree.TerminalNode; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.mapping.PropertyPath; @@ -58,7 +59,7 @@ * @author Mark Paluch * @since 4.0 */ -@SuppressWarnings({ "unchecked", "rawtypes", "ConstantValue" }) +@SuppressWarnings({ "unchecked", "rawtypes", "ConstantValue", "NullAway" }) class HqlOrderExpressionVisitor extends HqlBaseVisitor> { private static final DateTimeFormatter DATE_TIME = new DateTimeFormatterBuilder().parseCaseInsensitive() @@ -119,7 +120,7 @@ Expression createCriteriaExpression(Sort.Order jpaOrder) { } @Override - public Expression visitSortExpression(HqlParser.SortExpressionContext ctx) { + public @Nullable Expression visitSortExpression(HqlParser.SortExpressionContext ctx) { if (ctx.identifier() != null) { HqlParser.IdentifierContext identifier = ctx.identifier(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java index 5ccc7b3556..d3ba055bb9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java @@ -22,7 +22,8 @@ import java.util.List; import org.springframework.data.jpa.repository.query.HqlParser.VariableContext; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * {@link ParsedQueryIntrospector} for HQL queries. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java index 202a2107b4..9a3220dd1f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java @@ -20,9 +20,10 @@ import java.util.List; import org.springframework.data.domain.Sort; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; 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 99aec3ddf8..5600043375 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 @@ -38,6 +38,7 @@ import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.update.Update; +import org.jspecify.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -50,9 +51,9 @@ import java.util.StringJoiner; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.SerializationUtils; import org.springframework.util.StringUtils; @@ -76,7 +77,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { private final String projection; private final Set joinAliases; private final Set selectAliases; - private final byte[] serialized; + private final byte @Nullable[] serialized; /** * @param query the query we want to enhance. Must not be {@literal null}. @@ -92,6 +93,8 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) { this.projection = detectProjection(this.statement); this.selectAliases = Collections.unmodifiableSet(getSelectionAliases(this.statement)); this.joinAliases = Collections.unmodifiableSet(getJoinAliases(this.statement)); + byte[] tmp = SerializationUtils.serialize(this.statement); +// this.serialized = tmp != null ? tmp : new byte[0]; this.serialized = SerializationUtils.serialize(this.statement); } @@ -131,8 +134,7 @@ static T parseStatement(String sql, Class classOfT) { * * @return Might return {@literal null}. */ - @Nullable - private static String detectAlias(ParsedType parsedType, Statement statement) { + private static @Nullable String detectAlias(ParsedType parsedType, Statement statement) { if (ParsedType.MERGE.equals(parsedType)) { @@ -273,7 +275,7 @@ public boolean hasConstructorExpression() { } @Override - public String detectAlias() { + public @Nullable String detectAlias() { return this.primaryAlias; } @@ -319,17 +321,21 @@ private String doApplySorting(Sort sort, @Nullable String alias) { return queryString; } - return applySorting((Select) deserialize(this.serialized), sort, alias); + return applySorting(deserializeRequired(this.serialized, Select.class), sort, alias); } - private String applySorting(Select selectStatement, Sort sort, @Nullable String alias) { + private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullable String alias) { if (selectStatement instanceof SetOperationList setOperationList) { return applySortingToSetOperationList(setOperationList, sort); } if (!(selectStatement instanceof PlainSelect selectBody)) { - return selectStatement.toString(); + if(selectStatement != null) { + return selectStatement.toString(); + } else { + throw new IllegalArgumentException("Select must not be null"); + } } List orderByElements = new ArrayList<>(16); @@ -363,10 +369,10 @@ public String createCountQueryFor(@Nullable String countProjection) { return this.query.getQueryString(); } - return createCountQueryFor(this.query, selectBody, countProjection, primaryAlias); + return createCountQueryFor(selectBody, countProjection, primaryAlias); } - private static String createCountQueryFor(DeclaredQuery query, PlainSelect selectBody, + private static String createCountQueryFor(PlainSelect selectBody, @Nullable String countProjection, @Nullable String primaryAlias) { // remove order by @@ -520,7 +526,10 @@ enum ParsedType { * @param bytes a serialized object * @return the result of deserializing the bytes */ - private static Object deserialize(byte[] bytes) { + private static @Nullable Object deserialize(byte @Nullable[] bytes) { + if(ObjectUtils.isEmpty(bytes)) { + return null; + } try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { return ois.readObject(); } catch (IOException ex) { @@ -530,4 +539,12 @@ private static Object deserialize(byte[] bytes) { } } + private static T deserializeRequired(byte @Nullable[] bytes, Class type) { + Object deserialize = deserialize(bytes); + if(deserialize != null) { + return type.cast(deserialize); + } + throw new IllegalStateException("Failed to deserialize object type"); + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java index 4530aac26b..ff1ce50b54 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java @@ -26,8 +26,9 @@ import java.util.List; import org.springframework.data.jpa.repository.support.MutableQueryHints; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.support.QueryHints; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -188,8 +189,7 @@ private static boolean exists(String attributeNodeName, List> n * @param parent * @return {@literal null} if not found. */ - @Nullable - private static AttributeNode findAttributeNode(String attributeNodeName, EntityGraph entityGraph, + private static @Nullable AttributeNode findAttributeNode(String attributeNodeName, EntityGraph entityGraph, @Nullable Subgraph parent) { return findAttributeNode(attributeNodeName, parent != null ? parent.getAttributeNodes() : entityGraph.getAttributeNodes()); @@ -203,8 +203,7 @@ private static AttributeNode findAttributeNode(String attributeNodeName, Enti * @param nodes * @return {@literal null} if not found. */ - @Nullable - private static AttributeNode findAttributeNode(String attributeNodeName, List> nodes) { + private static @Nullable AttributeNode findAttributeNode(String attributeNodeName, List> nodes) { for (AttributeNode node : nodes) { if (ObjectUtils.nullSafeEquals(node.getAttributeName(), attributeNodeName)) { @@ -223,8 +222,7 @@ private static AttributeNode findAttributeNode(String attributeNodeName, List * @param node * @return */ - @Nullable - private static Subgraph getSubgraph(AttributeNode node) { + private static @Nullable Subgraph getSubgraph(AttributeNode node) { return node.getSubgraphs().isEmpty() ? null : node.getSubgraphs().values().iterator().next(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java index 3a7d9421b8..cf85c65d47 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java @@ -18,8 +18,9 @@ import java.util.List; import org.springframework.data.jpa.repository.EntityGraph; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -56,7 +57,7 @@ public JpaEntityGraph(EntityGraph entityGraph, String nameFallback) { * @param attributePaths may be {@literal null}. * @since 1.9 */ - public JpaEntityGraph(String name, EntityGraphType type, @Nullable String[] attributePaths) { + public JpaEntityGraph(String name, EntityGraphType type, String @Nullable[] attributePaths) { Assert.hasText(name, "The name of an EntityGraph must not be null or empty"); Assert.notNull(type, "FetchGraphType must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java index ce0d5a5a1f..1acb62d768 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java @@ -25,12 +25,13 @@ import java.util.concurrent.atomic.AtomicInteger; import org.springframework.data.domain.KeysetScrollPosition; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.lang.Nullable; /** * Extension to {@link JpaQueryCreator} to create queries considering {@link KeysetScrollPosition keyset scrolling}. @@ -68,7 +69,7 @@ public List getBindings() { } @Override - protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) { + protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(JpqlQueryBuilder.@Nullable Predicate predicate, Sort sort) { KeysetScrollSpecification keysetSpec = new KeysetScrollSpecification<>(scrollPosition, sort, entityInformation); @@ -90,9 +91,9 @@ protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuil return query; } - @Nullable - private static JpqlQueryBuilder.Predicate getPredicate(@Nullable JpqlQueryBuilder.Predicate predicate, - @Nullable JpqlQueryBuilder.Predicate keysetPredicate) { + + private static JpqlQueryBuilder.@Nullable Predicate getPredicate(JpqlQueryBuilder.@Nullable Predicate predicate, + JpqlQueryBuilder.@Nullable Predicate keysetPredicate) { if (keysetPredicate != null) { if (predicate != null) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java index b7c49ffc64..f94f4ba8c6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParameters.java @@ -23,13 +23,14 @@ import java.util.function.Function; import org.springframework.core.MethodParameter; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.Temporal; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Custom extension of {@link Parameters} discovering additional query parameter annotations. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java index 2093e0d3d6..9d22c7bbb4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java @@ -15,11 +15,12 @@ */ package org.springframework.data.jpa.repository.query; +import org.jspecify.annotations.Nullable; + import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.lang.Nullable; /** * {@link org.springframework.data.repository.query.ParameterAccessor} based on an {@link Parameters} instance. It also @@ -48,8 +49,7 @@ public JpaParameters getParameters() { return parameters; } - @Nullable - public T getValue(Parameter parameter) { + public @Nullable T getValue(Parameter parameter) { return super.getValue(parameter.getIndex()); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 12073a595d..9a828a9b3f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -34,6 +34,8 @@ import java.util.stream.Collectors; import org.springframework.data.domain.Sort; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder; import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding; @@ -45,7 +47,6 @@ import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -142,13 +143,13 @@ protected JpqlQueryBuilder.Predicate or(JpqlQueryBuilder.Predicate base, JpqlQue * it the current {@link JpqlQueryBuilder.Predicate}. */ @Override - protected final String complete(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) { + protected final String complete(JpqlQueryBuilder.@Nullable Predicate predicate, Sort sort) { JpqlQueryBuilder.AbstractJpqlQuery query = createQuery(predicate, sort); return query.render(); } - protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(@Nullable JpqlQueryBuilder.Predicate predicate, Sort sort) { + protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(JpqlQueryBuilder.@Nullable Predicate predicate, Sort sort) { JpqlQueryBuilder.Select query = buildQuery(sort); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index d6d4cbeda1..1b57f4beb0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -32,10 +32,10 @@ import org.antlr.v4.runtime.atn.PredictionMode; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ParseTreeVisitor; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -210,7 +210,7 @@ public boolean hasConstructorExpression() { * already find the alias when generating sorted and count queries, this is mainly to serve test cases. */ @Override - public String detectAlias() { + public @Nullable String detectAlias() { return this.queryInformation.getAlias(); } @@ -267,7 +267,7 @@ public String rewrite(QueryRewriteInformation rewriteInformation) { * @return */ @Override - public String applySorting(Sort sort, String alias) { + public String applySorting(Sort sort, @Nullable String alias) { return applySorting(sort); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 1fca772ed7..961123b94e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java @@ -26,6 +26,8 @@ import java.util.Optional; import org.springframework.core.convert.ConversionService; + +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -39,7 +41,6 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.StreamUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -80,8 +81,7 @@ public abstract class JpaQueryExecution { * @param accessor must not be {@literal null}. * @return */ - @Nullable - public Object execute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { + public @Nullable Object execute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { Assert.notNull(query, "AbstractJpaQuery must not be null"); Assert.notNull(accessor, "JpaParametersParameterAccessor must not be null"); @@ -110,8 +110,7 @@ public Object execute(AbstractJpaQuery query, JpaParametersParameterAccessor acc * @param query must not be {@literal null}. * @param accessor must not be {@literal null}. */ - @Nullable - protected abstract Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor); + protected abstract @Nullable Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor); /** * Executes the query to return a simple collection of entities. @@ -142,7 +141,7 @@ static class ScrollExecution extends JpaQueryExecution { } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings("NullAway") protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { ScrollPosition scrollPosition = accessor.getScrollPosition(); @@ -212,7 +211,7 @@ private long count(AbstractJpaQuery repositoryQuery, JpaParametersParameterAcces static class SingleEntityExecution extends JpaQueryExecution { @Override - protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { + protected @Nullable Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { return query.createQuery(accessor).getSingleResultOrNull(); } @@ -327,7 +326,7 @@ static class ProcedureExecution extends JpaQueryExecution { } @Override - protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) { + protected @Nullable Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) { Assert.isInstanceOf(StoredProcedureJpaQuery.class, jpaQuery); @@ -372,10 +371,10 @@ static class StreamExecution extends JpaQueryExecution { private static final String NO_SURROUNDING_TRANSACTION = "You're trying to execute a streaming query method without a surrounding transaction that keeps the connection open so that the Stream can actually be consumed; Make sure the code consuming the stream uses @Transactional or any other way of declaring a (read-only) transaction"; - private static final Method streamMethod = ReflectionUtils.findMethod(Query.class, "getResultStream"); + private static final @Nullable Method streamMethod = ReflectionUtils.findMethod(Query.class, "getResultStream"); @Override - protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { + protected @Nullable Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { if (!SurroundingTransactionDetectorMethodInterceptor.INSTANCE.isSurroundingTransactionActive()) { throw new InvalidDataAccessApiUsageException(NO_SURROUNDING_TRANSACTION); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java index 82babfb9e4..384330af14 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java @@ -18,10 +18,11 @@ import jakarta.persistence.EntityManager; import org.springframework.data.jpa.repository.QueryRewriter; + +import org.jspecify.annotations.Nullable; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; /** * Factory to create the appropriate {@link RepositoryQuery} for a {@link JpaQueryMethod}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 317a66df94..d4b58f9429 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -21,6 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryRewriter; @@ -32,7 +33,6 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -184,8 +184,7 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer return query != null ? query : NO_QUERY; } - @Nullable - private String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) { + private @Nullable String getCountQuery(JpaQueryMethod method, NamedQueries namedQueries, EntityManager em) { if (StringUtils.hasText(method.getCountQuery())) { return method.getCountQuery(); 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 39202fcb77..afc0e0b98d 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 @@ -28,6 +28,8 @@ import java.util.function.Function; import org.springframework.core.annotation.AnnotatedElementUtils; + +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.EntityGraph; @@ -45,7 +47,6 @@ import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -301,8 +302,7 @@ public org.springframework.data.jpa.repository.query.Meta getQueryMetaAttributes * * @return */ - @Nullable - public String getAnnotatedQuery() { + public @Nullable String getAnnotatedQuery() { String query = getAnnotationValue("value", String.class); return StringUtils.hasText(query) ? query : null; @@ -340,8 +340,7 @@ public String getRequiredAnnotatedQuery() throws IllegalStateException { * * @return */ - @Nullable - public String getCountQuery() { + public @Nullable String getCountQuery() { String countQuery = getAnnotationValue("countQuery", String.class); return StringUtils.hasText(countQuery) ? countQuery : null; @@ -418,7 +417,7 @@ private T getAnnotationValue(String attribute, Class type) { return getMergedOrDefaultAnnotationValue(attribute, Query.class, type); } - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({ "rawtypes", "unchecked", "NullAway" }) private T getMergedOrDefaultAnnotationValue(String attribute, Class annotationType, Class targetType) { Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationType); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java index 6cb8f11104..79a31e556f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java @@ -9,10 +9,11 @@ import java.util.regex.Pattern; import org.springframework.dao.InvalidDataAccessApiUsageException; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.jpa.domain.JpaSort; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaResultConverters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaResultConverters.java index 06382e5e9b..9ec1c5f1e5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaResultConverters.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaResultConverters.java @@ -22,9 +22,10 @@ import java.sql.SQLException; import org.springframework.core.convert.converter.Converter; + +import org.jspecify.annotations.Nullable; import org.springframework.dao.CleanupFailureDataAccessException; import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.lang.Nullable; import org.springframework.util.StreamUtils; /** @@ -50,9 +51,9 @@ enum BlobToByteArrayConverter implements Converter { INSTANCE; - @Nullable + @Override - public byte[] convert(@Nullable Blob source) { + public byte @Nullable[] convert(@Nullable Blob source) { if (source == null) { return null; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index 289e6a5b64..480ec3426d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -18,8 +18,10 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; -import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed JPQL query into a @@ -80,8 +82,10 @@ public QueryRendererBuilder visitSelect_clause(JpqlParser.Select_clauseContext c if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); nested.append(getDistinctCountSelection(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA))); - } else { + } else if(StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); + } else { + throw new IllegalStateException("No primary alias present"); } } else { builder.append(QueryTokens.token(countProjection)); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index e99e825338..a3e8d70edc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -29,9 +29,10 @@ import java.util.function.Supplier; import org.springframework.data.domain.Sort; + +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.Predicates; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -364,8 +365,7 @@ public Predicate neq(Expression value) { }; } - @Nullable - public static Predicate and(List intermediate) { + public static @Nullable Predicate and(List intermediate) { Predicate predicate = null; @@ -381,8 +381,7 @@ public static Predicate and(List intermediate) { return predicate; } - @Nullable - public static Predicate or(List intermediate) { + public static @Nullable Predicate or(List intermediate) { Predicate predicate = null; @@ -784,8 +783,7 @@ public AbstractJpqlQuery where(Predicate predicate) { return this; } - @Nullable - public Predicate getWhere() { + public @Nullable Predicate getWhere() { return where; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java index 48f6fef46b..43f6f7fd1f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java @@ -21,7 +21,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link ParsedQueryIntrospector} for JPQL queries. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index 0b6a610614..654fb7df88 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -20,9 +20,10 @@ import java.util.List; import org.springframework.data.domain.Sort; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java index 354ce28aad..f3e20a1d6c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -26,7 +26,8 @@ import java.util.Objects; import org.springframework.data.mapping.PropertyPath; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** @@ -34,12 +35,12 @@ */ class JpqlUtils { - static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property) { return toExpressionRecursively(metamodel, source, from, property, false); } - static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property, boolean isForSelection) { return toExpressionRecursively(metamodel, source, from, property, isForSelection, false); } @@ -53,7 +54,7 @@ static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamod * @param hasRequiredOuterJoin has a parent already required an outer join? * @return the expression */ - static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { String segment = property.getSegment(); @@ -80,6 +81,10 @@ static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamod ManagedType managedTypeForModel = QueryUtils.getManagedTypeForModel(from); Attribute nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from); + if(nextAttribute == null) { + throw new IllegalStateException("Binding property is null"); + } + return toExpressionRecursively(metamodel, joinSource, (Bindable) nextAttribute, nextProperty, isForSelection, requiresOuterJoin); } @@ -96,7 +101,7 @@ static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamod * @param hasRequiredOuterJoin * @return */ - static boolean requiresOuterJoin(Metamodel metamodel, Bindable bindable, PropertyPath propertyPath, + static boolean requiresOuterJoin(@Nullable Metamodel metamodel, Bindable bindable, PropertyPath propertyPath, boolean isForSelection, boolean hasRequiredOuterJoin) { ManagedType managedType = QueryUtils.getManagedTypeForModel(bindable); @@ -127,8 +132,7 @@ static boolean requiresOuterJoin(Metamodel metamodel, Bindable bindable, Prop return hasRequiredOuterJoin || QueryUtils.getAnnotationProperty(attribute, "optional", true); } - @Nullable - private static Attribute getModelForPath(Metamodel metamodel, PropertyPath path, + private static @Nullable Attribute getModelForPath(@Nullable Metamodel metamodel, PropertyPath path, @Nullable ManagedType managedType, Bindable fallback) { String segment = path.getSegment(); @@ -140,11 +144,14 @@ static boolean requiresOuterJoin(Metamodel metamodel, Bindable bindable, Prop } } - Class fallbackType = fallback.getBindableJavaType(); - try { - return metamodel.managedType(fallbackType).getAttribute(segment); - } catch (IllegalArgumentException e) { + if(metamodel != null) { + Class fallbackType = fallback.getBindableJavaType(); + try { + return metamodel.managedType(fallbackType).getAttribute(segment); + } catch (IllegalArgumentException e) { + // nothing to do here + } } return null; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java index 0ff9902525..cfa65ccd17 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java @@ -23,11 +23,12 @@ import java.util.Map; import org.springframework.data.domain.KeysetScrollPosition; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.ScrollPosition.Direction; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.repository.support.JpaEntityInformation; -import org.springframework.lang.Nullable; /** * Delegate for keyset scrolling. @@ -69,8 +70,7 @@ public static Collection getProjectionInputProperties(JpaEntityInformati return properties; } - @Nullable - public P createPredicate(KeysetScrollPosition keyset, Sort sort, QueryStrategy strategy) { + public @Nullable P createPredicate(KeysetScrollPosition keyset, Sort sort, QueryStrategy strategy) { Map keysetValues = keyset.getKeys(); @@ -207,16 +207,16 @@ public interface QueryStrategy { * * @param order must not be {@literal null}. * @param propertyExpression must not be {@literal null}. - * @param value the value to compare with. Must not be {@literal null}. + * @param value the value to compare with. Can be {@literal null}. * @return an object representing the comparison predicate. */ - P compare(Order order, E propertyExpression, Object value); + P compare(Order order, E propertyExpression, @Nullable Object value); /** * Create an equals-comparison object. * * @param propertyExpression must not be {@literal null}. - * @param value the value to compare with. Must not be {@literal null}. + * @param value the value to compare with. Can be {@literal null}. * @return an object representing the comparison predicate. */ P compare(E propertyExpression, @Nullable Object value); @@ -227,7 +227,7 @@ public interface QueryStrategy { * @param intermediate the predicates to combine. Must not be {@literal null}. * @return a single predicate. */ - P and(List

intermediate); + @Nullable P and(List

intermediate); /** * OR-combine the {@code intermediate} predicates. @@ -235,7 +235,7 @@ public interface QueryStrategy { * @param intermediate the predicates to combine. Must not be {@literal null}. * @return a single predicate. */ - P or(List

intermediate); + @Nullable P or(List

intermediate); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index 9ef9d4e790..f39505222f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -27,13 +27,14 @@ import java.util.List; import org.springframework.data.domain.KeysetScrollPosition; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.query.KeysetScrollDelegate.QueryStrategy; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.mapping.PropertyPath; -import org.springframework.lang.Nullable; /** * {@link Specification} to create scroll queries using keyset-scrolling. @@ -67,19 +68,18 @@ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntit } @Override - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { + public @Nullable Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder criteriaBuilder) { return createPredicate(root, criteriaBuilder); } - @Nullable - public Predicate createPredicate(Root root, CriteriaBuilder criteriaBuilder) { + public @Nullable Predicate createPredicate(Root root, CriteriaBuilder criteriaBuilder) { KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); return delegate.createPredicate(position, sort, new CriteriaBuilderStrategy(root, criteriaBuilder)); } - @Nullable - public JpqlQueryBuilder.Predicate createJpqlPredicate(Bindable from, JpqlQueryBuilder.Entity entity, + + public JpqlQueryBuilder.@Nullable Predicate createJpqlPredicate(Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); @@ -106,10 +106,14 @@ public Expression createExpression(String property) { } @Override - public Predicate compare(Order order, Expression propertyExpression, Object value) { + public Predicate compare(Order order, Expression propertyExpression, @Nullable Object value) { + + if(value instanceof Comparable compareValue) { + return order.isAscending() ? cb.greaterThan(propertyExpression, compareValue) + : cb.lessThan(propertyExpression, compareValue); + } + return order.isAscending() ? cb.isNull(propertyExpression) : cb.isNotNull(propertyExpression); - return order.isAscending() ? cb.greaterThan(propertyExpression, (Comparable) value) - : cb.lessThan(propertyExpression, (Comparable) value); } @Override @@ -133,9 +137,9 @@ private static class JpqlStrategy implements QueryStrategy from; private final JpqlQueryBuilder.Entity entity; private final ParameterFactory factory; - private final Metamodel metamodel; + private final @Nullable Metamodel metamodel; - public JpqlStrategy(Metamodel metamodel, Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { + public JpqlStrategy(@Nullable Metamodel metamodel, Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { this.from = from; this.entity = entity; @@ -152,9 +156,12 @@ public JpqlQueryBuilder.Expression createExpression(String property) { @Override public JpqlQueryBuilder.Predicate compare(Order order, JpqlQueryBuilder.Expression propertyExpression, - Object value) { + @Nullable Object value) { JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression); + if(value == null) { + return order.isAscending() ? where.isNull() : where.isNotNull(); + } return order.isAscending() ? where.gt(factory.capture(value)) : where.lt(factory.capture(value)); } @@ -167,12 +174,12 @@ public JpqlQueryBuilder.Predicate compare(JpqlQueryBuilder.Expression propertyEx } @Override - public JpqlQueryBuilder.Predicate and(List intermediate) { + public JpqlQueryBuilder.@Nullable Predicate and(List intermediate) { return JpqlQueryBuilder.and(intermediate); } @Override - public JpqlQueryBuilder.Predicate or(List intermediate) { + public JpqlQueryBuilder.@Nullable Predicate or(List intermediate) { return JpqlQueryBuilder.or(intermediate); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Meta.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Meta.java index 53790bcf4f..a7e8dc35e6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Meta.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Meta.java @@ -19,8 +19,9 @@ import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; + +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** @@ -69,8 +70,7 @@ public void setComment(String comment) { /** * @return {@literal null} if not set. */ - @Nullable - public String getComment() { + public @Nullable String getComment() { return getValue(MetaKey.COMMENT.key); } @@ -106,9 +106,8 @@ void setValue(String key, @Nullable Object value) { this.values.put(key, value); } - @Nullable @SuppressWarnings("unchecked") - private T getValue(String key) { + private @Nullable T getValue(String key) { return (T) this.values.get(key); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index 4b436fd8a0..c0c133f4cf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -33,7 +34,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; /** * Implementation of {@link RepositoryQuery} based on {@link jakarta.persistence.NamedQuery}s. @@ -132,8 +132,8 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { * @param em must not be {@literal null}. * @param queryRewriter must not be {@literal null}. */ - @Nullable - public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, QueryRewriter queryRewriter) { + public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, + QueryRewriter queryRewriter) { String queryName = method.getNamedQueryName(); @@ -198,7 +198,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc } @Override - protected Class getTypeToRead(ReturnedType returnedType) { + protected @Nullable Class getTypeToRead(ReturnedType returnedType) { if (getQueryMethod().isNativeQuery()) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 9221cc3807..ae240942d5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -20,6 +20,8 @@ import jakarta.persistence.Tuple; import org.springframework.core.annotation.MergedAnnotation; + +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -28,7 +30,6 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -71,7 +72,7 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin } @Override - protected Query createJpaQuery(String queryString, Sort sort, Pageable pageable, ReturnedType returnedType) { + protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable, ReturnedType returnedType) { EntityManager em = getEntityManager(); String query = potentiallyRewriteQuery(queryString, sort, pageable); @@ -84,8 +85,7 @@ protected Query createJpaQuery(String queryString, Sort sort, Pageable pageable, return type == null ? em.createNativeQuery(query) : em.createNativeQuery(query, type); } - @Nullable - private Class getTypeToQueryFor(ReturnedType returnedType) { + private @Nullable Class getTypeToQueryFor(ReturnedType returnedType) { Class result = queryForEntity ? returnedType.getDomainType() : null; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index b68ac78c83..dda3211cd9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -26,12 +26,14 @@ import java.util.stream.Collectors; import org.springframework.data.expression.ValueExpression; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part.Type; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -75,8 +77,7 @@ public ParameterOrigin getOrigin() { /** * @return the name if available or {@literal null}. */ - @Nullable - public String getName() { + public @Nullable String getName() { return identifier.hasName() ? identifier.getName() : null; } @@ -150,8 +151,7 @@ public String toString() { /** * @param valueToBind value to prepare */ - @Nullable - public Object prepare(@Nullable Object valueToBind) { + public @Nullable Object prepare(@Nullable Object valueToBind) { return valueToBind; } @@ -234,7 +234,7 @@ public boolean isIsNullParameter() { } @Override - public Object prepare(@Nullable Object value) { + public @Nullable Object prepare(@Nullable Object value) { if (value == null || parameterType == null) { return value; @@ -255,9 +255,10 @@ public Object prepare(@Nullable Object value) { : value; } - @Nullable + @SuppressWarnings("unchecked") - private Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { + @Contract("false, _ -> param2; _, null -> null; true, !null -> new)") + private @Nullable Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { if (!ignoreCase || CollectionUtils.isEmpty(collection)) { return collection; @@ -278,8 +279,7 @@ private Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collec * @param value the value to be converted to a {@link Collection}. * @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value. */ - @Nullable - private static Collection toCollection(@Nullable Object value) { + private static @Nullable Collection toCollection(@Nullable Object value) { if (value == null) { return null; @@ -316,7 +316,7 @@ static class InParameterBinding extends ParameterBinding { } @Override - public Object prepare(@Nullable Object value) { + public @Nullable Object prepare(@Nullable Object value) { if (!ObjectUtils.isArray(value)) { return value; @@ -378,9 +378,8 @@ public Type getType() { /** * Extracts the raw value properly. */ - @Nullable @Override - public Object prepare(@Nullable Object value) { + public @Nullable Object prepare(@Nullable Object value) { Object unwrapped = PersistenceProvider.unwrapTypedParameterValue(value); if (unwrapped == null) { @@ -657,8 +656,10 @@ static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Int identifier = BindingIdentifier.of(name, position); } else if (!ObjectUtils.isEmpty(name)) { identifier = BindingIdentifier.of(name); - } else { + } else if (position != null) { identifier = BindingIdentifier.of(position); + } else { + throw new IllegalStateException("Neither name nor position available for binding"); } return ofParameter(identifier); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java index 65d3538d04..5071e23ff4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java @@ -28,6 +28,8 @@ import java.util.stream.Collectors; import org.springframework.data.jpa.provider.PersistenceProvider; + +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -36,7 +38,6 @@ import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -264,8 +265,7 @@ public boolean isIsNullParameter() { * * @param value can be {@literal null}. */ - @Nullable - public Object prepare(@Nullable Object value) { + public @Nullable Object prepare(@Nullable Object value) { if (value == null || parameterType == null) { return value; @@ -294,8 +294,7 @@ public Object prepare(@Nullable Object value) { * @param value the value to be converted to a {@link Collection}. * @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value. */ - @Nullable - private static Collection toCollection(@Nullable Object value) { + private static @Nullable Collection toCollection(@Nullable Object value) { if (value == null) { return null; @@ -314,9 +313,8 @@ private static Collection toCollection(@Nullable Object value) { return Collections.singleton(value); } - @Nullable @SuppressWarnings("unchecked") - private Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { + private @Nullable Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { if (!ignoreCase || CollectionUtils.isEmpty(collection)) { return collection; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index e5107ee7c2..66dac47929 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -24,6 +24,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +44,6 @@ import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -246,7 +246,7 @@ public Query createQuery(JpaParametersParameterAccessor accessor) { * Restricts the max results of the given {@link Query} if the current {@code tree} marks this {@code query} as * limited. */ - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) private Query restrictMaxResultsIfNecessary(Query query, @Nullable ScrollPosition scrollPosition) { if (scrollPosition instanceof OffsetScrollPosition offset && !offset.isInitial()) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java index 21bead5d27..707ee20518 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeQueryCache.java @@ -22,7 +22,8 @@ import java.util.Objects; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java index 0cef0b0a0f..a2f9546d59 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ProcedureParameter.java @@ -20,7 +20,7 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * This class represents a Stored Procedure Parameter and an instance of the annotation 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 88d4716d88..65304dcbba 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 @@ -18,8 +18,9 @@ import java.util.Set; import org.springframework.data.domain.Sort; + +import org.jspecify.annotations.Nullable; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; /** * This interface describes the API for enhancing a given Query. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java index 07c1def305..b681037cbf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java @@ -17,7 +17,7 @@ import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Value object capturing introspection details of a parsed query. @@ -44,8 +44,7 @@ class QueryInformation { * * @return */ - @Nullable - public String getAlias() { + public @Nullable String getAlias() { return alias; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java index d88589d6ef..caeb8fd78f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java @@ -29,8 +29,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index 3944628cf4..3a6bb4c7e9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -21,6 +21,8 @@ import java.util.function.Function; import org.springframework.data.expression.ValueEvaluationContext; + +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; import org.springframework.data.expression.ValueExpressionParser; @@ -32,7 +34,6 @@ import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -53,8 +54,7 @@ abstract class QueryParameterSetterFactory { * @param binding the parameter binding to create a {@link QueryParameterSetter} for. * @return */ - @Nullable - abstract QueryParameterSetter create(ParameterBinding binding); + abstract @Nullable QueryParameterSetter create(ParameterBinding binding); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -109,7 +109,7 @@ static QueryParameterSetterFactory parsing(ValueExpressionParser parser, * @param binding the binding of the query parameter to be set. * @param parameter the method parameter to bind. */ - private static QueryParameterSetter createSetter(Function valueExtractor, + private static QueryParameterSetter createSetter(Function valueExtractor, ParameterBinding binding, @Nullable JpaParameter parameter) { TemporalType temporalType = parameter != null && parameter.isTemporalParameter() // @@ -120,8 +120,7 @@ private static QueryParameterSetter createSetter(Function parameters, String name) { + static @Nullable JpaParameter findParameterForBinding(Parameters parameters, String name) { JpaParameters bindableParameters = parameters.getBindableParameters(); @@ -180,9 +179,8 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar this.evaluationContextProvider = evaluationContextProvider; } - @Nullable @Override - public QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding) { if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) { return null; @@ -198,8 +196,7 @@ public QueryParameterSetter create(ParameterBinding binding) { * @param accessor must not be {@literal null}. * @return the result of the evaluation. */ - @Nullable - private Object evaluateExpression(ValueExpression expression, JpaParametersParameterAccessor accessor) { + private @Nullable Object evaluateExpression(ValueExpression expression, JpaParametersParameterAccessor accessor) { ValueEvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(accessor.getValues()); return expression.evaluate(evaluationContext); @@ -215,7 +212,7 @@ private Object evaluateExpression(ValueExpression expression, JpaParametersParam private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory { @Override - public QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding) { if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) { return null; @@ -251,7 +248,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @Override - public QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding) { Assert.notNull(binding, "Binding must not be null"); @@ -276,8 +273,7 @@ public QueryParameterSetter create(ParameterBinding binding) { : createSetter(values -> getValue(values, parameter), binding, parameter); } - @Nullable - protected Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { + protected @Nullable Object getValue(JpaParametersParameterAccessor accessor, Parameter parameter) { return accessor.getValue(parameter); } } @@ -298,7 +294,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { } @Override - public QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding) { if (!binding.getOrigin().isMethodArgument()) { return null; @@ -352,15 +348,13 @@ public ParameterImpl(BindingIdentifier identifier, Class parameterType) { this.parameterType = parameterType; } - @Nullable @Override - public String getName() { + public @Nullable String getName() { return identifier.hasName() ? identifier.getName() : null; } - @Nullable @Override - public Integer getPosition() { + public @Nullable Integer getPosition() { return identifier.hasPosition() ? identifier.getPosition() : null; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java index b7f0b45123..5c0969ea2b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java @@ -22,9 +22,10 @@ import java.util.List; import java.util.stream.Stream; -import org.springframework.lang.Nullable; import org.springframework.util.CompositeIterator; +import org.jspecify.annotations.Nullable; + /** * Abstraction to encapsulate query expressions and render a query. *

@@ -271,8 +272,7 @@ QueryRenderer append(QueryTokenStream tokens) { } @Override - @Nullable - public QueryToken getLast() { + public @Nullable QueryToken getLast() { for (int i = nested.size() - 1; i > -1; i--) { @@ -368,14 +368,12 @@ public List toList() { } @Override - @Nullable - public QueryToken getFirst() { + public @Nullable QueryToken getFirst() { return tokens.isEmpty() ? null : tokens.get(0); } @Override - @Nullable - public QueryToken getLast() { + public @Nullable QueryToken getLast() { return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); } @@ -438,14 +436,12 @@ public Iterator iterator() { } @Override - @Nullable - public QueryToken getFirst() { + public @Nullable QueryToken getFirst() { return tokens.getFirst(); } @Override - @Nullable - public QueryToken getLast() { + public @Nullable QueryToken getLast() { return tokens.getLast(); } @@ -574,14 +570,12 @@ public Stream stream() { } @Override - @Nullable - public QueryToken getFirst() { + public @Nullable QueryToken getFirst() { return current.getFirst(); } @Override - @Nullable - public QueryToken getLast() { + public @Nullable QueryToken getLast() { return current.getLast(); } @@ -645,14 +639,12 @@ public Iterator iterator() { } @Override - @Nullable - public QueryToken getFirst() { + public @Nullable QueryToken getFirst() { return delegate.getFirst(); } @Override - @Nullable - public QueryToken getLast() { + public @Nullable QueryToken getLast() { return delegate.getLast(); } @@ -701,14 +693,12 @@ public Iterator iterator() { } @Override - @Nullable - public QueryToken getFirst() { + public @Nullable QueryToken getFirst() { return delegate.getFirst(); } @Override - @Nullable - public QueryToken getLast() { + public @Nullable QueryToken getLast() { return delegate.getLast(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java index 0b3b659c8d..5b68191cfd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java @@ -23,9 +23,9 @@ import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.TerminalNode; +import org.jspecify.annotations.Nullable; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -142,8 +142,7 @@ static QueryTokenStream concat(Collection elements, Function it = iterator(); return it.hasNext() ? it.next() : null; @@ -167,8 +166,7 @@ default QueryToken getRequiredFirst() { /** * @return the last query token or {@code null} if empty. */ - @Nullable - default QueryToken getLast() { + default @Nullable QueryToken getLast() { return CollectionUtils.lastElement(toList()); } 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 bbb638eda0..9b931a34e7 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 @@ -47,13 +47,14 @@ import java.util.stream.Collectors; import org.springframework.core.annotation.AnnotationUtils; + +import org.jspecify.annotations.Nullable; 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.JpaOrder; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -446,9 +447,8 @@ private static String toJpaDirection(Order order) { * @return Might return {@literal null}. * @deprecated use {@link DeclaredQuery#getAlias()} instead. */ - @Nullable @Deprecated - public static String detectAlias(String query) { + public static @Nullable String detectAlias(String query) { String alias = null; Matcher matcher = ALIAS_MATCH.matcher(removeSubqueries(query)); @@ -859,6 +859,7 @@ static boolean requiresOuterJoin(From from, PropertyPath property, boolean return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); } + @SuppressWarnings("unchecked") static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { Class associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType()); @@ -874,7 +875,12 @@ static T getAnnotationProperty(Attribute attribute, String propertyNam } Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation); - return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName); + if(annotation == null) { + return defaultValue; + } + + T value = (T) AnnotationUtils.getValue(annotation, propertyName); + return value != null ? value : defaultValue; } /** @@ -962,8 +968,7 @@ static void checkSortExpression(Order order) { * @see https://github.com/jakartaee/persistence/issues/562 */ - @Nullable - private static Bindable getModelForPath(PropertyPath path, @Nullable ManagedType managedType, + private static @Nullable Bindable getModelForPath(PropertyPath path, @Nullable ManagedType managedType, Path fallback) { String segment = path.getSegment(); @@ -987,8 +992,7 @@ private static Bindable getModelForPath(PropertyPath path, @Nullable ManagedT * @param model * @return */ - @Nullable - static ManagedType getManagedTypeForModel(Bindable model) { + static @Nullable ManagedType getManagedTypeForModel(Bindable model) { if (model instanceof ManagedType managedType) { return managedType; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index b90648223b..b43f555c12 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -19,9 +19,10 @@ 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; -import org.springframework.lang.Nullable; /** * {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java index 2616c3d796..2463b64c6a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSource.java @@ -27,7 +27,8 @@ import java.util.List; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -174,8 +175,7 @@ private List extractOutputParametersFrom(NamedStoredProcedur * @param procedure must not be {@literal null}. * @return */ - @Nullable - private NamedStoredProcedureQuery tryFindAnnotatedNamedStoredProcedureQuery(Method method, + private @Nullable NamedStoredProcedureQuery tryFindAnnotatedNamedStoredProcedureQuery(Method method, JpaEntityMetadata entityMetadata, Procedure procedure) { Assert.notNull(method, "Method must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java index e7ef76a3eb..0429ac5f6f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributes.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -93,7 +94,7 @@ private ProcedureParameter getParameterWithCompletedName(ProcedureParameter para parameter.getType()); } - private String completeOutputParameterName(int i, String paramName) { + private String completeOutputParameterName(int i, @Nullable String paramName) { return StringUtils.hasText(paramName) // ? paramName // diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java index e91ffbffb1..3423c71e45 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java @@ -26,9 +26,10 @@ import java.util.Map; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; + +import org.jspecify.annotations.Nullable; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; 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 ef58f18ff4..b0b50cecb8 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 @@ -30,6 +30,8 @@ import java.util.regex.Pattern; import org.springframework.data.expression.ValueExpression; + +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding; @@ -38,7 +40,6 @@ 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.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -168,8 +169,7 @@ public String getQueryString() { } @Override - @Nullable - public String getAlias() { + public @Nullable String getAlias() { return queryEnhancer.detectAlias(); } @@ -393,8 +393,10 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que BindingIdentifier queryParameter; if (parameterIndex != null) { queryParameter = BindingIdentifier.of(parameterIndex); - } else { + } else if (parameterName != null) { queryParameter = BindingIdentifier.of(parameterName); + } else { + throw new IllegalStateException("No bindable expression found"); } ParameterOrigin origin = ObjectUtils.isEmpty(expression) ? ParameterOrigin.ofParameter(parameterName, parameterIndex) @@ -458,8 +460,7 @@ private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(Stri return rewriter.parse(queryWithSpel); } - @Nullable - private static Integer getParameterIndex(@Nullable String parameterIndexString) { + private static @Nullable Integer getParameterIndex(@Nullable String parameterIndexString) { if (parameterIndexString == null || parameterIndexString.isEmpty()) { return null; @@ -519,8 +520,7 @@ private enum ParameterBindingType { * * @return the keyword */ - @Nullable - public String getKeyword() { + public @Nullable String getKeyword() { return keyword; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/package-info.java index efbf2d7af3..9f42b926da 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/package-info.java @@ -1,5 +1,5 @@ /** * Query implementation to execute queries against JPA. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.repository.query; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java index 4b0b7bacaf..6ac031cc56 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java @@ -21,7 +21,8 @@ import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * Interface to abstract {@link CrudMethodMetadata} that provide the {@link LockModeType} to be used for query @@ -76,7 +77,8 @@ public interface CrudMethodMetadata { * @return * @since 1.9 */ - Optional getEntityGraph(); + @Nullable + EntityGraph getEntityGraph(); /** * Returns the {@link Method} to be used. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java index 135d3c6e44..0a9a902e00 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java @@ -28,6 +28,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; @@ -41,7 +42,6 @@ import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -61,11 +61,12 @@ */ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, BeanClassLoaderAware { - private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader classLoader; @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.classLoader = classLoader; + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader(); + } @Override @@ -120,15 +121,16 @@ static MethodInvocation currentInvocation() throws IllegalStateException { MethodInvocation mi = currentInvocation.get(); - if (mi == null) - throw new IllegalStateException( - "No MethodInvocation found: Check that an AOP invocation is in progress, and that the " - + "CrudMethodMetadataPopulatingMethodInterceptor is upfront in the interceptor chain."); - return mi; + if (mi != null) { + return mi; + } + throw new IllegalStateException( + "No MethodInvocation found: Check that an AOP invocation is in progress, and that the " + + "CrudMethodMetadataPopulatingMethodInterceptor is upfront in the interceptor chain."); } @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); @@ -184,7 +186,7 @@ private static class DefaultCrudMethodMetadata implements CrudMethodMetadata { private final org.springframework.data.jpa.repository.support.QueryHints queryHints; private final org.springframework.data.jpa.repository.support.QueryHints queryHintsForCount; private final @Nullable String comment; - private final Optional entityGraph; + private final @Nullable EntityGraph entityGraph; private final Method method; /** @@ -204,12 +206,11 @@ private static class DefaultCrudMethodMetadata implements CrudMethodMetadata { this.method = method; } - private static Optional findEntityGraph(Method method) { - return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class)); + private static @Nullable EntityGraph findEntityGraph(Method method) { + return AnnotatedElementUtils.findMergedAnnotation(method, EntityGraph.class); } - @Nullable - private static LockModeType findLockModeType(Method method) { + private static @Nullable LockModeType findLockModeType(Method method) { Lock annotation = AnnotatedElementUtils.findMergedAnnotation(method, Lock.class); return annotation == null ? null : (LockModeType) AnnotationUtils.getValue(annotation); @@ -238,16 +239,14 @@ private static org.springframework.data.jpa.repository.support.QueryHints findQu return queryHints; } - @Nullable - private static String findComment(Method method) { + private static @Nullable String findComment(Method method) { Meta annotation = AnnotatedElementUtils.findMergedAnnotation(method, Meta.class); return annotation == null ? null : (String) AnnotationUtils.getValue(annotation, "comment"); } - @Nullable @Override - public LockModeType getLockModeType() { + public @Nullable LockModeType getLockModeType() { return lockModeType; } @@ -262,12 +261,12 @@ public org.springframework.data.jpa.repository.support.QueryHints getQueryHintsF } @Override - public String getComment() { + public @Nullable String getComment() { return comment; } @Override - public Optional getEntityGraph() { + public @Nullable EntityGraph getEntityGraph() { return entityGraph; } @@ -291,7 +290,7 @@ public boolean isStatic() { } @Override - public Object getTarget() { + public @Nullable Object getTarget() { MethodInvocation invocation = CrudMethodMetadataPopulatingMethodInterceptor.currentInvocation(); return TransactionSynchronizationManager.getResource(invocation.getMethod()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultQueryHints.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultQueryHints.java index 228251d4f2..12c05b6e76 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultQueryHints.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultQueryHints.java @@ -17,13 +17,12 @@ import jakarta.persistence.EntityManager; -import java.util.Optional; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.query.Jpa21Utils; import org.springframework.data.jpa.repository.query.JpaEntityGraph; -import org.springframework.data.util.Optionals; import org.springframework.util.Assert; /** @@ -38,7 +37,7 @@ class DefaultQueryHints implements QueryHints { private final JpaEntityInformation information; private final CrudMethodMetadata metadata; - private final Optional entityManager; + private final @Nullable EntityManager entityManager; private final boolean forCounts; /** @@ -46,12 +45,12 @@ class DefaultQueryHints implements QueryHints { * {@link CrudMethodMetadata}, {@link EntityManager} and whether to include fetch graphs. * * @param information must not be {@literal null}. - * @param metadata must not be {@literal null}. + * @param metadata can be {@literal null}. * @param entityManager must not be {@literal null}. * @param forCounts */ private DefaultQueryHints(JpaEntityInformation information, CrudMethodMetadata metadata, - Optional entityManager, boolean forCounts) { + @Nullable EntityManager entityManager, boolean forCounts) { this.information = information; this.metadata = metadata; @@ -72,12 +71,12 @@ public static QueryHints of(JpaEntityInformation information, CrudMethodMe Assert.notNull(information, "JpaEntityInformation must not be null"); Assert.notNull(metadata, "CrudMethodMetadata must not be null"); - return new DefaultQueryHints(information, metadata, Optional.empty(), false); + return new DefaultQueryHints(information, metadata, null, false); } @Override public QueryHints withFetchGraphs(EntityManager em) { - return new DefaultQueryHints(this.information, this.metadata, Optional.of(em), this.forCounts); + return new DefaultQueryHints(this.information, this.metadata, em, this.forCounts); } @Override @@ -96,10 +95,10 @@ private QueryHints combineHints() { private QueryHints getFetchGraphs() { - return Optionals - .mapIfAllPresent(entityManager, metadata.getEntityGraph(), - (em, graph) -> Jpa21Utils.getFetchGraphHint(em, getEntityGraph(graph), information.getJavaType())) - .orElseGet(MutableQueryHints::new); + if(entityManager != null && metadata.getEntityGraph() != null) { + return Jpa21Utils.getFetchGraphHint(entityManager, getEntityGraph(metadata.getEntityGraph()), information.getJavaType()); + } + return new MutableQueryHints(); } private JpaEntityGraph getEntityGraph(EntityGraph entityGraph) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java index 5308fa64b8..6a63a8260e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java @@ -61,10 +61,13 @@ public static EntityGraph create(EntityManager entityManager, Class do currentFullPath += path.getSegment() + "."; if (path.hasNext()) { - final Subgraph finalCurrent = current; - current = current == null - ? existingSubgraphs.computeIfAbsent(currentFullPath, k -> entityGraph.addSubgraph(path.getSegment())) - : existingSubgraphs.computeIfAbsent(currentFullPath, k -> finalCurrent.addSubgraph(path.getSegment())); + + if(current == null) { + current = existingSubgraphs.computeIfAbsent(currentFullPath, k -> entityGraph.addSubgraph(path.getSegment())); + } else { + final Subgraph finalCurrent = current; + current = existingSubgraphs.computeIfAbsent(currentFullPath, k -> finalCurrent.addSubgraph(path.getSegment())); + } continue; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java index f5d42e2257..171e59012e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryByPredicate.java @@ -41,7 +41,6 @@ import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.querydsl.core.types.EntityPath; @@ -52,6 +51,7 @@ import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.JPQLSerializer; import com.querydsl.jpa.impl.AbstractJPAQuery; +import org.jspecify.annotations.Nullable; /** * Immutable implementation of {@link FetchableFluentQuery} based on a Querydsl {@link Predicate}. All methods that @@ -147,7 +147,7 @@ public FetchableFluentQuery project(Collection properties) { } @Override - public R oneValue() { + public @Nullable R oneValue() { List results = createSortedAndProjectedQuery(this.sort) // .limit(2) // Never need more than 2 values @@ -161,7 +161,7 @@ public R oneValue() { } @Override - public R firstValue() { + public @Nullable R firstValue() { List results = createSortedAndProjectedQuery(this.sort) // .limit(1) // Never need more than 1 value diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java index a1c91b9148..ba882f244c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java @@ -29,6 +29,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -44,7 +46,6 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -134,7 +135,7 @@ public SpecificationFluentQuery project(Collection properties) { } @Override - public R oneValue() { + public @Nullable R oneValue() { List results = createSortedAndProjectedQuery(this.sort) // .setMaxResults(2) // Never need more than 2 values @@ -148,7 +149,7 @@ public R oneValue() { } @Override - public R firstValue() { + public @Nullable R firstValue() { List results = createSortedAndProjectedQuery(this.sort) // .setMaxResults(1) // Never need more than 1 value diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java index 10b484d98a..f530e6eade 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FluentQuerySupport.java @@ -22,6 +22,8 @@ import java.util.function.Function; import org.springframework.core.convert.support.DefaultConversionService; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; @@ -29,7 +31,6 @@ import org.springframework.data.jpa.repository.query.AbstractJpaQuery; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.lang.Nullable; /** * Supporting class containing some state and convenience methods for building and executing fluent queries. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java index 9c367343c5..6b2e6361e9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JakartaTuple.java @@ -20,8 +20,6 @@ import java.util.Arrays; import java.util.List; -import org.springframework.lang.Nullable; - import com.querydsl.core.types.Expression; import com.querydsl.core.types.ExpressionBase; import com.querydsl.core.types.ExpressionUtils; @@ -31,6 +29,7 @@ import com.querydsl.core.types.Projections; import com.querydsl.core.types.Visitor; import com.querydsl.jpa.JPQLSerializer; +import org.jspecify.annotations.Nullable; /** * Expression based on a {@link Tuple}. It's a simplified variant of {@link com.querydsl.core.types.QTuple} without @@ -72,8 +71,7 @@ protected JakartaTuple(List> args) { } @Override - @Nullable - public R accept(Visitor v, @Nullable C context) { + public @Nullable R accept(Visitor v, @Nullable C context) { if (v instanceof JPQLSerializer) { return Projections.tuple(args).accept(v, context); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java index 98828424ab..1e378c3308 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformation.java @@ -21,8 +21,9 @@ import java.util.Map; import org.springframework.data.jpa.repository.query.JpaEntityMetadata; + +import org.jspecify.annotations.Nullable; import org.springframework.data.repository.core.EntityInformation; -import org.springframework.lang.Nullable; /** * Extension of {@link EntityInformation} to capture additional JPA specific information about entities. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java index f635a221a4..347b10fde5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEvaluationContextExtension.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jpa.repository.support; +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.spel.spi.EvaluationContextExtension; @@ -66,7 +67,7 @@ public static JpaRootObject of(EscapeCharacter character) { * @return * @see EscapeCharacter#escape(String) */ - public String escape(String source) { + public @Nullable String escape(@Nullable String source) { return character.escape(source); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java index 4f337709cc..66994749da 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java @@ -38,11 +38,12 @@ import java.util.function.Function; import org.springframework.beans.BeanWrapper; + +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -143,9 +144,8 @@ public String getEntityName() { } @Override - @Nullable @SuppressWarnings("unchecked") - public ID getId(T entity) { + public @Nullable ID getId(T entity) { // check if this is a proxy. If so use Proxy mechanics to access the id. PersistenceProvider persistenceProvider = PersistenceProvider.fromMetamodel(metamodel); @@ -215,7 +215,7 @@ public Collection getIdAttributeNames() { } @Override - public Object getCompositeIdAttributeValue(Object id, String idAttribute) { + public @Nullable Object getCompositeIdAttributeValue(Object id, String idAttribute) { Assert.isTrue(hasCompositeId(), "Model must have a composite Id"); @@ -312,8 +312,7 @@ public Class getType() { return this.idType; } - @Nullable - private Class tryExtractIdTypeWithFallbackToIdTypeLookup() { + private @Nullable Class tryExtractIdTypeWithFallbackToIdTypeLookup() { try { @@ -330,8 +329,7 @@ private Class tryExtractIdTypeWithFallbackToIdTypeLookup() { } } - @Nullable - private static Class lookupIdClass(IdentifiableType type) { + private static @Nullable Class lookupIdClass(IdentifiableType type) { IdClass annotation = type.getJavaType() != null ? AnnotationUtils.findAnnotation(type.getJavaType(), IdClass.class) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaPersistableEntityInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaPersistableEntityInformation.java index aaaff2050c..5832047303 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaPersistableEntityInformation.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaPersistableEntityInformation.java @@ -19,7 +19,8 @@ import jakarta.persistence.metamodel.Metamodel; import org.springframework.data.domain.Persistable; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * Extension of {@link JpaMetamodelEntityInformation} that consideres methods of {@link Persistable} to lookup the id. @@ -48,9 +49,8 @@ public boolean isNew(T entity) { return entity.isNew(); } - @Nullable @Override - public ID getId(T entity) { + public @Nullable ID getId(T entity) { return entity.getId(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index e14658773b..2d5a95a27a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; @@ -61,7 +62,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -123,7 +123,7 @@ public JpaRepositoryFactory(EntityManager entityManager) { } @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { super.setBeanClassLoader(classLoader); this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); @@ -226,11 +226,15 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { } @Override - protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { CollectionAwareProjectionFactory factory = new CollectionAwareProjectionFactory(); - factory.setBeanClassLoader(classLoader); - factory.setBeanFactory(beanFactory); + if(classLoader != null) { + factory.setBeanClassLoader(classLoader); + } + if(beanFactory != null) { + factory.setBeanFactory(beanFactory); + } return factory; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index 86f2f14d6c..e0a1b00e62 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -19,6 +19,8 @@ import jakarta.persistence.PersistenceContext; import org.springframework.beans.factory.ObjectProvider; + +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; @@ -28,7 +30,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,7 +49,7 @@ public class JpaRepositoryFactoryBean, S, ID> private @Nullable EntityManager entityManager; private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; - private JpaQueryMethodFactory queryMethodFactory; + private @Nullable JpaQueryMethodFactory queryMethodFactory; /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java index 62155b8f0b..da00e05368 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java @@ -25,7 +25,6 @@ import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.querydsl.QSort; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.querydsl.core.types.EntityPath; @@ -41,6 +40,7 @@ import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.AbstractJPAQuery; import com.querydsl.jpa.impl.JPAQuery; +import org.jspecify.annotations.Nullable; /** * Helper instance to ease access to Querydsl JPA query API. @@ -87,10 +87,9 @@ public AbstractJPAQuery> createQuery() { * Obtains the {@link JPQLTemplates} for the configured {@link EntityManager}. Can return {@literal null} to use the * default templates. * - * @return the {@link JPQLTemplates} for the configured {@link EntityManager} or {@literal null} to use the default. + * @return the {@link JPQLTemplates} for the configured {@link EntityManager}, {@link JPQLTemplates#DEFAULT} by default. * @since 3.5 */ - @Nullable public JPQLTemplates getTemplates() { return switch (provider) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index b37a6e0209..c28660cb92 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -44,7 +44,6 @@ import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.querydsl.core.NonUniqueResultException; @@ -60,6 +59,7 @@ import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.AbstractJPAQuery; +import org.jspecify.annotations.Nullable; /** * Querydsl specific fragment for extending {@link SimpleJpaRepository} with an implementation of @@ -297,8 +297,7 @@ protected JPQLQuery createCountQuery(@Nullable Predicate... predicate) { return doCreateQuery(getQueryHintsForCount(), predicate); } - @Nullable - private CrudMethodMetadata getRepositoryMethodMetadata() { + private @Nullable CrudMethodMetadata getRepositoryMethodMetadata() { return metadata; } @@ -375,7 +374,12 @@ public Expression createExpression(String property) { } @Override - public BooleanExpression compare(Order order, Expression propertyExpression, Object value) { + public BooleanExpression compare(Order order, Expression propertyExpression, @Nullable Object value) { + + if(value == null) { + return Expressions.booleanOperation(order.isAscending() ? Ops.IS_NULL : Ops.IS_NOT_NULL, propertyExpression); + } + return Expressions.booleanOperation(order.isAscending() ? Ops.GT : Ops.LT, propertyExpression, ConstantImpl.create(value)); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupport.java index 09c43e198b..562b2ad25d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupport.java @@ -16,10 +16,11 @@ package org.springframework.data.jpa.repository.support; import jakarta.annotation.PostConstruct; +import org.jspecify.annotations.Nullable; + import jakarta.persistence.EntityManager; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Repository; import org.springframework.util.Assert; @@ -84,8 +85,7 @@ public void validate() { * * @return the entityManager */ - @Nullable - protected EntityManager getEntityManager() { + protected @Nullable EntityManager getEntityManager() { return entityManager; } @@ -145,8 +145,7 @@ protected PathBuilder getBuilder() { * * @return */ - @Nullable - protected Querydsl getQuerydsl() { + protected @Nullable Querydsl getQuerydsl() { return this.querydsl; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index aad76c29ca..48632c09b6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -41,6 +41,8 @@ import java.util.function.Function; import org.springframework.dao.InvalidDataAccessApiUsageException; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -72,7 +74,7 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.ProxyUtils; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -169,8 +171,7 @@ public void setProjectionFactory(ProjectionFactory projectionFactory) { this.projectionFactory = projectionFactory; } - @Nullable - protected CrudMethodMetadata getRepositoryMethodMetadata() { + protected @Nullable CrudMethodMetadata getRepositoryMethodMetadata() { return metadata; } @@ -252,7 +253,7 @@ public void deleteAllByIdInBatch(Iterable ids) { } else { String queryString = String.format(DELETE_ALL_QUERY_BY_ID_STRING, entityInformation.getEntityName(), - entityInformation.getIdAttribute().getName()); + entityInformation.getRequiredIdAttribute().getName()); Query query = entityManager.createQuery(queryString); @@ -717,8 +718,9 @@ protected Page readPage(TypedQuery query, Pageable pageable, Specification * @param spec must not be {@literal null}. * @param pageable can be {@literal null}. */ + @Contract("_, _, _, null -> fail") protected Page readPage(TypedQuery query, Class domainClass, Pageable pageable, - Specification spec) { + @Nullable Specification spec) { Assert.notNull(spec, "Specification must not be null"); @@ -737,7 +739,7 @@ protected Page readPage(TypedQuery query, Class domainCla * @param spec must not be {@literal null}. * @param pageable must not be {@literal null}. */ - protected TypedQuery getQuery(Specification spec, Pageable pageable) { + protected TypedQuery getQuery(@Nullable Specification spec, Pageable pageable) { return getQuery(spec, getDomainClass(), pageable.getSort()); } @@ -1109,7 +1111,7 @@ private record ExampleSpecification(Example example, } @Override - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { + public @Nullable Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder cb) { return QueryByExamplePredicateBuilder.getPredicate(root, cb, example, escapeCharacter); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java index 2ee289253a..d5d518d004 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SpringDataJpaQuery.java @@ -22,8 +22,6 @@ import java.util.Collection; import java.util.Map; -import org.springframework.lang.Nullable; - import com.querydsl.core.QueryModifiers; import com.querydsl.core.types.Expression; import com.querydsl.core.types.FactoryExpression; @@ -31,6 +29,7 @@ import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAUtil; +import org.jspecify.annotations.Nullable; /** * Customized String-Query implementation that specifically routes tuple query creation to diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/package-info.java index 2f75e71375..c40a1ae92f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/package-info.java @@ -1,5 +1,5 @@ /** * JPA repository implementations. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.repository.support; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java index dd4690086b..686d2ab7ed 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java @@ -26,6 +26,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; @@ -39,7 +41,6 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; import org.springframework.util.Assert; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/package-info.java index ad7b5e7f45..6e60ae77b4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/support/package-info.java @@ -1,5 +1,5 @@ /** * Various helper classes useful when working with JPA. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.support; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/HibernateProxyDetector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/HibernateProxyDetector.java index 149742c0b7..2caa4ea9a8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/HibernateProxyDetector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/HibernateProxyDetector.java @@ -18,8 +18,9 @@ import java.util.Optional; import org.hibernate.proxy.HibernateProxy; +import org.jspecify.annotations.Nullable; + import org.springframework.data.util.ProxyUtils.ProxyDetector; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -40,8 +41,7 @@ public Class getUserType(Class type) { .orElse(type); } - @Nullable - private static Class loadHibernateProxyType() { + private static @Nullable Class loadHibernateProxyType() { try { return ClassUtils.forName("org.hibernate.proxy.HibernateProxy", HibernateProxyDetector.class.getClassLoader()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/package-info.java index f49bdb7cc1..264664d04e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/package-info.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/package-info.java @@ -1,5 +1,5 @@ /** * Spring Data JPA utilities. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.jpa.util; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/AntlrVersionTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/AntlrVersionTests.java index 7c18f5d466..489f29326e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/AntlrVersionTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/AntlrVersionTests.java @@ -22,6 +22,7 @@ import org.antlr.v4.runtime.RuntimeMetaData; import org.hibernate.grammars.hql.HqlParser; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.asm.ClassReader; @@ -29,7 +30,6 @@ import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; import org.springframework.data.jpa.util.DisabledOnHibernate; -import org.springframework.lang.Nullable; /** * Test to verify that we use the same Antlr version as Hibernate. We parse {@code org.hibernate.grammars.hql.HqlParser} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/JpaSortTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/JpaSortTests.java index dac929f40d..c64d55f7f7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/JpaSortTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/JpaSortTests.java @@ -23,6 +23,7 @@ import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.PluralAttribute; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,7 +31,6 @@ import org.springframework.data.jpa.domain.sample.MailMessage_; import org.springframework.data.jpa.domain.sample.MailSender_; import org.springframework.data.jpa.domain.sample.User_; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/AuditableUser.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/AuditableUser.java index 65d4e6e2ad..59b561968f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/AuditableUser.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/AuditableUser.java @@ -24,7 +24,8 @@ import java.util.Set; import org.springframework.data.jpa.domain.AbstractAuditable; -import org.springframework.lang.Nullable; + +import org.jspecify.annotations.Nullable; /** * Sample auditable user to demonstrate working with {@code AbstractAuditableEntity}. No declaration of an ID is diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserWithOptionalField.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserWithOptionalField.java index cdfb9a3bfc..2833123509 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserWithOptionalField.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/UserWithOptionalField.java @@ -21,7 +21,7 @@ import java.util.Optional; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Greg Turnquist 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 ed49e479bb..879158bfd8 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 @@ -46,6 +46,7 @@ import org.assertj.core.api.SoftAssertions; import org.hibernate.LazyInitializationException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -74,7 +75,6 @@ import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.jpa.repository.sample.UserRepository.NameOnly; import org.springframework.data.jpa.util.DisabledOnHibernate; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index 3fb97409f8..44d061094f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java @@ -27,6 +27,7 @@ import org.assertj.core.api.Assertions; import org.assertj.core.util.Arrays; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -43,7 +44,6 @@ import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java index 3c1fec2ed3..782c460a24 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -20,6 +20,7 @@ import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -28,7 +29,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort; -import org.springframework.lang.Nullable; /** * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index 40a8c4dc7a..1098f6a623 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -22,6 +22,7 @@ import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -32,7 +33,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -1183,8 +1183,7 @@ private String createCountQueryFor(String query, @Nullable String countProjectio return newParser(query).createCountQueryFor(countProjection); } - @Nullable - private String alias(String query) { + private @Nullable String alias(String query) { return newParser(query).detectAlias(); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java index aebad09360..937568e01d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java @@ -33,12 +33,12 @@ import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; @@ -191,8 +191,7 @@ void allowsEmptyGraph() { /** * Lookup the {@link AttributeNode} with given {@literal nodeName} in the {@link List} of given {@literal nodes}. */ - @Nullable - static AttributeNode findNode(String nodeName, List> nodes) { + static @Nullable AttributeNode findNode(String nodeName, List> nodes) { if (CollectionUtils.isEmpty(nodes)) { return null; @@ -211,8 +210,7 @@ static AttributeNode findNode(String nodeName, List> nodes) * Lookup the {@link AttributeNode} with given {@literal nodeName} in the first {@link Subgraph} of the given * {@literal node}. */ - @Nullable - static AttributeNode findNode(String attributeName, AttributeNode node) { + static @Nullable AttributeNode findNode(String attributeName, AttributeNode node) { if (CollectionUtils.isEmpty(node.getSubgraphs())) { return null; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java index f73b45e92d..55e9f39122 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryCreatorTests.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -49,7 +50,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; /** * Unit tests for {@link JpaQueryCreator}. @@ -979,9 +979,8 @@ public int bindingIndexFor(String placeholder) { public ParameterAccessor bindableParameters() { return new ParameterAccessor() { - @Nullable @Override - public ScrollPosition getScrollPosition() { + public @Nullable ScrollPosition getScrollPosition() { return null; } @@ -995,15 +994,13 @@ public Sort getSort() { return null; } - @Nullable @Override - public Class findDynamicProjection() { + public @Nullable Class findDynamicProjection() { return null; } - @Nullable @Override - public Object getBindableValue(int index) { + public @Nullable Object getBindableValue(int index) { ParameterBinding parameterBinding = queryCreator.get().getBindings().get(index); return parameterBinding.prepare(parameterAccessor.get().getBindableValue(index)); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index 660f3c9a7d..acc6617811 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -20,6 +20,7 @@ import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -28,7 +29,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort; -import org.springframework.lang.Nullable; /** * Verify that JPQL queries are properly transformed through the {@link JpaQueryEnhancer} and the 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 99b8a7a730..f95e9007b1 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 @@ -19,6 +19,7 @@ import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -26,7 +27,6 @@ import org.springframework.data.jpa.repository.query.QueryEnhancerFactory.NativeQueryEnhancer; import org.springframework.data.jpa.util.ClassPathExclusions; -import org.springframework.lang.Nullable; /** * Unit tests for {@link QueryEnhancerFactory}. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeIntegrationTests.java index 9f7e2da8ea..5cf31423a9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeIntegrationTests.java @@ -24,6 +24,7 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,7 +39,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.repository.query.Param; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 5d2beb3d9b..4b53c362c3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -55,7 +56,6 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Unit test for {@link SimpleJpaQuery}. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java index fe8e0dd4b6..b02a606673 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java @@ -27,9 +27,9 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.Nullable; import com.querydsl.core.types.Predicate; +import org.jspecify.annotations.Nullable; /** * Custom repository interface that customizes the fetching behavior of querys of well known repository interface 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 9e0a4a8941..9f86810dc9 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 @@ -29,6 +29,8 @@ import java.util.stream.Stream; import org.springframework.data.domain.Limit; + +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -50,7 +52,6 @@ import org.springframework.data.querydsl.ListQuerydslPredicateExecutor; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; -import org.springframework.lang.Nullable; import org.springframework.transaction.annotation.Transactional; /** diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index 583ab2330f..3d17c347c1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java @@ -145,7 +145,7 @@ void shouldPropagateConfiguredEntityGraphToFindOne() throws Exception { String entityGraphName = "User.detail"; when(entityGraphAnnotation.value()).thenReturn(entityGraphName); when(entityGraphAnnotation.type()).thenReturn(EntityGraphType.LOAD); - when(metadata.getEntityGraph()).thenReturn(Optional.of(entityGraphAnnotation)); + when(metadata.getEntityGraph()).thenReturn(entityGraphAnnotation); when(em.getEntityGraph(entityGraphName)).thenReturn((EntityGraph) entityGraph); when(information.getEntityName()).thenReturn("User"); when(metadata.getMethod()).thenReturn(CrudRepository.class.getMethod("findById", Object.class)); From c149e151b11362209946c831c7d9ce90ea54b9af Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 19 Feb 2025 08:56:11 +0100 Subject: [PATCH 46/94] Add contract annotations to public API. See #3745 Original pull request: #3781 --- .../convert/QueryByExamplePredicateBuilder.java | 6 ++---- .../data/jpa/domain/AbstractAuditable.java | 9 +++------ .../data/jpa/domain/AbstractPersistable.java | 5 +---- .../data/jpa/domain/DeleteSpecification.java | 1 + .../data/jpa/domain/JpaSort.java | 14 ++++++++++++++ .../jpa/repository/query/AbstractJpaQuery.java | 2 ++ .../jpa/repository/query/EscapeCharacter.java | 2 ++ .../jpa/repository/query/JpaQueryMethod.java | 1 - .../jpa/repository/query/JpqlQueryBuilder.java | 17 +++++++++++++++++ 9 files changed, 42 insertions(+), 15 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java index c27d9f8804..69baedf0dd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -43,6 +43,7 @@ import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -242,7 +243,6 @@ private static class PathNode { String name; @Nullable PathNode parent; - List siblings = new ArrayList<>(); @Nullable Object value; PathNode(String edge, @Nullable PathNode parent, @Nullable Object value) { @@ -254,9 +254,7 @@ private static class PathNode { PathNode add(String attribute, @Nullable Object value) { - PathNode node = new PathNode(attribute, this, value); - siblings.add(node); - return node; + return new PathNode(attribute, this, value); } boolean spansCycle() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java index 8f93ab0fc6..0b394d0472 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractAuditable.java @@ -24,6 +24,7 @@ import java.time.ZoneId; import java.util.Optional; +import org.jspecify.annotations.NullUnmarked; import org.springframework.data.domain.Auditable; import org.jspecify.annotations.Nullable; @@ -38,23 +39,19 @@ * @param the type of the auditing type's identifier. */ @MappedSuperclass -@SuppressWarnings("NullAway") +@SuppressWarnings("NullAway") // querydsl does not work with jspecify -> 'Did not find type @org.jspecify.annotations.Nullable...' public abstract class AbstractAuditable extends AbstractPersistable implements Auditable { -// @Nullable @ManyToOne // private U createdBy; -// @Nullable private Instant createdDate; -// @Nullable @ManyToOne // private U lastModifiedBy; -// @Nullable - private Instant lastModifiedDate; + private Instant lastModifiedDate; @Override public Optional getCreatedBy() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java index 0d645c1519..19153d70c5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/AbstractPersistable.java @@ -39,16 +39,13 @@ * @param the type of the identifier. */ @MappedSuperclass - +@SuppressWarnings("NullAway") // querydsl does not work with jspecify -> 'Did not find type @org.jspecify.annotations.Nullable...' public abstract class AbstractPersistable implements Persistable { @Nullable @Id @GeneratedValue private PK id; @Override - @SuppressWarnings("NullAway") - // TODO: Querydsl APT does not like @Nullable - // -> errors with cryptic 'Did not find type @org.jspecify.annotations.Nullable PK' public PK getId() { return id; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index bd6911df9c..32278c7ba5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -151,6 +151,7 @@ default DeleteSpecification or(PredicateSpecification other) { * @param spec can be {@literal null}. * @return guaranteed to be not {@literal null}. */ + @Contract("_ -> new") static DeleteSpecification not(DeleteSpecification spec) { Assert.notNull(spec, "Specification must not be null"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java index 4fc0f813aa..2f55c0bf03 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/JpaSort.java @@ -28,6 +28,8 @@ import org.springframework.data.domain.Sort; import org.jspecify.annotations.Nullable; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -104,6 +106,8 @@ public static JpaSort of(Direction direction, Path... paths) { * @param attributes must not be {@literal null}. * @return */ + @Contract("_, _ -> new") + @CheckReturnValue public JpaSort and(@Nullable Direction direction, Attribute... attributes) { Assert.notNull(attributes, "Attributes must not be null"); @@ -118,6 +122,8 @@ public JpaSort and(@Nullable Direction direction, Attribute... attributes) * @param paths must not be {@literal null}. * @return */ + @Contract("_, _ -> new") + @CheckReturnValue public JpaSort and(@Nullable Direction direction, Path... paths) { Assert.notNull(paths, "Paths must not be null"); @@ -138,6 +144,8 @@ public JpaSort and(@Nullable Direction direction, Path... paths) { * @param properties must not be {@literal null} or empty. * @return */ + @Contract("_, _ -> new") + @CheckReturnValue public JpaSort andUnsafe(@Nullable Direction direction, String... properties) { Assert.notEmpty(properties, "Properties must not be empty"); @@ -275,6 +283,8 @@ private Path(List> attributes) { * @param attribute must not be {@literal null}. * @return */ + @Contract("_ -> new") + @CheckReturnValue public , U> Path dot(A attribute) { return new Path<>(add(attribute)); } @@ -285,6 +295,8 @@ public , U> Path dot(A attribute) { * @param attribute must not be {@literal null}. * @return */ + @Contract("_ -> new") + @CheckReturnValue public

, U> Path dot(P attribute) { return new Path<>(add(attribute)); } @@ -372,6 +384,8 @@ public JpaOrder with(NullHandling nullHandling) { * @param properties must not be {@literal null}. * @return */ + @Contract("_ -> new") + @CheckReturnValue public Sort withUnsafe(String... properties) { Assert.notEmpty(properties, "Properties must not be empty"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 851c40a55f..2d75b3970c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -57,6 +57,7 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.Lazy; import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -193,6 +194,7 @@ protected JpaQueryExecution getExecution() { * @return */ @SuppressWarnings("NullAway") + @Contract("_, _ -> param1") protected T applyHints(T query, JpaQueryMethod method) { List hints = method.getHints(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java index 448b80bad1..d6ef5c321b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EscapeCharacter.java @@ -20,6 +20,7 @@ import java.util.stream.Stream; import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; /** * A value type encapsulating an escape character for LIKE queries and the actually usage of it in escaping @@ -49,6 +50,7 @@ public static EscapeCharacter of(char escapeCharacter) { * @param value may be {@literal null}. * @return */ + @Contract("null -> null") public @Nullable String escape(@Nullable String value) { return value == null // 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 afc0e0b98d..25f50b9f22 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 @@ -165,7 +165,6 @@ private static Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metada } @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) public JpaEntityMetadata getEntityInformation() { return this.entityMetadata.get(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index a3e8d70edc..45c804e124 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -33,6 +33,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.Predicates; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -405,16 +407,19 @@ public interface SelectStep { /** * Apply {@code DISTINCT}. */ + @CheckReturnValue SelectStep distinct(); /** * Select the entity. */ + @CheckReturnValue Select entity(); /** * Select the count. */ + @CheckReturnValue Select count(); /** @@ -425,6 +430,7 @@ public interface SelectStep { * @param paths * @return */ + @CheckReturnValue default Select instantiate(Class resultType, Collection paths) { return instantiate(resultType.getName(), paths); } @@ -436,6 +442,7 @@ default Select instantiate(Class resultType, Collection paths); /** @@ -444,6 +451,7 @@ default Select instantiate(Class resultType, Collection paths); /** @@ -452,6 +460,7 @@ default Select instantiate(Class resultType, Collection new") + @CheckReturnValue default Predicate or(Predicate other) { return new OrPredicate(this, other); } @@ -636,6 +647,8 @@ default Predicate or(Predicate other) { * @param other * @return a composed predicate combining this and {@code other} using the AND operator. */ + @Contract("_ -> new") + @CheckReturnValue default Predicate and(Predicate other) { // don't like the structuring of this and the nest() thing return new AndPredicate(this, other); } @@ -645,6 +658,8 @@ default Predicate and(Predicate other) { // don't like the structuring of this a * * @return a nested variant of this predicate. */ + @Contract("-> new") + @CheckReturnValue default Predicate nest() { return new NestedPredicate(this); } @@ -700,6 +715,7 @@ private Select(Selection selection, Entity entity) { * @param join * @return */ + @Contract("_ -> this") public Select join(Join join) { if (join.source() instanceof Join parent) { @@ -716,6 +732,7 @@ public Select join(Join join) { * @param orderBy * @return */ + @Contract("_ -> this") public Select orderBy(Expression orderBy) { this.orderBy.add(orderBy); return this; From a6f8716d0892b3ba9878a75dbf8a520919821ba2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Feb 2025 13:23:29 +0100 Subject: [PATCH 47/94] Polishing. Simplify POM setup. Reformat code. See #3745 Original pull request: #3781 --- spring-data-jpa/pom.xml | 128 +----------------- .../data/jpa/domain/Specification.java | 6 +- .../jpa/domain/SpecificationComposition.java | 2 +- .../config/JpaRepositoryConfigExtension.java | 6 +- .../query/JSqlParserQueryEnhancer.java | 21 +-- .../query/JpqlCountQueryTransformer.java | 6 +- .../data/jpa/repository/query/JpqlUtils.java | 23 ++-- .../query/KeysetScrollSpecification.java | 19 +-- .../data/jpa/repository/query/QueryUtils.java | 6 +- .../support/EntityGraphFactory.java | 8 +- .../FetchableFluentQueryBySpecification.java | 4 +- .../support/JpaRepositoryFactory.java | 11 +- .../support/QuerydslJpaPredicateExecutor.java | 5 +- .../support/SimpleJpaRepository.java | 4 +- .../jpa/repository/UserRepositoryTests.java | 38 ------ 15 files changed, 64 insertions(+), 223 deletions(-) diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 13f0b11177..b6470bdc89 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -343,7 +343,7 @@ org.apache.maven.plugins maven-compiler-plugin - + com.querydsl querydsl-apt @@ -424,130 +424,4 @@ - - - all-dbs - - - - org.apache.maven.plugins - maven-surefire-plugin - - - mysql-test - test - - test - - - - **/MySql*IntegrationTests.java - - - - - postgres-test - test - - test - - - - **/Postgres*IntegrationTests.java - - - - - - - - - - - - - - nullaway - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - com.querydsl - querydsl-apt - ${querydsl} - jakarta - - - org.hibernate.orm - hibernate-jpamodelgen - ${hibernate} - - - org.hibernate.orm - hibernate-core - ${hibernate} - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh} - - - jakarta.persistence - jakarta.persistence-api - ${jakarta-persistence-api} - - - com.google.errorprone - error_prone_core - ${errorprone} - - - com.uber.nullaway - nullaway - ${nullaway} - - - - - - default-compile - none - - - default-testCompile - none - - - java-compile - compile - - compile - - - - -XDcompilePolicy=simple - --should-stop=ifError=FLOW - -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract - - - - - java-test-compile - test-compile - - testCompile - - - - - - - - - diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index f0c782d7a7..b9994b79ad 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -25,9 +25,9 @@ import java.util.Arrays; import java.util.stream.StreamSupport; -import org.springframework.lang.CheckReturnValue; - import org.jspecify.annotations.Nullable; + +import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -232,6 +232,6 @@ static Specification anyOf(Iterable> specifications) { * @return a {@link Predicate}, may be {@literal null}. */ @Nullable - Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder criteriaBuilder); + Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java index 5600b40f58..0c73627bae 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/SpecificationComposition.java @@ -61,7 +61,7 @@ static Specification composed(@Nullable Specification lhs, @Nullable S } private static @Nullable Predicate toPredicate(@Nullable Specification specification, Root root, - @Nullable CriteriaQuery query, CriteriaBuilder builder) { + CriteriaQuery query, CriteriaBuilder builder) { return specification == null ? null : specification.toPredicate(root, query, builder); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 6366a8d5db..32b8670802 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -33,9 +33,9 @@ import java.util.Optional; import java.util.Set; -import org.springframework.aot.generate.GenerationContext; - import org.jspecify.annotations.Nullable; + +import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -117,7 +117,7 @@ public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSo Optional transactionManagerRef = source.getAttribute("transactionManagerRef"); builder.addPropertyValue("transactionManager", transactionManagerRef.orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)); - if(entityManagerRefs.containsKey(source)) { + if (entityManagerRefs.containsKey(source)) { builder.addPropertyReference("entityManager", entityManagerRefs.get(source)); } builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\')); 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 5600043375..f68443adda 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 @@ -38,7 +38,6 @@ import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.update.Update; -import org.jspecify.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -50,6 +49,8 @@ import java.util.Set; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Sort; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -77,7 +78,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { private final String projection; private final Set joinAliases; private final Set selectAliases; - private final byte @Nullable[] serialized; + private final byte @Nullable [] serialized; /** * @param query the query we want to enhance. Must not be {@literal null}. @@ -94,7 +95,7 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) { this.selectAliases = Collections.unmodifiableSet(getSelectionAliases(this.statement)); this.joinAliases = Collections.unmodifiableSet(getJoinAliases(this.statement)); byte[] tmp = SerializationUtils.serialize(this.statement); -// this.serialized = tmp != null ? tmp : new byte[0]; + // this.serialized = tmp != null ? tmp : new byte[0]; this.serialized = SerializationUtils.serialize(this.statement); } @@ -331,7 +332,7 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab } if (!(selectStatement instanceof PlainSelect selectBody)) { - if(selectStatement != null) { + if (selectStatement != null) { return selectStatement.toString(); } else { throw new IllegalArgumentException("Select must not be null"); @@ -372,8 +373,8 @@ public String createCountQueryFor(@Nullable String countProjection) { return createCountQueryFor(selectBody, countProjection, primaryAlias); } - private static String createCountQueryFor(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); @@ -526,8 +527,8 @@ enum ParsedType { * @param bytes a serialized object * @return the result of deserializing the bytes */ - private static @Nullable Object deserialize(byte @Nullable[] bytes) { - if(ObjectUtils.isEmpty(bytes)) { + private static @Nullable Object deserialize(byte @Nullable [] bytes) { + if (ObjectUtils.isEmpty(bytes)) { return null; } try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { @@ -539,9 +540,9 @@ enum ParsedType { } } - private static T deserializeRequired(byte @Nullable[] bytes, Class type) { + private static T deserializeRequired(byte @Nullable [] bytes, Class type) { Object deserialize = deserialize(bytes); - if(deserialize != null) { + if (deserialize != null) { return type.cast(deserialize); } throw new IllegalStateException("Failed to deserialize object type"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index 480ec3426d..6318d8acfd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -17,9 +17,9 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; -import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; import org.springframework.util.StringUtils; @@ -82,7 +82,7 @@ public QueryRendererBuilder visitSelect_clause(JpqlParser.Select_clauseContext c if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); nested.append(getDistinctCountSelection(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA))); - } else if(StringUtils.hasText(primaryFromAlias)) { + } else if (StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); } else { throw new IllegalStateException("No primary alias present"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java index f3e20a1d6c..298b095915 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java @@ -25,23 +25,25 @@ import java.util.Objects; -import org.springframework.data.mapping.PropertyPath; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.mapping.PropertyPath; import org.springframework.util.StringUtils; /** + * Utilities to create JPQL expressions, derived from {@link QueryUtils}. + * * @author Mark Paluch */ class JpqlUtils { - static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, JpqlQueryBuilder.Origin source, - Bindable from, PropertyPath property) { + static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, + JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property) { return toExpressionRecursively(metamodel, source, from, property, false); } - static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, JpqlQueryBuilder.Origin source, - Bindable from, PropertyPath property, boolean isForSelection) { + static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, + JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property, boolean isForSelection) { return toExpressionRecursively(metamodel, source, from, property, isForSelection, false); } @@ -54,8 +56,9 @@ static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamod * @param hasRequiredOuterJoin has a parent already required an outer join? * @return the expression */ - static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, JpqlQueryBuilder.Origin source, - Bindable from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { + static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, + JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property, boolean isForSelection, + boolean hasRequiredOuterJoin) { String segment = property.getSegment(); @@ -81,7 +84,7 @@ static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamod ManagedType managedTypeForModel = QueryUtils.getManagedTypeForModel(from); Attribute nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from); - if(nextAttribute == null) { + if (nextAttribute == null) { throw new IllegalStateException("Binding property is null"); } @@ -144,7 +147,7 @@ static boolean requiresOuterJoin(@Nullable Metamodel metamodel, Bindable bind } } - if(metamodel != null) { + if (metamodel != null) { Class fallbackType = fallback.getBindableJavaType(); try { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index f39505222f..504658726c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -26,9 +26,9 @@ import java.util.List; -import org.springframework.data.domain.KeysetScrollPosition; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.Specification; @@ -68,7 +68,8 @@ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntit } @Override - public @Nullable Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder criteriaBuilder) { + public @Nullable Predicate toPredicate(Root root, @Nullable CriteriaQuery query, + CriteriaBuilder criteriaBuilder) { return createPredicate(root, criteriaBuilder); } @@ -78,7 +79,6 @@ public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntit return delegate.createPredicate(position, sort, new CriteriaBuilderStrategy(root, criteriaBuilder)); } - public JpqlQueryBuilder.@Nullable Predicate createJpqlPredicate(Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { @@ -108,9 +108,9 @@ public Expression createExpression(String property) { @Override public Predicate compare(Order order, Expression propertyExpression, @Nullable Object value) { - if(value instanceof Comparable compareValue) { + if (value instanceof Comparable compareValue) { return order.isAscending() ? cb.greaterThan(propertyExpression, compareValue) - : cb.lessThan(propertyExpression, compareValue); + : cb.lessThan(propertyExpression, compareValue); } return order.isAscending() ? cb.isNull(propertyExpression) : cb.isNotNull(propertyExpression); @@ -139,12 +139,13 @@ private static class JpqlStrategy implements QueryStrategy from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { + public JpqlStrategy(@Nullable Metamodel metamodel, Bindable from, JpqlQueryBuilder.Entity entity, + ParameterFactory factory) { this.from = from; this.entity = entity; this.factory = factory; - this.metamodel = metamodel; + this.metamodel = metamodel; } @Override @@ -159,7 +160,7 @@ public JpqlQueryBuilder.Predicate compare(Order order, JpqlQueryBuilder.Expressi @Nullable Object value) { JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression); - if(value == null) { + if (value == null) { return order.isAscending() ? where.isNull() : where.isNotNull(); } return order.isAscending() ? where.gt(factory.capture(value)) : where.lt(factory.capture(value)); 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 9b931a34e7..41c572731e 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 @@ -46,9 +46,9 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.springframework.core.annotation.AnnotationUtils; - import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; @@ -875,7 +875,7 @@ static T getAnnotationProperty(Attribute attribute, String propertyNam } Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation); - if(annotation == null) { + if (annotation == null) { return defaultValue; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java index 6a63a8260e..266bd3e003 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityGraphFactory.java @@ -62,11 +62,13 @@ public static EntityGraph create(EntityManager entityManager, Class do if (path.hasNext()) { - if(current == null) { - current = existingSubgraphs.computeIfAbsent(currentFullPath, k -> entityGraph.addSubgraph(path.getSegment())); + if (current == null) { + current = existingSubgraphs.computeIfAbsent(currentFullPath, + k -> entityGraph.addSubgraph(path.getSegment())); } else { final Subgraph finalCurrent = current; - current = existingSubgraphs.computeIfAbsent(currentFullPath, k -> finalCurrent.addSubgraph(path.getSegment())); + current = existingSubgraphs.computeIfAbsent(currentFullPath, + k -> finalCurrent.addSubgraph(path.getSegment())); } continue; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java index ba882f244c..0b21210ff9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java @@ -29,8 +29,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; - -import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -227,7 +225,7 @@ private TypedQuery createSortedAndProjectedQuery(Sort sort) { private Slice readSlice(Pageable pageable) { - TypedQuery pagedQuery = createSortedAndProjectedQuery(); + TypedQuery pagedQuery = createSortedAndProjectedQuery(pageable.getSort()); if (pageable.isPaged()) { pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable)); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 2d5a95a27a..96d6277010 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -226,13 +226,14 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { } @Override - protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { + protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, + @Nullable BeanFactory beanFactory) { CollectionAwareProjectionFactory factory = new CollectionAwareProjectionFactory(); - if(classLoader != null) { + if (classLoader != null) { factory.setBeanClassLoader(classLoader); } - if(beanFactory != null) { + if (beanFactory != null) { factory.setBeanFactory(beanFactory); } @@ -243,11 +244,9 @@ protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoad protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, - new CachingValueExpressionDelegate(valueExpressionDelegate), - queryRewriterProvider, escapeCharacter)); + new CachingValueExpressionDelegate(valueExpressionDelegate), queryRewriterProvider, escapeCharacter)); } - @Override @SuppressWarnings("unchecked") public JpaEntityInformation getEntityInformation(Class domainClass) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index c28660cb92..8881ab84c0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -23,6 +23,8 @@ import java.util.function.BiFunction; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.KeysetScrollPosition; @@ -59,7 +61,6 @@ import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.AbstractJPAQuery; -import org.jspecify.annotations.Nullable; /** * Querydsl specific fragment for extending {@link SimpleJpaRepository} with an implementation of @@ -376,7 +377,7 @@ public Expression createExpression(String property) { @Override public BooleanExpression compare(Order order, Expression propertyExpression, @Nullable Object value) { - if(value == null) { + if (value == null) { return Expressions.booleanOperation(order.isAscending() ? Ops.IS_NULL : Ops.IS_NOT_NULL, propertyExpression); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 48632c09b6..4226891175 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -40,9 +40,9 @@ import java.util.function.BiConsumer; import java.util.function.Function; -import org.springframework.dao.InvalidDataAccessApiUsageException; - import org.jspecify.annotations.Nullable; + +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; 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 879158bfd8..05f16dc09b 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 @@ -2802,44 +2802,6 @@ void findByFluentSpecificationPageCustomCountSpec() { assertThat(page0.getTotalElements()).isEqualTo(3L); } - @Test // GH-2274 - void findByFluentSpecificationSlice() { - - flushTestUsers(); - - Slice slice = repository.findBy(userHasFirstnameLike("v"), - q -> q.sortBy(Sort.by("firstname")).slice(PageRequest.of(0, 2))); - - assertThat(slice).isNotInstanceOf(Page.class); - assertThat(slice.getContent()).containsExactly(thirdUser, firstUser); - assertThat(slice.hasNext()).isTrue(); - - slice = repository.findBy(userHasFirstnameLike("v"), - q -> q.sortBy(Sort.by("firstname")).slice(PageRequest.of(0, 3))); - - assertThat(slice).isNotInstanceOf(Page.class); - assertThat(slice).hasSize(3); - assertThat(slice.hasNext()).isFalse(); - } - - @Test // GH-3727 - void findByFluentSpecificationPageCustomCountSpec() { - - flushTestUsers(); - - Page page0 = repository.findBy(userHasFirstnameLike("v"), - q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2), (root, query, criteriaBuilder) -> null)); - - assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); - assertThat(page0.getTotalElements()).isEqualTo(4L); - - page0 = repository.findBy(userHasFirstnameLike("v"), - q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2))); - - assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); - assertThat(page0.getTotalElements()).isEqualTo(3L); - } - @Test // GH-2274, GH-3716 void findByFluentSpecificationWithInterfaceBasedProjection() { From 38f5c114e927e80ec9f1a3e4d119157689e2f8d0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 27 Jun 2024 11:14:02 +0200 Subject: [PATCH 48/94] Introduce `QueryEnhancerSelector` to configure which `QueryEnhancerFactory` to use. Introduce QueryEnhancerSelector to EnableJpaRepositories. Also, split DeclaredQuery into two interfaces to resolve the inner cycle of query introspection while just a value object is being created. Introduce JpaQueryConfiguration to capture a multitude of configuration elements. Remove `spring.data.jpa.query.native.parser` option introduced earlier with #2989 Closes #3622 Original pull request: #3527 --- .../repository/query/HqlParserBenchmarks.java | 4 +- .../JSqlParserQueryEnhancerBenchmarks.java | 2 +- .../config/EnableJpaRepositories.java | 29 ++- .../config/JpaRepositoryConfigExtension.java | 5 + .../query/AbstractStringBasedJpaQuery.java | 59 +++--- .../jpa/repository/query/DeclaredQuery.java | 87 ++------- .../query/DefaultDeclaredQuery.java | 68 +++++++ ...Query.java => EmptyIntrospectedQuery.java} | 20 ++- .../jpa/repository/query/EntityQuery.java | 107 +++++++++++ .../query/ExpressionBasedStringQuery.java | 33 ++-- .../repository/query/IntrospectedQuery.java | 53 ++++++ .../query/JpaQueryConfiguration.java | 57 ++++++ .../repository/query/JpaQueryEnhancer.java | 27 +-- .../jpa/repository/query/JpaQueryFactory.java | 68 ------- .../query/JpaQueryLookupStrategy.java | 126 +++++++------ .../data/jpa/repository/query/NamedQuery.java | 22 ++- .../jpa/repository/query/NativeJpaQuery.java | 10 +- .../query/ParameterBinderFactory.java | 16 +- .../jpa/repository/query/QueryEnhancer.java | 1 - .../query/QueryEnhancerFactories.java | 168 ++++++++++++++++++ .../query/QueryEnhancerFactory.java | 124 ++----------- .../query/QueryEnhancerSelector.java | 93 ++++++++++ .../query/QueryParameterSetterFactory.java | 31 +--- .../data/jpa/repository/query/QueryUtils.java | 6 +- .../jpa/repository/query/SimpleJpaQuery.java | 33 +--- .../jpa/repository/query/StringQuery.java | 77 ++++++-- .../support/JpaRepositoryFactory.java | 34 ++-- .../support/JpaRepositoryFactoryBean.java | 65 ++++++- ...ctStringBasedJpaQueryIntegrationTests.java | 7 +- .../AbstractStringBasedJpaQueryUnitTests.java | 13 +- .../query/DefaultQueryEnhancerUnitTests.java | 6 +- .../EqlParserQueryEnhancerUnitTests.java | 2 +- .../query/EqlQueryTransformerTests.java | 2 +- .../ExpressionBasedStringQueryUnitTests.java | 38 ++-- .../HqlParserQueryEnhancerUnitTests.java | 2 +- .../query/HqlQueryTransformerTests.java | 2 +- .../JSqlParserQueryEnhancerUnitTests.java | 26 +-- .../JpaQueryLookupStrategyUnitTests.java | 17 +- .../JpaQueryRewriteIntegrationTests.java | 25 ++- .../JpqlParserQueryEnhancerUnitTests.java | 2 +- .../query/JpqlQueryTransformerTests.java | 2 +- .../repository/query/NamedQueryUnitTests.java | 6 +- .../query/NativeJpaQueryUnitTests.java | 4 +- .../query/QueryEnhancerFactoryUnitTests.java | 82 +-------- .../query/QueryEnhancerTckTests.java | 11 +- .../query/QueryEnhancerUnitTests.java | 18 +- .../QueryParameterSetterFactoryUnitTests.java | 28 +-- .../query/SimpleJpaQueryUnitTests.java | 42 ++--- .../query/StringQueryUnitTests.java | 8 +- 49 files changed, 1064 insertions(+), 704 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{EmptyDeclaredQuery.java => EmptyIntrospectedQuery.java} (77%) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java index fd46a3f6c2..fb524d76bf 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java @@ -55,8 +55,8 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" """; - query = DeclaredQuery.of(s, false); - enhancer = QueryEnhancerFactory.forQuery(query); + query = DeclaredQuery.ofJpql(s); + enhancer = QueryEnhancerFactory.forQuery(query).create(query); } } diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java index 845282e319..aeb1764c5c 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java @@ -56,7 +56,7 @@ public void doSetup() throws IOException { select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; - enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.of(s, true)); + enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.ofNative(s)); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java index 3ff333ea7c..68a173f059 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.config.BootstrapMode; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; @@ -83,46 +84,39 @@ * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning * for {@code PersonRepositoryImpl}. - * - * @return */ String repositoryImplementationPostfix() default "Impl"; /** * Configures the location of where to find the Spring Data named queries properties file. Will default to * {@code META-INF/jpa-named-queries.properties}. - * - * @return */ String namedQueriesLocation() default ""; /** * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to * {@link Key#CREATE_IF_NOT_FOUND}. - * - * @return */ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; /** * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to * {@link JpaRepositoryFactoryBean}. - * - * @return */ Class repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class; /** * Configure the repository base class to be used to create repository proxies for this particular configuration. * - * @return * @since 1.9 */ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** * Configure a specific {@link BeanNameGenerator} to be used when creating the repository beans. - * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate context default. + * + * @return the {@link BeanNameGenerator} to be used or the base {@link BeanNameGenerator} interface to indicate + * context default. * @since 3.4 */ Class nameGenerator() default BeanNameGenerator.class; @@ -132,22 +126,18 @@ /** * Configures the name of the {@link EntityManagerFactory} bean definition to be used to create repositories * discovered through this annotation. Defaults to {@code entityManagerFactory}. - * - * @return */ String entityManagerFactoryRef() default "entityManagerFactory"; /** * Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories * discovered through this annotation. Defaults to {@code transactionManager}. - * - * @return */ String transactionManagerRef() default "transactionManager"; /** * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the - * repositories infrastructure. + * repository infrastructure. */ boolean considerNestedRepositories() default false; @@ -169,7 +159,6 @@ * completed its bootstrap. {@link BootstrapMode#DEFERRED} is fundamentally the same as {@link BootstrapMode#LAZY}, * but triggers repository initialization when the application context finishes its bootstrap. * - * @return * @since 2.1 */ BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT; @@ -181,4 +170,12 @@ * @return a single character used for escaping. */ char escapeCharacter() default '\\'; + + /** + * Configures the {@link QueryEnhancerSelector} to select a query enhancer for query introspection and transformation. + * + * @return a {@link QueryEnhancerSelector} class providing a no-args constructor. + * @since 4.0 + */ + Class queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 32b8670802..7abdd4758e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -122,6 +122,11 @@ public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSo } builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\')); builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME); + + if (source instanceof AnnotationRepositoryConfigurationSource) { + builder.addPropertyValue("queryEnhancerSelector", + source.getAttribute("queryEnhancerSelector", Class.class).orElse(null)); + } } @Override 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 a7b0b04e42..30c47bf8ec 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 @@ -49,8 +49,8 @@ */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final DeclaredQuery query; - private final Lazy countQuery; + private final StringQuery query; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -65,37 +65,32 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { * @param em must not be {@literal null}. * @param queryString must not be {@literal null}. * @param countQueryString must not be {@literal null}. - * @param queryRewriter must not be {@literal null}. - * @param valueExpressionDelegate must not be {@literal null}. + * @param queryConfiguration must not be {@literal null}. */ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { + @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { super(method, em); Assert.hasText(queryString, "Query string must not be null or empty"); - Assert.notNull(valueExpressionDelegate, "ValueExpressionDelegate must not be null"); - Assert.notNull(queryRewriter, "QueryRewriter must not be null"); + Assert.notNull(queryConfiguration, "JpaQueryConfiguration must not be null"); - this.valueExpressionDelegate = valueExpressionDelegate; + this.valueExpressionDelegate = queryConfiguration.getValueExpressionDelegate(); this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); - this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), valueExpressionDelegate, - method.isNativeQuery()); + this.query = ExpressionBasedStringQuery.create(queryString, method, queryConfiguration); this.countQuery = Lazy.of(() -> { if (StringUtils.hasText(countQueryString)) { - - return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), valueExpressionDelegate, - method.isNativeQuery()); + return ExpressionBasedStringQuery.create(countQueryString, method, queryConfiguration); } - return query.deriveCountQuery(method.getCountQueryProjection()); + return this.query.deriveCountQuery(method.getCountQueryProjection()); }); this.countParameterBinder = Lazy.of(() -> this.createBinder(this.countQuery.get())); - this.queryRewriter = queryRewriter; + this.queryRewriter = queryConfiguration.getQueryRewriter(method); JpaParameters parameters = method.getParameters(); @@ -109,7 +104,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri } } - Assert.isTrue(method.isNativeQuery() || !query.usesJdbcStyleParameters(), + Assert.isTrue(method.isNativeQuery() || !this.query.usesJdbcStyleParameters(), "JDBC style parameters (?) are not supported for JPA queries"); } @@ -136,7 +131,7 @@ protected ParameterBinder createBinder() { return createBinder(query); } - protected ParameterBinder createBinder(DeclaredQuery query) { + protected ParameterBinder createBinder(IntrospectedQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, valueExpressionDelegate, valueExpressionContextProvider); } @@ -162,14 +157,14 @@ protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) { /** * @return the query */ - public DeclaredQuery getQuery() { + public EntityQuery getQuery() { return query; } /** * @return the countQuery */ - public DeclaredQuery getCountQuery() { + public IntrospectedQuery getCountQuery() { return countQuery.get(); } @@ -211,8 +206,7 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla } String applySorting(CachableQuery cachableQuery) { - - return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()) + return cachableQuery.getDeclaredQuery().getQueryEnhancer() .rewrite(new DefaultQueryRewriteInformation(cachableQuery.getSort(), cachableQuery.getReturnedType())); } @@ -220,7 +214,7 @@ String applySorting(CachableQuery cachableQuery) { * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType); + String getSorted(StringQuery query, Sort sort, ReturnedType returnedType); } /** @@ -230,9 +224,8 @@ enum SimpleQuerySortRewriter implements QuerySortRewriter { INSTANCE; - public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { - - return QueryEnhancerFactory.forQuery(query).rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); + public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { + return query.getQueryEnhancer().rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } } @@ -240,7 +233,7 @@ static class UnsortedCachingQuerySortRewriter implements QuerySortRewriter { private volatile @Nullable String cachedQueryString; - public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { if (sort.isSorted()) { throw new UnsupportedOperationException("NoOpQueryCache does not support sorting"); @@ -248,7 +241,7 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp String cachedQueryString = this.cachedQueryString; if (cachedQueryString == null) { - this.cachedQueryString = cachedQueryString = QueryEnhancerFactory.forQuery(query) + this.cachedQueryString = cachedQueryString = query.getQueryEnhancer() .rewrite(new DefaultQueryRewriteInformation(sort, returnedType)); } @@ -267,7 +260,7 @@ class CachingQuerySortRewriter implements QuerySortRewriter { private volatile @Nullable String cachedQueryString; @Override - public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) { if (sort.isUnsorted()) { @@ -292,21 +285,21 @@ public String getSorted(DeclaredQuery query, Sort sort, ReturnedType returnedTyp */ static class CachableQuery { - private final DeclaredQuery declaredQuery; + private final StringQuery query; private final String queryString; private final Sort sort; private final ReturnedType returnedType; - CachableQuery(DeclaredQuery query, Sort sort, ReturnedType returnedType) { + CachableQuery(StringQuery query, Sort sort, ReturnedType returnedType) { - this.declaredQuery = query; + this.query = query; this.queryString = query.getQueryString(); this.sort = sort; this.returnedType = returnedType; } - DeclaredQuery getDeclaredQuery() { - return declaredQuery; + StringQuery getDeclaredQuery() { + return query; } Sort getSort() { 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 0e6f760ed3..ca32d1f46b 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 @@ -15,100 +15,45 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.List; - -import org.springframework.util.ObjectUtils; - -import org.jspecify.annotations.Nullable; - /** - * A wrapper for a String representation of a query offering information about the query. + * Interface defining the contract to represent a declared query. * * @author Jens Schauder * @author Diego Krupitza + * @author Mark Paluch * @since 2.0.3 */ -interface DeclaredQuery { +public interface DeclaredQuery { /** - * Creates a {@literal DeclaredQuery} from a query {@literal String}. + * Creates a DeclaredQuery for a JPQL query. * - * @param query might be {@literal null} or empty. - * @param nativeQuery is a given query is native or not - * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument. + * @param query the JPQL query string. + * @return */ - static DeclaredQuery of(@Nullable String query, boolean nativeQuery) { - return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery); + static DeclaredQuery ofJpql(String query) { + return new DefaultDeclaredQuery(query, false); } /** - * @return whether the underlying query has at least one named parameter. - */ - boolean hasNamedParameter(); - - /** - * Returns the query string. - */ - String getQueryString(); - - /** - * Returns the main alias used in the query. - * - * @return the alias - */ - @Nullable - String getAlias(); - - /** - * Returns whether the query is using a constructor expression. - * - * @since 1.10 - */ - boolean hasConstructorExpression(); - - /** - * 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 getParameterBindings(); - - /** - * Creates a new {@literal DeclaredQuery} 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. + * Creates a DeclaredQuery for a native query. * - * @param countQueryProjection an optional return type for the query. - * @return a new {@literal DeclaredQuery} instance. - */ - DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection); - - /** - * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. - * @since 2.0.6 + * @param query the native query string. + * @return */ - default boolean usesPaging() { - return false; + static DeclaredQuery ofNative(String query) { + return new DefaultDeclaredQuery(query, true); } /** - * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or - * name. - * - * @return Whether the query uses JDBC style parameters. - * @since 2.0.6 + * Returns the query string. */ - boolean usesJdbcStyleParameters(); + String getQueryString(); /** * Return whether the query is a native query of not. * * @return true if native query otherwise false */ - default boolean isNativeQuery() { - return false; - } + boolean isNativeQuery(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java new file mode 100644 index 0000000000..a24512a994 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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; + +/** + * @author Mark Paluch + */ +class DefaultDeclaredQuery implements DeclaredQuery { + + private final String query; + private final boolean nativeQuery; + + DefaultDeclaredQuery(String query, boolean nativeQuery) { + this.query = query; + this.nativeQuery = nativeQuery; + } + + @Override + public String getQueryString() { + return query; + } + + @Override + public boolean isNativeQuery() { + return nativeQuery; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof DefaultDeclaredQuery that)) { + return false; + } + if (nativeQuery != that.nativeQuery) { + return false; + } + return ObjectUtils.nullSafeEquals(query, that.query); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(query); + result = 31 * result + (nativeQuery ? 1 : 0); + return result; + } + + @Override + public String toString() { + return (isNativeQuery() ? "[native] " : "[JPQL] ") + getQueryString(); + } +} 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/EmptyIntrospectedQuery.java similarity index 77% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyDeclaredQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index 95693e8808..c51f0c4ca4 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/EmptyIntrospectedQuery.java @@ -18,20 +18,21 @@ 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 DeclaredQuery}. + * NULL-Object pattern implementation for {@link IntrospectedQuery}. * * @author Jens Schauder * @since 2.0.3 */ -class EmptyDeclaredQuery implements DeclaredQuery { +class EmptyIntrospectedQuery implements EntityQuery { /** * An implementation implementing the NULL-Object pattern for situations where there is no query. */ - static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery(); + static final EntityQuery EMPTY_QUERY = new EmptyIntrospectedQuery(); @Override public boolean hasNamedParameter() { @@ -43,11 +44,15 @@ public String getQueryString() { return ""; } - @Override public @Nullable String getAlias() { return null; } + @Override + public boolean isNativeQuery() { + return false; + } + @Override public boolean hasConstructorExpression() { return false; @@ -64,10 +69,15 @@ public List getParameterBindings() { } @Override - public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { + public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { return EMPTY_QUERY; } + @Override + public String applySorting(Sort sort) { + return ""; + } + @Override public boolean usesJdbcStyleParameters() { return false; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java new file mode 100644 index 0000000000..b959d3810e --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -0,0 +1,107 @@ +/* + * Copyright 2018-2024 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.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A wrapper for a String representation of a query offering information about the query. + * + * @author Jens Schauder + * @author Diego Krupitza + * @since 2.0.3 + */ +interface EntityQuery extends IntrospectedQuery { + + /** + * Creates a DeclaredQuery for a JPQL query. + * + * @param query the JPQL query string. + * @return + */ + static EntityQuery introspectJpql(String query, QueryEnhancerFactory queryEnhancer) { + return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY + : new StringQuery(query, false, queryEnhancer, parameterBindings -> {}); + } + + /** + * 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 -> {}); + } + + /** + * 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 -> {}); + } + + /** + * Returns whether the query is using a constructor expression. + * + * @since 1.10 + */ + boolean hasConstructorExpression(); + + /** + * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query. + */ + 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 + */ + default boolean usesPaging() { + 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 a414b52005..b6c93b5604 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 @@ -30,7 +30,7 @@ /** * 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. *
      @@ -66,25 +66,13 @@ class ExpressionBasedStringQuery extends StringQuery { * @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 nativeQuery is a given query is native or not. + * @param selector must not be {@literal null}. */ - public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser, - boolean nativeQuery) { - super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query)); - } - - /** - * Creates an {@link ExpressionBasedStringQuery} from a given {@link DeclaredQuery}. - * - * @param query the original query. Must not be {@literal null}. - * @param metadata the {@link JpaEntityMetadata} for the given entity. Must not be {@literal null}. - * @param parser Parser for resolving SpEL expressions. Must not be {@literal null}. - * @param nativeQuery is a given query native or not - * @return A query supporting SpEL expressions. - */ - static ExpressionBasedStringQuery from(DeclaredQuery query, JpaEntityMetadata metadata, - ValueExpressionParser parser, boolean nativeQuery) { - return new ExpressionBasedStringQuery(query.getQueryString(), metadata, parser, nativeQuery); + ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser, + boolean nativeQuery, QueryEnhancerSelector selector) { + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query), + selector, parameterBindings -> {}); } /** @@ -131,4 +119,11 @@ private static String potentiallyQuoteExpressionsParameter(String query) { private static boolean containsExpression(String query) { 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()); + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java new file mode 100644 index 0000000000..427dbcc03b --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018-2024 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; + +/** + * A wrapper for a String representation of a query offering information about the query. + * + * @author Jens Schauder + * @author Diego Krupitza + * @since 2.0.3 + */ +interface IntrospectedQuery extends DeclaredQuery { + + /** + * @return whether the underlying query has at least one named 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 getParameterBindings(); + + /** + * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or + * name. + * + * @return Whether the query uses JDBC style parameters. + * @since 2.0.6 + */ + boolean usesJdbcStyleParameters(); + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java new file mode 100644 index 0000000000..7bce8dc8f7 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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.data.jpa.repository.QueryRewriter; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * Configuration object holding configuration information for JPA queries within a repository. + * + * @author Mark Paluch + */ +public class JpaQueryConfiguration { + + private final QueryRewriterProvider queryRewriter; + private final QueryEnhancerSelector selector; + private final EscapeCharacter escapeCharacter; + private final ValueExpressionDelegate valueExpressionDelegate; + + public JpaQueryConfiguration(QueryRewriterProvider queryRewriter, QueryEnhancerSelector selector, + ValueExpressionDelegate valueExpressionDelegate, EscapeCharacter escapeCharacter) { + + this.queryRewriter = queryRewriter; + this.selector = selector; + this.escapeCharacter = escapeCharacter; + this.valueExpressionDelegate = valueExpressionDelegate; + } + + public QueryRewriter getQueryRewriter(JpaQueryMethod queryMethod) { + return queryRewriter.getQueryRewriter(queryMethod); + } + + public QueryEnhancerSelector getSelector() { + return selector; + } + + public EscapeCharacter getEscapeCharacter() { + return escapeCharacter; + } + + public ValueExpressionDelegate getValueExpressionDelegate() { + return valueExpressionDelegate; + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index 1b57f4beb0..ff4b6efb7d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -142,43 +142,34 @@ static void configureParser(String query, String grammar, Lexer lexer, Parser pa } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using JPQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using JPQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using JPQL. */ - public static JpaQueryEnhancer forJpql(DeclaredQuery query) { - - Assert.notNull(query, "DeclaredQuery must not be null!"); - - return JpqlQueryParser.parseQuery(query.getQueryString()); + public static JpaQueryEnhancer forJpql(String query) { + return JpqlQueryParser.parseQuery(query); } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using HQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using HQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using HQL. */ - public static JpaQueryEnhancer forHql(DeclaredQuery query) { - - Assert.notNull(query, "DeclaredQuery must not be null!"); - - return HqlQueryParser.parseQuery(query.getQueryString()); + public static JpaQueryEnhancer forHql(String query) { + return HqlQueryParser.parseQuery(query); } /** - * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using EQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link IntrospectedQuery} using EQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using EQL. * @since 3.2 */ - public static JpaQueryEnhancer forEql(DeclaredQuery query) { - - Assert.notNull(query, "DeclaredQuery must not be null!"); - - return EqlQueryParser.parseQuery(query.getQueryString()); + public static JpaQueryEnhancer forEql(String query) { + return EqlQueryParser.parseQuery(query); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java deleted file mode 100644 index 384330af14..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2013-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 org.springframework.data.jpa.repository.QueryRewriter; - -import org.jspecify.annotations.Nullable; -import org.springframework.data.repository.query.QueryCreationException; -import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.repository.query.ValueExpressionDelegate; - -/** - * Factory to create the appropriate {@link RepositoryQuery} for a {@link JpaQueryMethod}. - * - * @author Thomas Darimont - * @author Mark Paluch - */ -enum JpaQueryFactory { - - INSTANCE; - - /** - * Creates a {@link RepositoryQuery} from the given {@link String} query. - */ - AbstractJpaQuery fromMethodWithQueryString(JpaQueryMethod method, EntityManager em, String queryString, - @Nullable String countQueryString, QueryRewriter queryRewriter, - ValueExpressionDelegate valueExpressionDelegate) { - - 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, queryRewriter, valueExpressionDelegate) - : new SimpleJpaQuery(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate); - } - - /** - * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query. - * - * @param method must not be {@literal null}. - * @param em must not be {@literal null}. - * @return - */ - public StoredProcedureJpaQuery fromProcedureAnnotation(JpaQueryMethod method, EntityManager em) { - - if (method.isScrollQuery()) { - throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures"); - } - - return new StoredProcedureJpaQuery(method, em); - } -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index d4b58f9429..b032000ba3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -24,10 +24,10 @@ import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethod; @@ -70,33 +70,31 @@ private abstract static class AbstractQueryLookupStrategy implements QueryLookup private final EntityManager em; private final JpaQueryMethodFactory queryMethodFactory; - private final QueryRewriterProvider queryRewriterProvider; + private final JpaQueryConfiguration configuration; /** * Creates a new {@link AbstractQueryLookupStrategy}. * * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public AbstractQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - QueryRewriterProvider queryRewriterProvider) { - - Assert.notNull(em, "EntityManager must not be null"); - Assert.notNull(queryMethodFactory, "JpaQueryMethodFactory must not be null"); + JpaQueryConfiguration configuration) { this.em = em; this.queryMethodFactory = queryMethodFactory; - this.queryRewriterProvider = queryRewriterProvider; + this.configuration = configuration; } @Override public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { JpaQueryMethod queryMethod = queryMethodFactory.build(method, metadata, factory); - return resolveQuery(queryMethod, queryRewriterProvider.getQueryRewriter(queryMethod), em, namedQueries); + return resolveQuery(queryMethod, configuration, em, namedQueries); } - protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, + protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries); } @@ -109,20 +107,16 @@ protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewr */ private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy { - private final EscapeCharacter escape; - public CreateQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - QueryRewriterProvider queryRewriterProvider, EscapeCharacter escape) { + JpaQueryConfiguration configuration) { - super(em, queryMethodFactory, queryRewriterProvider); - - this.escape = escape; + super(em, queryMethodFactory, configuration); } @Override - protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em, + protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries) { - return new PartTreeJpaQuery(method, em, escape); + return new PartTreeJpaQuery(method, em, configuration.getEscapeCharacter()); } } @@ -134,32 +128,27 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer * @author Thomas Darimont * @author Jens Schauder */ - private static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy { - - private final ValueExpressionDelegate valueExpressionDelegate; + static class DeclaredQueryLookupStrategy extends AbstractQueryLookupStrategy { /** * Creates a new {@link DeclaredQueryLookupStrategy}. * * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. - * @param delegate must not be {@literal null}. - * @param queryRewriterProvider must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public DeclaredQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider) { + JpaQueryConfiguration configuration) { - super(em, queryMethodFactory, queryRewriterProvider); - - this.valueExpressionDelegate = delegate; + super(em, queryMethodFactory, configuration); } @Override - protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em, + protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries) { if (method.isProcedureQuery()) { - return JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em); + return createProcedureQuery(method, em); } if (StringUtils.hasText(method.getAnnotatedQuery())) { @@ -169,17 +158,17 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer "Query method %s is annotated with both, a query and a query name; Using the declared query", method)); } - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(), - getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate); + return createStringQuery(method, em, method.getRequiredAnnotatedQuery(), + getCountQuery(method, namedQueries, em), configuration); } String name = method.getNamedQueryName(); if (namedQueries.hasQuery(name)) { - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), - getCountQuery(method, namedQueries, em), queryRewriter, valueExpressionDelegate); + return createStringQuery(method, em, namedQueries.getQuery(name), getCountQuery(method, namedQueries, em), + configuration); } - RepositoryQuery query = NamedQuery.lookupFrom(method, em, queryRewriter); + RepositoryQuery query = NamedQuery.lookupFrom(method, em, configuration.getSelector()); return query != null ? query : NO_QUERY; } @@ -208,6 +197,44 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer return null; } + + /** + * Creates a {@link RepositoryQuery} from the given {@link String} query. + * + * @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 configuration must not be {@literal null}. + * @return + */ + static AbstractJpaQuery createStringQuery(JpaQueryMethod method, EntityManager em, String queryString, + @Nullable String countQueryString, 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); + } + + /** + * Creates a {@link StoredProcedureJpaQuery} from the given {@link JpaQueryMethod} query. + * + * @param method must not be {@literal null}. + * @param em must not be {@literal null}. + * @return + */ + static StoredProcedureJpaQuery createProcedureQuery(JpaQueryMethod method, EntityManager em) { + + if (method.isScrollQuery()) { + throw QueryCreationException.create(method, "Scroll queries are not supported using stored procedures"); + } + + return new StoredProcedureJpaQuery(method, em); + } + } /** @@ -230,31 +257,29 @@ private static class CreateIfNotFoundQueryLookupStrategy extends AbstractQueryLo * @param queryMethodFactory must not be {@literal null}. * @param createStrategy must not be {@literal null}. * @param lookupStrategy must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public CreateIfNotFoundQueryLookupStrategy(EntityManager em, JpaQueryMethodFactory queryMethodFactory, CreateQueryLookupStrategy createStrategy, DeclaredQueryLookupStrategy lookupStrategy, - QueryRewriterProvider queryRewriterProvider) { - - super(em, queryMethodFactory, queryRewriterProvider); + JpaQueryConfiguration configuration) { - Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null"); - Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null"); + super(em, queryMethodFactory, configuration); this.createStrategy = createStrategy; this.lookupStrategy = lookupStrategy; } @Override - protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter queryRewriter, EntityManager em, + protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfiguration configuration, EntityManager em, NamedQueries namedQueries) { - RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, queryRewriter, em, namedQueries); + RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, configuration, em, namedQueries); if (lookupQuery != NO_QUERY) { return lookupQuery; } - return createStrategy.resolveQuery(method, queryRewriter, em, namedQueries); + return createStrategy.resolveQuery(method, configuration, em, namedQueries); } } @@ -264,25 +289,20 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, QueryRewriter quer * @param em must not be {@literal null}. * @param queryMethodFactory must not be {@literal null}. * @param key may be {@literal null}. - * @param delegate must not be {@literal null}. - * @param queryRewriterProvider must not be {@literal null}. - * @param escape must not be {@literal null}. + * @param configuration must not be {@literal null}. */ public static QueryLookupStrategy create(EntityManager em, JpaQueryMethodFactory queryMethodFactory, - @Nullable Key key, ValueExpressionDelegate delegate, QueryRewriterProvider queryRewriterProvider, - EscapeCharacter escape) { + @Nullable Key key, JpaQueryConfiguration configuration) { Assert.notNull(em, "EntityManager must not be null"); - Assert.notNull(delegate, "ValueExpressionDelegate must not be null"); + Assert.notNull(configuration, "JpaQueryConfiguration must not be null"); return switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) { - case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape); - case USE_DECLARED_QUERY -> - new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider); + case CREATE -> new CreateQueryLookupStrategy(em, queryMethodFactory, configuration); + case USE_DECLARED_QUERY -> new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration); case CREATE_IF_NOT_FOUND -> new CreateIfNotFoundQueryLookupStrategy(em, queryMethodFactory, - new CreateQueryLookupStrategy(em, queryMethodFactory, queryRewriterProvider, escape), - new DeclaredQueryLookupStrategy(em, queryMethodFactory, delegate, queryRewriterProvider), - queryRewriterProvider); + new CreateQueryLookupStrategy(em, queryMethodFactory, configuration), + new DeclaredQueryLookupStrategy(em, queryMethodFactory, configuration), configuration); default -> throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key)); }; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index c0c133f4cf..6f4138760c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -55,12 +55,12 @@ final class NamedQuery extends AbstractJpaQuery { private final String countQueryName; private final @Nullable String countProjection; private final boolean namedCountQueryIsPresent; - private final Lazy declaredQuery; + private final Lazy entityQuery; /** * Creates a new {@link NamedQuery}. */ - private NamedQuery(JpaQueryMethod method, EntityManager em, QueryRewriter queryRewriter) { + private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelector selector, QueryRewriter queryRewriter) { super(method, em); @@ -96,8 +96,12 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryRewriter queryR String queryString = extractor.extractQueryString(query); - this.declaredQuery = Lazy - .of(() -> DeclaredQuery.of(queryString, method.isNativeQuery() || query.toString().contains("NativeQuery"))); + // TODO: What is queryString is null? + if (method.isNativeQuery() || (query != null && query.toString().contains("NativeQuery"))) { + this.entityQuery = Lazy.of(() -> EntityQuery.introspectNativeQuery(queryString, selector)); + } else { + this.entityQuery = Lazy.of(() -> EntityQuery.introspectJpql(queryString, selector)); + } } /** @@ -130,10 +134,10 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { * * @param method must not be {@literal null}. * @param em must not be {@literal null}. - * @param queryRewriter must not be {@literal null}. + * @param selector must not be {@literal null}. */ public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, - QueryRewriter queryRewriter) { + QueryEnhancerSelector selector) { String queryName = method.getNamedQueryName(); @@ -151,7 +155,7 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { method.isNativeQuery() ? "NativeQuery" : "Query")); } - RepositoryQuery query = new NamedQuery(method, em, queryRewriter); + RepositoryQuery query = new NamedQuery(method, em, selector); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Found named query '%s'", queryName)); } @@ -188,7 +192,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc } else { - String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString(); + String countQueryString = entityQuery.get().deriveCountQuery(countProjection).getQueryString(); countQueryString = potentiallyRewriteQuery(countQueryString, accessor.getSort(), accessor.getPageable()); cacheKey = countQueryString; countQuery = em.createQuery(countQueryString, Long.class); @@ -219,7 +223,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc return type.isInterface() ? Tuple.class : null; } - return declaredQuery.get().hasConstructorExpression() // + return entityQuery.get().hasConstructorExpression() // ? null // : super.getTypeToRead(returnedType); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index ae240942d5..4c2fefe23f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -26,7 +26,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.NativeQuery; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -43,7 +42,7 @@ * @author Mark Paluch * @author Greg Turnquist */ -final class NativeJpaQuery extends AbstractStringBasedJpaQuery { +class NativeJpaQuery extends AbstractStringBasedJpaQuery { private final @Nullable String sqlResultSetMapping; @@ -56,13 +55,12 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { * @param em must not be {@literal null}. * @param queryString must not be {@literal null} or empty. * @param countQueryString must not be {@literal null} or empty. - * @param rewriter the query rewriter to use. - * @param valueExpressionDelegate must not be {@literal null}. + * @param queryConfiguration must not be {@literal null}. */ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, - QueryRewriter rewriter, ValueExpressionDelegate valueExpressionDelegate) { + JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, rewriter, valueExpressionDelegate); + super(method, em, queryString, countQueryString, queryConfiguration); MergedAnnotations annotations = MergedAnnotations.from(method.getMethod()); MergedAnnotation annotation = annotations.get(NativeQuery.class); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 384d5c16d7..8abf7d461d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -84,7 +84,7 @@ static ParameterBinder createBinder(JpaParameters parameters, List getBindings(JpaParameters parameters) { @@ -124,26 +126,26 @@ static List getBindings(JpaParameters parameters) { private static Iterable createSetters(List parameterBindings, QueryParameterSetterFactory... factories) { - return createSetters(parameterBindings, EmptyDeclaredQuery.EMPTY_QUERY, factories); + return createSetters(parameterBindings, EmptyIntrospectedQuery.EMPTY_QUERY, factories); } private static Iterable createSetters(List parameterBindings, - DeclaredQuery declaredQuery, QueryParameterSetterFactory... strategies) { + IntrospectedQuery query, QueryParameterSetterFactory... strategies) { List setters = new ArrayList<>(parameterBindings.size()); for (ParameterBinding parameterBinding : parameterBindings) { - setters.add(createQueryParameterSetter(parameterBinding, strategies, declaredQuery)); + setters.add(createQueryParameterSetter(parameterBinding, strategies, query)); } return setters; } private static QueryParameterSetter createQueryParameterSetter(ParameterBinding binding, - QueryParameterSetterFactory[] strategies, DeclaredQuery declaredQuery) { + QueryParameterSetterFactory[] strategies, IntrospectedQuery query) { for (QueryParameterSetterFactory strategy : strategies) { - QueryParameterSetter setter = strategy.create(binding); + QueryParameterSetter setter = strategy.create(binding, query); if (setter != null) { return setter; 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 65304dcbba..ff9f44c44a 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 @@ -66,7 +66,6 @@ public interface QueryEnhancer { * * @return non-null {@link DeclaredQuery} that wraps the query. */ - @Deprecated(forRemoval = true) DeclaredQuery getQuery(); /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java new file mode 100644 index 0000000000..b88a6953f0 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.util.ClassUtils; + +/** + * Pre-defined QueryEnhancerFactories to be used for query enhancement. + * + * @author Mark Paluch + */ +public class QueryEnhancerFactories { + + private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); + + static final boolean jSqlParserPresent = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", + QueryEnhancerFactory.class.getClassLoader()); + + static { + + if (jSqlParserPresent) { + LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used"); + } + + if (PersistenceProvider.ECLIPSELINK.isPresent()) { + LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used."); + } + + if (PersistenceProvider.HIBERNATE.isPresent()) { + LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used."); + } + } + + enum BuiltinQueryEnhancerFactories implements QueryEnhancerFactory { + + FALLBACK { + @Override + public boolean supports(DeclaredQuery query) { + return true; + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return new DefaultQueryEnhancer(query); + } + }, + + JSQLPARSER { + @Override + public boolean supports(DeclaredQuery query) { + return query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + if (jSqlParserPresent) { + return new JSqlParserQueryEnhancer(query); + } + + throw new IllegalStateException("JSQLParser is not available on the class path"); + } + }, + + HQL { + @Override + public boolean supports(DeclaredQuery query) { + return !query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return JpaQueryEnhancer.forHql(query.getQueryString()); + } + }, + EQL { + @Override + public boolean supports(DeclaredQuery query) { + return !query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return JpaQueryEnhancer.forEql(query.getQueryString()); + } + }, + JPQL { + @Override + public boolean supports(DeclaredQuery query) { + return !query.isNativeQuery(); + } + + @Override + public QueryEnhancer create(DeclaredQuery query) { + return JpaQueryEnhancer.forJpql(query.getQueryString()); + } + } + } + + /** + * Returns the default fallback {@link QueryEnhancerFactory} using regex-based detection. This factory supports only + * simple SQL queries. + * + * @return fallback {@link QueryEnhancerFactory} using regex-based detection. + */ + public static QueryEnhancerFactory fallback() { + return BuiltinQueryEnhancerFactories.FALLBACK; + } + + /** + * Returns a {@link QueryEnhancerFactory} that uses JSqlParser + * if it is available from the class path. + * + * @return a {@link QueryEnhancerFactory} that uses JSqlParser. + * @throws IllegalStateException if JSQLParser is not on the class path. + */ + public static QueryEnhancerFactory jsqlparser() { + + if (!jSqlParserPresent) { + throw new IllegalStateException("JSQLParser is not available on the class path"); + } + + return BuiltinQueryEnhancerFactories.JSQLPARSER; + } + + /** + * Returns a {@link QueryEnhancerFactory} using HQL (Hibernate Query Language) parser. + * + * @return a {@link QueryEnhancerFactory} using HQL (Hibernate Query Language) parser. + */ + public static QueryEnhancerFactory hql() { + return BuiltinQueryEnhancerFactories.HQL; + } + + /** + * Returns a {@link QueryEnhancerFactory} using EQL (EclipseLink Query Language) parser. + * + * @return a {@link QueryEnhancerFactory} using EQL (EclipseLink Query Language) parser. + */ + public static QueryEnhancerFactory eql() { + return BuiltinQueryEnhancerFactories.EQL; + } + + /** + * Returns a {@link QueryEnhancerFactory} using JPQL (Jakarta Persistence Query Language) parser as per the JPA spec. + * + * @return a {@link QueryEnhancerFactory} using JPQL (Jakarta Persistence Query Language) parser as per the JPA spec. + */ + public static QueryEnhancerFactory jpql() { + return BuiltinQueryEnhancerFactories.JPQL; + } +} 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 5a2853cb1a..a3e7b5f06d 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,133 +15,41 @@ */ package org.springframework.data.jpa.repository.query; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.core.SpringProperties; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - /** - * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link DeclaredQuery}. + * Encapsulates different strategies for the creation of a {@link QueryEnhancer} from a {@link IntrospectedQuery}. * * @author Diego Krupitza * @author Greg Turnquist * @author Mark Paluch * @author Christoph Strobl - * @since 2.7.0 + * @since 2.7 */ -public final class QueryEnhancerFactory { - - private static final Log LOG = LogFactory.getLog(QueryEnhancerFactory.class); - private static final NativeQueryEnhancer NATIVE_QUERY_ENHANCER; - - static { - - NATIVE_QUERY_ENHANCER = NativeQueryEnhancer.select(); - - if (PersistenceProvider.ECLIPSELINK.isPresent()) { - LOG.info("EclipseLink is in classpath; If applicable, EQL parser will be used."); - } - - if (PersistenceProvider.HIBERNATE.isPresent()) { - LOG.info("Hibernate is in classpath; If applicable, HQL parser will be used."); - } - } - - private QueryEnhancerFactory() {} +public interface QueryEnhancerFactory { /** - * Creates a new {@link QueryEnhancer} for the given {@link DeclaredQuery}. + * Returns whether this QueryEnhancerFactory supports the given {@link DeclaredQuery}. * - * @param query must not be {@literal null}. - * @return an implementation of {@link QueryEnhancer} that suits the query the most + * @param query the query to be enhanced and introspected. + * @return {@code true} if this QueryEnhancer supports the given query; {@code false} otherwise. */ - public static QueryEnhancer forQuery(DeclaredQuery query) { - - if (query.isNativeQuery()) { - return getNativeQueryEnhancer(query); - } - - if (PersistenceProvider.HIBERNATE.isPresent()) { - return JpaQueryEnhancer.forHql(query); - } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { - return JpaQueryEnhancer.forEql(query); - } else { - return JpaQueryEnhancer.forJpql(query); - } - } + boolean supports(DeclaredQuery query); /** - * Get the native query enhancer for the given {@link DeclaredQuery query} based on {@link #NATIVE_QUERY_ENHANCER}. + * Creates a new {@link QueryEnhancer} for the given query. * - * @param query the declared query. - * @return new instance of {@link QueryEnhancer}. + * @param query the query to be enhanced and introspected. + * @return */ - private static QueryEnhancer getNativeQueryEnhancer(DeclaredQuery query) { - - if (NATIVE_QUERY_ENHANCER.equals(NativeQueryEnhancer.JSQLPARSER)) { - return new JSqlParserQueryEnhancer(query); - } - - return new DefaultQueryEnhancer(query); - } + QueryEnhancer create(DeclaredQuery query); /** - * Possible choices for the {@link #NATIVE_PARSER_PROPERTY}. Resolve the parser through {@link #select()}. + * Creates a new {@link QueryEnhancerFactory} for the given {@link DeclaredQuery}. * - * @since 3.3.5 + * @param query must not be {@literal null}. + * @return an implementation of {@link QueryEnhancer} that suits the query the most */ - enum NativeQueryEnhancer { - - AUTO, REGEX, JSQLPARSER; - - static final String NATIVE_PARSER_PROPERTY = "spring.data.jpa.query.native.parser"; - - static final boolean JSQLPARSER_PRESENT = ClassUtils.isPresent("net.sf.jsqlparser.parser.JSqlParser", null); - - /** - * @return the current selection considering classpath availability and user selection via - * {@link #NATIVE_PARSER_PROPERTY}. - */ - static NativeQueryEnhancer select() { - - NativeQueryEnhancer selected = resolve(); - - if (selected.equals(NativeQueryEnhancer.JSQLPARSER)) { - LOG.info("User choice: Using JSqlParser"); - return NativeQueryEnhancer.JSQLPARSER; - } - - if (selected.equals(NativeQueryEnhancer.REGEX)) { - LOG.info("Using Regex QueryEnhancer"); - return NativeQueryEnhancer.REGEX; - } - - if (!JSQLPARSER_PRESENT) { - return NativeQueryEnhancer.REGEX; - } - - LOG.info("JSqlParser is in classpath; If applicable, JSqlParser will be used."); - return NativeQueryEnhancer.JSQLPARSER; - } - - /** - * Resolve {@link NativeQueryEnhancer} from {@link SpringProperties}. - * - * @return the {@link NativeQueryEnhancer} constant. - */ - private static NativeQueryEnhancer resolve() { - - String name = SpringProperties.getProperty(NATIVE_PARSER_PROPERTY); - - if (StringUtils.hasText(name)) { - return ObjectUtils.caseInsensitiveValueOf(NativeQueryEnhancer.values(), name); - } - - return AUTO; - } + static QueryEnhancerFactory forQuery(DeclaredQuery query) { + return QueryEnhancerSelector.DEFAULT_SELECTOR.select(query); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java new file mode 100644 index 0000000000..75bee83f1d --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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.data.jpa.provider.PersistenceProvider; + +/** + * Interface declaring a strategy to select a {@link QueryEnhancer} for a given {@link DeclaredQuery query}. + *

      + * 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. + * + * @author Mark Paluch + */ +public interface QueryEnhancerSelector { + + /** + * Default selector strategy. + */ + QueryEnhancerSelector DEFAULT_SELECTOR = new DefaultQueryEnhancerSelector(); + + /** + * Select a {@link QueryEnhancer} for a {@link DeclaredQuery query}. + * + * @param query + * @return + */ + QueryEnhancerFactory select(DeclaredQuery query); + + /** + * Default {@link QueryEnhancerSelector} implementation using class-path information to determine enhancer + * availability. Subclasses may provide a different configuration by using the protected constructor. + */ + class DefaultQueryEnhancerSelector implements QueryEnhancerSelector { + + protected static QueryEnhancerFactory DEFAULT_NATIVE; + protected static QueryEnhancerFactory DEFAULT_JPQL; + + static { + + DEFAULT_NATIVE = QueryEnhancerFactories.jSqlParserPresent ? QueryEnhancerFactories.jsqlparser() + : QueryEnhancerFactories.fallback(); + + if (PersistenceProvider.HIBERNATE.isPresent()) { + DEFAULT_JPQL = QueryEnhancerFactories.hql(); + } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { + DEFAULT_JPQL = QueryEnhancerFactories.eql(); + } else { + DEFAULT_JPQL = QueryEnhancerFactories.jpql(); + } + } + + private final QueryEnhancerFactory nativeQuery; + private final QueryEnhancerFactory jpql; + + public DefaultQueryEnhancerSelector() { + this(DEFAULT_NATIVE, DEFAULT_JPQL); + } + + protected DefaultQueryEnhancerSelector(QueryEnhancerFactory nativeQuery, QueryEnhancerFactory jpql) { + this.nativeQuery = nativeQuery; + this.jpql = jpql; + } + + /** + * Returns the default JPQL {@link QueryEnhancerFactory} based on class path presence of Hibernate and EclipseLink. + * + * @return the default JPQL {@link QueryEnhancerFactory}. + */ + public static QueryEnhancerFactory jpql() { + return DEFAULT_JPQL; + } + + @Override + public QueryEnhancerFactory select(DeclaredQuery query) { + return jpql.supports(query) ? jpql : nativeQuery; + } + + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index 3a6bb4c7e9..3a9d2af875 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -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); + abstract @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery); /** * Creates a new {@link QueryParameterSetterFactory} for the given {@link JpaParameters}. @@ -116,8 +116,8 @@ private static QueryParameterSetter createSetter(Function parameters, String name) { @@ -180,7 +180,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { if (!(binding.getOrigin() instanceof ParameterBinding.Expression e)) { return null; @@ -212,7 +212,7 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar private static class SyntheticParameterSetterFactory extends QueryParameterSetterFactory { @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { if (!(binding.getOrigin() instanceof ParameterBinding.Synthetic s)) { return null; @@ -248,7 +248,7 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery introspectedQuery) { Assert.notNull(binding, "Binding must not be null"); @@ -294,22 +294,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding) { - - if (!binding.getOrigin().isMethodArgument()) { - return null; - } - - int parameterIndex = binding.getRequiredPosition() - 1; - - Assert.isTrue( // - parameterIndex < parameters.getNumberOfParameters(), // - () -> String.format( // - "At least %s parameter(s) provided but only %s parameter(s) present in query", // - binding.getRequiredPosition(), // - parameters.getNumberOfParameters() // - ) // - ); + public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) { @@ -317,7 +302,7 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { return QueryParameterSetter.NOOP; } - return super.create(binding); + return super.create(binding, query); } return null; 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 41c572731e..1619dedb86 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 @@ -445,7 +445,7 @@ private static String toJpaDirection(Order order) { * * @param query must not be {@literal null}. * @return Might return {@literal null}. - * @deprecated use {@link DeclaredQuery#getAlias()} instead. + * @deprecated use {@link IntrospectedQuery#getAlias()} instead. */ @Deprecated public static @Nullable String detectAlias(String query) { @@ -554,7 +554,7 @@ public static Query applyAndBind(String queryString, Iterable entities, E * * @param originalQuery must not be {@literal null} or empty. * @return Guaranteed to be not {@literal null}. - * @deprecated use {@link DeclaredQuery#deriveCountQuery(String)} instead. + * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. */ @Deprecated public static String createCountQueryFor(String originalQuery) { @@ -568,7 +568,7 @@ public static String createCountQueryFor(String originalQuery) { * @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 DeclaredQuery#deriveCountQuery(String)} instead. + * @deprecated use {@link IntrospectedQuery#deriveCountQuery(String)} instead. */ @Deprecated public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index b43f555c12..b913061ad6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -34,36 +34,21 @@ * @author Mark Paluch * @author Greg Turnquist */ -final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { - - /** - * Creates a new {@link SimpleJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}. - * - * @param method must not be {@literal null} - * @param em must not be {@literal null} - * @param countQueryString - * @param queryRewriter must not be {@literal null} - * @param valueExpressionDelegate must not be {@literal null} - */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString, - QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { - this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, valueExpressionDelegate); - } +class SimpleJpaQuery extends AbstractStringBasedJpaQuery { /** * Creates a new {@link SimpleJpaQuery} that encapsulates a simple query string. * - * @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 - * @param queryRewriter - * @param valueExpressionDelegate must not be {@literal null} + * @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 queryConfiguration must not be {@literal null}. */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, QueryRewriter queryRewriter, - ValueExpressionDelegate valueExpressionDelegate) { + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, @Nullable String countQueryString, + JpaQueryConfiguration queryConfiguration) { - super(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate); + super(method, em, queryString, countQueryString, queryConfiguration); validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", method); 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 b0b50cecb8..39af6fb1e3 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 @@ -29,6 +29,7 @@ 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; @@ -61,13 +62,14 @@ * @author Greg Turnquist * @author Yuriy Tsarkov */ -class StringQuery implements DeclaredQuery { +class StringQuery implements EntityQuery { private final String query; private final List bindings; private final boolean containsPageableInSpel; private final boolean usesJdbcStyleParameters; private final boolean isNative; + private final QueryEnhancerFactory queryEnhancerFactory; private final QueryEnhancer queryEnhancer; private final boolean hasNamedParameters; @@ -77,7 +79,7 @@ class StringQuery implements DeclaredQuery { * @param query must not be {@literal null} or empty. */ public StringQuery(String query, boolean isNative) { - this(query, isNative, it -> {}); + this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR, it -> {}); } /** @@ -85,20 +87,21 @@ public StringQuery(String query, boolean isNative) { * * @param query must not be {@literal null} or empty. */ - private StringQuery(String query, boolean isNative, Consumer> parameterPostProcessor) { + StringQuery(String query, boolean isNative, QueryEnhancerFactory factory,Consumer> parameterPostProcessor) { Assert.hasText(query, "Query must not be null or empty"); this.isNative = isNative; this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); + this.queryEnhancerFactory = factory; Metadata queryMeta = new Metadata(); this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, this.bindings, queryMeta); this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancer = QueryEnhancerFactory.forQuery(this); + this.queryEnhancer = factory.create(this); parameterPostProcessor.accept(this.bindings); @@ -113,6 +116,44 @@ private StringQuery(String query, boolean isNative, Consumer> parameterPostProcessor) { + + Assert.hasText(query, "Query must not be null or empty"); + + this.isNative = isNative; + this.bindings = new ArrayList<>(); + this.containsPageableInSpel = query.contains("#pageable"); + + Metadata queryMeta = new Metadata(); + this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, + this.bindings, queryMeta); + + this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; + this.queryEnhancerFactory = selector.select(this); + this.queryEnhancer = queryEnhancerFactory.create(this); + + parameterPostProcessor.accept(this.bindings); + + boolean hasNamedParameters = false; + for (ParameterBinding parameterBinding : getParameterBindings()) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + hasNamedParameters = true; + break; + } + } + + this.hasNamedParameters = hasNamedParameters; + } + + QueryEnhancer getQueryEnhancer() { + return queryEnhancer; + } + /** * Returns whether we have found some like bindings. */ @@ -130,13 +171,13 @@ public List getParameterBindings() { } @Override - public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { + 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.isNative, derivedBindings -> { + this.isNative, queryEnhancerFactory, derivedBindings -> { // need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees // JPA @@ -158,6 +199,11 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { }); } + @Override + public String applySorting(Sort sort) { + return queryEnhancer.applySorting(sort); + } + @Override public boolean usesJdbcStyleParameters() { return usesJdbcStyleParameters; @@ -168,7 +214,6 @@ public String getQueryString() { return query; } - @Override public @Nullable String getAlias() { return queryEnhancer.detectAlias(); } @@ -404,16 +449,18 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que BindingIdentifier targetBinding = queryParameter; Function bindingFactory = switch (ParameterBindingType.of(typeSource)) { - case LIKE -> { + case LIKE -> { - Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - yield (identifier) -> new 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. - default -> (identifier) -> new ParameterBinding(identifier, origin); - }; + Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + yield (identifier) -> new 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. + default -> (identifier) -> new ParameterBinding(identifier, origin); + }; - if (origin.isExpression()) { + if (origin.isExpression()) { parameterBindings.register(bindingFactory.apply(queryParameter)); } else { targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 96d6277010..2e24577f8f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -35,17 +35,8 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.query.AbstractJpaQuery; -import org.springframework.data.jpa.repository.query.BeanFactoryQueryRewriterProvider; -import org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory; -import org.springframework.data.jpa.repository.query.EscapeCharacter; -import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; -import org.springframework.data.jpa.repository.query.JpaQueryMethod; -import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; -import org.springframework.data.jpa.repository.query.Procedure; -import org.springframework.data.jpa.repository.query.QueryRewriterProvider; +import org.springframework.data.jpa.repository.query.*; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; @@ -82,12 +73,12 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private final EntityManager entityManager; - private final QueryExtractor extractor; private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; private final CrudMethodMetadata crudMethodMetadata; private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR; private JpaQueryMethodFactory queryMethodFactory; private QueryRewriterProvider queryRewriterProvider; @@ -101,7 +92,7 @@ public JpaRepositoryFactory(EntityManager entityManager) { Assert.notNull(entityManager, "EntityManager must not be null"); this.entityManager = entityManager; - this.extractor = PersistenceProvider.fromEntityManager(entityManager); + PersistenceProvider extractor = PersistenceProvider.fromEntityManager(entityManager); this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); this.entityPathResolver = SimpleEntityPathResolver.INSTANCE; this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor); @@ -179,6 +170,19 @@ public void setQueryMethodFactory(JpaQueryMethodFactory queryMethodFactory) { this.queryMethodFactory = queryMethodFactory; } + /** + * Configures the {@link QueryEnhancerSelector} to be used. Defaults to + * {@link QueryEnhancerSelector#DEFAULT_SELECTOR}. + * + * @param queryEnhancerSelector must not be {@literal null}. + */ + public void setQueryEnhancerSelector(QueryEnhancerSelector queryEnhancerSelector) { + + Assert.notNull(queryEnhancerSelector, "QueryEnhancerSelector must not be null"); + + this.queryEnhancerSelector = queryEnhancerSelector; + } + /** * Configures the {@link QueryRewriterProvider} to be used. Defaults to instantiate query rewriters through * {@link BeanUtils#instantiateClass(Class)}. @@ -243,8 +247,12 @@ protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoad @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { + + JpaQueryConfiguration queryConfiguration = new JpaQueryConfiguration(queryRewriterProvider, queryEnhancerSelector, + new CachingValueExpressionDelegate(valueExpressionDelegate), escapeCharacter); + return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, - new CachingValueExpressionDelegate(valueExpressionDelegate), queryRewriterProvider, escapeCharacter)); + queryConfiguration)); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index e0a1b00e62..ebb24268d1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -18,12 +18,18 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.util.function.Function; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.SimpleEntityPathResolver; @@ -46,10 +52,12 @@ public class JpaRepositoryFactoryBean, S, ID> extends TransactionalRepositoryFactoryBeanSupport { + private @Nullable BeanFactory beanFactory; private @Nullable EntityManager entityManager; private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; private @Nullable JpaQueryMethodFactory queryMethodFactory; + private @Nullable Function queryEnhancerSelectorSource; /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. @@ -75,6 +83,12 @@ public void setMappingContext(MappingContext mappingContext) { super.setMappingContext(mappingContext); } + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + super.setBeanFactory(beanFactory); + } + /** * Configures the {@link EntityPathResolver} to be used. Will expect a canonical bean to be present but fallback to * {@link SimpleEntityPathResolver#INSTANCE} in case none is available. @@ -101,6 +115,43 @@ public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) { } } + /** + * Configures the {@link QueryEnhancerSelector} to be used. Defaults to + * {@link QueryEnhancerSelector#DEFAULT_SELECTOR}. + * + * @param queryEnhancerSelectorSource must not be {@literal null}. + */ + public void setQueryEnhancerSelectorSource(QueryEnhancerSelector queryEnhancerSelectorSource) { + this.queryEnhancerSelectorSource = bf -> queryEnhancerSelectorSource; + } + + /** + * Configures the {@link QueryEnhancerSelector} to be used. + * + * @param queryEnhancerSelectorType must not be {@literal null}. + */ + public void setQueryEnhancerSelector(Class queryEnhancerSelectorType) { + + this.queryEnhancerSelectorSource = bf -> { + + if (bf != null) { + + ObjectProvider beanProvider = bf.getBeanProvider(queryEnhancerSelectorType); + QueryEnhancerSelector selector = beanProvider.getIfAvailable(); + + if (selector != null) { + return selector; + } + + if (bf instanceof AutowireCapableBeanFactory acbf) { + return acbf.createBean(queryEnhancerSelectorType); + } + } + + return BeanUtils.instantiateClass(queryEnhancerSelectorType); + }; + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { @@ -114,15 +165,19 @@ protected RepositoryFactorySupport doCreateRepositoryFactory() { */ protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { - JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); - jpaRepositoryFactory.setEntityPathResolver(entityPathResolver); - jpaRepositoryFactory.setEscapeCharacter(escapeCharacter); + JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager); + factory.setEntityPathResolver(entityPathResolver); + factory.setEscapeCharacter(escapeCharacter); if (queryMethodFactory != null) { - jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory); + factory.setQueryMethodFactory(queryMethodFactory); + } + + if (queryEnhancerSelectorSource != null) { + factory.setQueryEnhancerSelector(queryEnhancerSelectorSource.apply(beanFactory)); } - return jpaRepositoryFactory; + return factory; } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java index 6590db4022..204471b6d9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java @@ -34,7 +34,6 @@ import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -53,6 +52,9 @@ @ContextConfiguration("classpath:infrastructure.xml") class AbstractStringBasedJpaQueryIntegrationTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @PersistenceContext EntityManager em; @Autowired BeanFactory beanFactory; @@ -66,8 +68,7 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception { when(mock.getMetamodel()).thenReturn(em.getMetamodel()); JpaQueryMethod method = getMethod("findRolesByEmailAddress", String.class); - AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, null, QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + AbstractStringBasedJpaQuery jpaQuery = new SimpleJpaQuery(method, mock, method.getAnnotatedQuery(), null, CONFIG); jpaQuery.createJpaQuery(method.getAnnotatedQuery(), Sort.unsorted(), null, method.getResultProcessor().getReturnedType()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index 44d061094f..adc489cc98 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java @@ -36,7 +36,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -56,6 +55,9 @@ */ class AbstractStringBasedJpaQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @Test // GH-3310 void shouldNotAttemptToAppendSortIfNoSortArgumentPresent() { @@ -118,8 +120,8 @@ static InvocationCapturingStringQueryStub forMethod(Class repository, String Query query = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class); - return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery()); - + return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery(), + CONFIG); } static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQuery { @@ -128,7 +130,7 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu private final MultiValueMap capturedArguments = new LinkedMultiValueMap<>(3); InvocationCapturingStringQueryStub(Method targetMethod, JpaQueryMethod queryMethod, String queryString, - @Nullable String countQueryString) { + @Nullable String countQueryString, JpaQueryConfiguration queryConfiguration) { super(queryMethod, new Supplier() { @Override @@ -142,8 +144,7 @@ public EntityManager get() { return em; } - }.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class), - ValueExpressionDelegate.create()); + }.get(), queryString, countQueryString, queryConfiguration); this.targetMethod = targetMethod; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java index 6b9c4e2478..e0488df118 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java @@ -31,8 +31,8 @@ class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override - QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { - return new DefaultQueryEnhancer(declaredQuery); + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + return new DefaultQueryEnhancer(query); } @Override @@ -43,7 +43,7 @@ void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {} @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java index 8895fc4c19..5303378b84 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java @@ -32,7 +32,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { assumeThat(query.isNativeQuery()).isFalse(); - return JpaQueryEnhancer.forEql(query); + return JpaQueryEnhancer.forEql(query.getQueryString()); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java index 782c460a24..61436aae55 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -827,6 +827,6 @@ private String projection(String query) { } private QueryEnhancer newParser(String query) { - return JpaQueryEnhancer.forEql(DeclaredQuery.of(query, false)); + return JpaQueryEnhancer.forEql(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java index 2b81871822..8e8528a4bd 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java @@ -28,8 +28,8 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.Part.Type; /** @@ -47,7 +47,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) class ExpressionBasedStringQueryUnitTests { - private static final ValueExpressionParser PARSER = ValueExpressionParser.create(); + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @Mock JpaEntityMetadata metadata; @BeforeEach @@ -59,14 +61,16 @@ void setUp() { void shouldReturnQueryWithDomainTypeExpressionReplacedWithSimpleDomainTypeName() { String source = "select u from #{#entityName} u where u.firstname like :firstname"; - StringQuery query = new ExpressionBasedStringQuery(source, metadata, PARSER, false); + StringQuery query = new ExpressionBasedStringQuery(source, metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); 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, PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); assertThat(query.getAlias()).isEqualTo("u"); assertThat(query.getQueryString()).isEqualTo("select u from User u"); } @@ -79,7 +83,7 @@ void shouldDetectBindParameterCountCorrectly() { + "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, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getParameterBindings()).hasSize(8); } @@ -92,7 +96,7 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { + "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, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getParameterBindings()).hasSize(8); } @@ -105,7 +109,7 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { + "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, PARSER, true); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isFalse(); } @@ -113,7 +117,8 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { @Test void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { - StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isFalse(); } @@ -121,7 +126,8 @@ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { @Test void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { - StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, PARSER, true); + StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); assertThat(query.isNativeQuery()).isTrue(); } @@ -130,8 +136,8 @@ void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { void namedExpressionsShouldCreateLikeBindings() { StringQuery query = new ExpressionBasedStringQuery( - "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, PARSER, - false); + "select u from User u where u.firstname like %:#{foo} or u.firstname like :#{foo}%", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()).isEqualTo( @@ -155,8 +161,8 @@ void namedExpressionsShouldCreateLikeBindings() { void indexedExpressionsShouldCreateLikeBindings() { StringQuery query = new ExpressionBasedStringQuery( - "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, PARSER, - false); + "select u from User u where u.firstname like %?#{foo} or u.firstname like ?#{foo}%", metadata, + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.hasParameterBindings()).isTrue(); assertThat(query.getQueryString()) @@ -180,7 +186,7 @@ void indexedExpressionsShouldCreateLikeBindings() { void doesTemplatingWhenEntityNameSpelIsPresent() { StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u", - metadata, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -189,7 +195,7 @@ void doesTemplatingWhenEntityNameSpelIsPresent() { void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata, - PARSER, false); + CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select UserHallo from User u"); } @@ -198,7 +204,7 @@ void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() { StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}", - metadata, PARSER, false); + metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); assertThat(query.getQueryString()).isEqualTo("select u from User u where name = :__$synthetic$__1"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java index ef7b269115..916db5e06a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java @@ -32,7 +32,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { assumeThat(query.isNativeQuery()).isFalse(); - return JpaQueryEnhancer.forHql(query); + return JpaQueryEnhancer.forHql(query.getQueryString()); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index 1098f6a623..d9634ea91c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -1196,6 +1196,6 @@ private String projection(String query) { } private QueryEnhancer newParser(String query) { - return JpaQueryEnhancer.forHql(DeclaredQuery.of(query, false)); + return JpaQueryEnhancer.forHql(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index dee9d10d66..a3977b8a64 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -37,14 +37,14 @@ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override - QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { - return new JSqlParserQueryEnhancer(declaredQuery); + QueryEnhancer createQueryEnhancer(DeclaredQuery query) { + return new JSqlParserQueryEnhancer(query); } @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); @@ -54,13 +54,13 @@ void shouldApplySorting() { @Test // GH-3707 void countQueriesShouldConsiderPrimaryTableAlias() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of(""" + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(""" SELECT DISTINCT a.*, b.b1 FROM TableA a JOIN TableB b ON a.b = b.b LEFT JOIN TableC c ON b.c = c.c ORDER BY b.b1, a.a1, a.a2 - """, true)); + """)); String sql = enhancer.createCountQueryFor(); @@ -83,7 +83,7 @@ void setOperationListWorks() { + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -106,7 +106,7 @@ void complexSetOperationListWorks() { + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -133,7 +133,7 @@ void deeplyNestedcomplexSetOperationListWorks() { + "\t;"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); @@ -153,7 +153,7 @@ void valuesStatementsWorks() { String setQuery = "VALUES (1, 2, 'test')"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isNullOrEmpty(); @@ -174,7 +174,7 @@ void withStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -197,7 +197,7 @@ void multipleWithStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -217,7 +217,7 @@ void multipleWithStatementsWorks() { void truncateStatementShouldWork() { StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(stringQuery.getAlias()).isNull(); assertThat(stringQuery.getProjection()).isEmpty(); @@ -235,7 +235,7 @@ void truncateStatementShouldWork() { void mergeStatementWorksWithJSqlParser(String query, String alias) { StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); assertThat(QueryUtils.detectAlias(query)).isNull(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index 861272154b..e68faf4092 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -34,7 +34,6 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.beans.factory.BeanFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -64,7 +63,8 @@ @MockitoSettings(strictness = Strictness.LENIENT) class JpaQueryLookupStrategyUnitTests { - private static final ValueExpressionDelegate VALUE_EXPRESSION_DELEGATE = ValueExpressionDelegate.create(); + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); @Mock EntityManager em; @Mock EntityManagerFactory emf; @@ -72,7 +72,6 @@ class JpaQueryLookupStrategyUnitTests { @Mock NamedQueries namedQueries; @Mock Metamodel metamodel; @Mock ProjectionFactory projectionFactory; - @Mock BeanFactory beanFactory; private JpaQueryMethodFactory queryMethodFactory; @@ -90,7 +89,7 @@ void setUp() { void invalidAnnotatedQueryCausesException() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method method = UserRepository.class.getMethod("findByFoo", String.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -102,7 +101,7 @@ void invalidAnnotatedQueryCausesException() throws Exception { void considersNamedCountQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo"); @@ -124,7 +123,7 @@ void considersNamedCountQuery() throws Exception { void considersNamedCountOnStringQueryQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); when(namedQueries.hasQuery("foo.count")).thenReturn(true); when(namedQueries.getQuery("foo.count")).thenReturn("select count(foo) from Foo foo"); @@ -143,7 +142,7 @@ void considersNamedCountOnStringQueryQuery() throws Exception { void prefersDeclaredQuery() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method method = UserRepository.class.getMethod("annotatedQueryWithQueryAndQueryName"); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -156,7 +155,7 @@ void prefersDeclaredQuery() throws Exception { void namedQueryWithSortShouldThrowIllegalStateException() throws NoSuchMethodException { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method method = UserRepository.class.getMethod("customNamedQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -181,7 +180,7 @@ void noQueryShouldNotBeInvoked() { void customQueryWithQuestionMarksShouldWork() throws NoSuchMethodException { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND, - VALUE_EXPRESSION_DELEGATE, new BeanFactoryQueryRewriterProvider(beanFactory), EscapeCharacter.DEFAULT); + CONFIG); Method namedMethod = UserRepository.class.getMethod("customQueryWithQuestionMarksAndNamedParam", String.class); RepositoryMetadata namedMetadata = new DefaultRepositoryMetadata(UserRepository.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java index 9637785e39..2d44dbf0a5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java @@ -15,8 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.Assertions.*; import java.util.HashMap; import java.util.LinkedHashSet; @@ -27,6 +26,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -43,8 +43,11 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.ReflectionTestUtils; /** * Unit tests for repository with {@link Query} and {@link QueryRewriter}. @@ -57,6 +60,7 @@ class JpaQueryRewriteIntegrationTests { @Autowired private UserRepositoryWithRewriter repository; + @Autowired private JpaRepositoryFactoryBean factoryBean; // Results static final String ORIGINAL_QUERY = "original query"; @@ -71,6 +75,14 @@ void setUp() { repository.deleteAll(); } + @Test + void shouldConfigureQueryEnhancerSelector() { + + JpaRepositoryFactory factory = (JpaRepositoryFactory) ReflectionTestUtils.getField(factoryBean, "factory"); + + assertThat(factory).extracting("queryEnhancerSelector").isInstanceOf(MyQueryEnhancerSelector.class); + } + @Test void nativeQueryShouldHandleRewrites() { @@ -228,7 +240,8 @@ private static String replaceAlias(String query, Sort sort) { @ImportResource("classpath:infrastructure.xml") @EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = UserRepositoryWithRewriter.class, // includeFilters = @ComponentScan.Filter(value = { UserRepositoryWithRewriter.class }, - type = FilterType.ASSIGNABLE_TYPE)) + type = FilterType.ASSIGNABLE_TYPE), + queryEnhancerSelector = MyQueryEnhancerSelector.class) static class JpaRepositoryConfig { @Bean @@ -237,4 +250,10 @@ QueryRewriter queryRewriter() { } } + + static class MyQueryEnhancerSelector extends QueryEnhancerSelector.DefaultQueryEnhancerSelector { + public MyQueryEnhancerSelector() { + super(QueryEnhancerFactories.fallback(), DefaultQueryEnhancerSelector.jpql()); + } + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java index 8b6385e65d..32f9e965a9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java @@ -32,7 +32,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { assumeThat(query.isNativeQuery()).isFalse(); - return JpaQueryEnhancer.forJpql(query); + return JpaQueryEnhancer.forJpql(query.getQueryString()); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index acc6617811..1a38f729e2 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -832,6 +832,6 @@ private String projection(String query) { } private QueryEnhancer newParser(String query) { - return JpaQueryEnhancer.forJpql(DeclaredQuery.of(query, false)); + return JpaQueryEnhancer.forJpql(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java index 68cae8bc60..79df5c5198 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java @@ -89,8 +89,7 @@ void rejectsPersistenceProviderIfIncapableOfExtractingQueriesAndPagebleBeingUsed JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, projectionFactory, extractor); when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenThrow(new IllegalArgumentException()); - assertThatExceptionOfType(QueryCreationException.class) - .isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, QueryRewriter.IdentityQueryRewriter.INSTANCE)); + assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR, QueryRewriter.IdentityQueryRewriter.INSTANCE)); } @Test // DATAJPA-142 @@ -102,8 +101,7 @@ void doesNotRejectPersistenceProviderIfNamedCountQueryIsAvailable() { TypedQuery countQuery = mock(TypedQuery.class); when(em.createNamedQuery(eq(queryMethod.getNamedCountQueryName()), eq(Long.class))).thenReturn(countQuery); - NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em, - QueryRewriter.IdentityQueryRewriter.INSTANCE); + NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR, QueryRewriter.IdentityQueryRewriter.INSTANCE); query.doCreateCountQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[1])); verify(em, times(1)).createNamedQuery(queryMethod.getNamedCountQueryName(), Long.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java index cf9dab51fb..fa44d2ca11 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java @@ -34,7 +34,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -75,7 +74,8 @@ void shouldApplySorting() { Query annotation = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class); NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, annotation.value(), annotation.countQuery(), - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR, + ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT)); String sql = query.getSortedQueryString(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType()); assertThat(sql).isEqualTo("SELECT e FROM Employee e order by e.foo asc, e.bar asc"); 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 f95e9007b1..7456e047c2 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,16 +17,7 @@ import static org.assertj.core.api.Assertions.*; -import java.util.stream.Stream; - -import org.jspecify.annotations.Nullable; 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.QueryEnhancerFactory.NativeQueryEnhancer; -import org.springframework.data.jpa.util.ClassPathExclusions; /** * Unit tests for {@link QueryEnhancerFactory}. @@ -43,7 +34,7 @@ void createsParsingImplementationForNonNativeQuery() { StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); assertThat(queryEnhancer) // .isInstanceOf(JpaQueryEnhancer.class); @@ -58,79 +49,10 @@ void createsJSqlImplementationForNativeQuery() { StringQuery query = new StringQuery("select * from User", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); assertThat(queryEnhancer) // .isInstanceOf(JSqlParserQueryEnhancer.class); } - @ParameterizedTest // GH-2989 - @MethodSource("nativeEnhancerSelectionArgs") - void createsNativeImplementationAccordingToUserChoice(@Nullable String selection, NativeQueryEnhancer enhancer) { - - assertThat(NativeQueryEnhancer.JSQLPARSER_PRESENT).isTrue(); - - withSystemProperty(NativeQueryEnhancer.NATIVE_PARSER_PROPERTY, selection, () -> { - assertThat(NativeQueryEnhancer.select()).isEqualTo(enhancer); - }); - } - - static Stream nativeEnhancerSelectionArgs() { - return Stream.of(Arguments.of(null, NativeQueryEnhancer.JSQLPARSER), // - Arguments.of("", NativeQueryEnhancer.JSQLPARSER), // - Arguments.of("auto", NativeQueryEnhancer.JSQLPARSER), // - Arguments.of("regex", NativeQueryEnhancer.REGEX), // - Arguments.of("jsqlparser", NativeQueryEnhancer.JSQLPARSER)); - } - - @ParameterizedTest // GH-2989 - @MethodSource("nativeEnhancerExclusionSelectionArgs") - @ClassPathExclusions(packages = { "net.sf.jsqlparser.parser" }) - void createsNativeImplementationAccordingWithoutJsqlParserToUserChoice(@Nullable String selection, - NativeQueryEnhancer enhancer) { - - assertThat(NativeQueryEnhancer.JSQLPARSER_PRESENT).isFalse(); - - withSystemProperty(NativeQueryEnhancer.NATIVE_PARSER_PROPERTY, selection, () -> { - assertThat(NativeQueryEnhancer.select()).isEqualTo(enhancer); - }); - } - - static Stream nativeEnhancerExclusionSelectionArgs() { - return Stream.of(Arguments.of(null, NativeQueryEnhancer.REGEX), // - Arguments.of("", NativeQueryEnhancer.REGEX), // - Arguments.of("auto", NativeQueryEnhancer.REGEX), // - Arguments.of("regex", NativeQueryEnhancer.REGEX), // - Arguments.of("jsqlparser", NativeQueryEnhancer.JSQLPARSER)); - } - - @Test // GH-2989 - @ClassPathExclusions(packages = { "net.sf.jsqlparser.parser" }) - void selectedDefaultImplementationIfJsqlNotAvailable() { - - assertThat(NativeQueryEnhancer.JSQLPARSER_PRESENT).isFalse(); - assertThat(NativeQueryEnhancer.select()).isEqualTo(NativeQueryEnhancer.REGEX); - } - - void withSystemProperty(String property, @Nullable String value, Runnable exeution) { - - String currentValue = System.getProperty(property); - if (value != null) { - System.setProperty(property, value); - } else { - System.clearProperty(property); - } - try { - exeution.run(); - } finally { - if (currentValue != null) { - System.setProperty(property, currentValue); - } else { - System.clearProperty(property); - } - } - - } - - } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java index 077d469177..4b4bb8dfe0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java @@ -35,8 +35,7 @@ abstract class QueryEnhancerTckTests { @MethodSource("nativeCountQueries") // GH-2773 void shouldDeriveNativeCountQuery(String query, String expected) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, true); - QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); String countQueryFor = enhancer.createCountQueryFor(); // lenient cleanup to allow for rendering variance @@ -120,8 +119,7 @@ static Stream nativeCountQueries() { @MethodSource("jpqlCountQueries") void shouldDeriveJpqlCountQuery(String query, String expected) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, false); - QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql(query)); String countQueryFor = enhancer.createCountQueryFor(null); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -180,8 +178,7 @@ static Stream jpqlCountQueries() { @MethodSource("nativeQueriesWithVariables") void shouldDeriveNativeCountQueryWithVariable(String query, String expected) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, true); - QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); String countQueryFor = enhancer.createCountQueryFor(); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -211,6 +208,6 @@ void findProjectionClauseWithIncludedFrom() { assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); } - abstract QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery); + abstract QueryEnhancer createQueryEnhancer(DeclaredQuery query); } 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 163a91dd95..66dbcca20d 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 @@ -78,7 +78,7 @@ void allowsShortJpaSyntax() { @ParameterizedTest @MethodSource("detectsAliasWithUCorrectlySource") - void detectsAliasWithUCorrectly(DeclaredQuery query, String alias) { + void detectsAliasWithUCorrectly(IntrospectedQuery query, String alias) { assumeThat(query.getQueryString()).as("JsqlParser does not support simple JPA syntax") .doesNotStartWithIgnoringCase("from"); @@ -186,8 +186,7 @@ void preserveSourceQueryWhenAddingSort() { true); assertThat(getEnhancer(query).applySorting(Sort.by("name"), "p")) // - .startsWithIgnoringCase(query.getQueryString()) - .endsWithIgnoringCase("ORDER BY p.name ASC"); + .startsWithIgnoringCase(query.getQueryString()).endsWithIgnoringCase("ORDER BY p.name ASC"); } @Test // GH-2812 @@ -433,7 +432,7 @@ void discoversAliasWithComplexFunction() { assertThat( QueryUtils.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) // - .contains("myAlias"); + .contains("myAlias"); } @Test // DATAJPA-1506 @@ -538,7 +537,7 @@ void detectsAliasWithGroupAndOrderByWithLineBreaks() { @ParameterizedTest // DATAJPA-1679 @MethodSource("findProjectionClauseWithDistinctSource") - void findProjectionClauseWithDistinct(DeclaredQuery query, String expected) { + void findProjectionClauseWithDistinct(IntrospectedQuery query, String expected) { SoftAssertions.assertSoftly(sofly -> sofly.assertThat(getEnhancer(query).getProjection()).isEqualTo(expected)); } @@ -633,7 +632,8 @@ void modifyingQueriesAreDetectedCorrectly() { assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType); assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery); - assertThat(QueryEnhancerFactory.forQuery(modiQuery).createCountQueryFor()).isEqualToIgnoringCase(modifyingQuery); + assertThat(QueryEnhancerFactory.forQuery(modiQuery).create(modiQuery).createCountQueryFor()) + .isEqualToIgnoringCase(modifyingQuery); } @ParameterizedTest // GH-2593 @@ -641,7 +641,7 @@ void modifyingQueriesAreDetectedCorrectly() { void insertStatementIsProcessedSameAsDefault(String insertQuery) { StringQuery stringQuery = new StringQuery(insertQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); Sort sorting = Sort.by("day").descending(); @@ -696,8 +696,8 @@ private static void assertCountQuery(StringQuery originalQuery, String countQuer assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery); } - private static QueryEnhancer getEnhancer(DeclaredQuery query) { - return QueryEnhancerFactory.forQuery(query); + private static QueryEnhancer getEnhancer(IntrospectedQuery query) { + return QueryEnhancerFactory.forQuery(query).create(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java index 4640443b99..34d3ab2397 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java @@ -52,7 +52,8 @@ void before() { @Test // DATAJPA-1058 void noExceptionWhenQueryDoesNotContainNamedParameters() { - setterFactory.create(binding); + setterFactory.create(binding, + EntityQuery.introspectJpql("from Employee e", QueryEnhancerSelector.DEFAULT_SELECTOR)); } @Test // DATAJPA-1058 @@ -61,28 +62,14 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter("NamedParameter", 1)); assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(() -> setterFactory.create(binding - )) // + .isThrownBy(() -> setterFactory.create(binding, + EntityQuery.introspectJpql("from Employee e where e.name = :NamedParameter", + QueryEnhancerSelector.DEFAULT_SELECTOR))) // .withMessageContaining("Java 8") // .withMessageContaining("@Param") // .withMessageContaining("-parameters"); } - @Test // DATAJPA-1281 - void exceptionWhenCriteriaQueryContainsInsufficientAmountOfParameters() { - - // no parameter present in the criteria query - QueryParameterSetterFactory setterFactory = QueryParameterSetterFactory.forPartTreeQuery(parameters); - - // one argument present in the method signature - when(binding.getRequiredPosition()).thenReturn(1); - when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); - - assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding)) // - .withMessage("At least 1 parameter(s) provided but only 0 parameter(s) present in query"); - } - @Test // DATAJPA-1281 void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { @@ -94,7 +81,10 @@ void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { when(binding.getOrigin()).thenReturn(ParameterOrigin.ofParameter(null, 1)); assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> setterFactory.create(binding)) // + .isThrownBy( + () -> setterFactory.create(binding, + EntityQuery.introspectJpql("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"); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 4b53c362c3..5887eab53b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -47,7 +47,6 @@ import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -75,6 +74,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) class SimpleJpaQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + private static final String USER_QUERY = "select u from User u"; private JpaQueryMethod method; @@ -119,8 +121,7 @@ void prefersDeclaredCountQueryOverCreatingOne() throws Exception { extractor); when(em.createQuery("foo", Long.class)).thenReturn(typedQuery); - SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + SimpleJpaQuery jpaQuery = new SimpleJpaQuery(method, em, "select u from User u", null, CONFIG); assertThat(jpaQuery.createCountQuery(new JpaParametersParameterAccessor(method.getParameters(), new Object[] {}))) .isEqualTo(typedQuery); @@ -134,8 +135,7 @@ void doesNotApplyPaginationToCountQuery() throws Exception { 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, - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + AbstractJpaQuery jpaQuery = new SimpleJpaQuery(queryMethod, em, "select u from User u", null, CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -149,9 +149,8 @@ void discoversNativeQuery() throws Exception { Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, - queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, + queryMethod.getAnnotatedQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -169,9 +168,8 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception { Method method = SampleRepository.class.getMethod("findByLastnameNativeAnnotation", String.class); JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - AbstractJpaQuery jpaQuery = JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, - queryMethod.getAnnotatedQuery(), null, QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + AbstractJpaQuery jpaQuery = JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, + queryMethod.getAnnotatedQuery(), null, CONFIG); assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); @@ -239,10 +237,11 @@ void allowsCountQueryUsingParametersNotInOriginalQuery() throws Exception { when(em.createNativeQuery(anyString())).thenReturn(query); AbstractJpaQuery jpaQuery = createJpaQuery( - SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), Optional.empty()); + SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), + Optional.empty()); jpaQuery.doCreateCountQuery(new JpaParametersParameterAccessor(jpaQuery.getQueryMethod().getParameters(), - new Object[]{"data", PageRequest.of(0, 10)})); + new Object[] { "data", PageRequest.of(0, 10) })); ArgumentCaptor queryStringCaptor = ArgumentCaptor.forClass(String.class); verify(em).createQuery(queryStringCaptor.capture(), eq(Long.class)); @@ -283,8 +282,7 @@ void resolvesExpressionInCountQuery() throws Exception { 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", QueryRewriter.IdentityQueryRewriter.INSTANCE, - ValueExpressionDelegate.create()); + "select count(u.id) from #{#entityName} u", CONFIG); jpaQuery.createCountQuery( new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { PageRequest.of(1, 10) })); @@ -296,16 +294,18 @@ private AbstractJpaQuery createJpaQuery(Method method) { return createJpaQuery(method, null); } - private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) { + private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, + @Nullable String countQueryString) { - return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString, - QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); + return JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.createStringQuery(queryMethod, em, queryString, + countQueryString, CONFIG); } private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional 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.getAnnotatedQuery(), + countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery())); } interface SampleRepository { @@ -337,8 +337,8 @@ interface SampleRepository { @Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u") List findAllWithExpressionInCountQuery(Pageable pageable); - - @Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}") + @Query(value = "select u from User u", + countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}") List findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable); // Typo in named parameter 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 41b36b21d7..134ae29417 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 @@ -911,12 +911,14 @@ void usingGreaterThanWithNamedParameter() { void checkNumberOfNamedParameters(String query, int expectedSize, String label, boolean nativeQuery) { - DeclaredQuery declaredQuery = DeclaredQuery.of(query, nativeQuery); + EntityQuery introspectedQuery = nativeQuery + ? EntityQuery.introspectNativeQuery(query, QueryEnhancerSelector.DEFAULT_SELECTOR) + : EntityQuery.introspectJpql(query, QueryEnhancerSelector.DEFAULT_SELECTOR); - assertThat(declaredQuery.hasNamedParameter()) // + assertThat(introspectedQuery.hasNamedParameter()) // .describedAs("hasNamed Parameter " + label) // .isEqualTo(expectedSize > 0); - assertThat(declaredQuery.getParameterBindings()) // + assertThat(introspectedQuery.getParameterBindings()) // .describedAs("parameterBindings " + label) // .hasSize(expectedSize); } From 7068a8ac857eda5d54808b9df180f56ea1e3d8f6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 4 Mar 2025 13:41:10 +0100 Subject: [PATCH 49/94] Refactoring. See #3622 Original pull request: #3527 --- .../repository/query/HqlParserBenchmarks.java | 2 +- .../JSqlParserQueryEnhancerBenchmarks.java | 2 +- .../jpa/repository/query/BindableQuery.java | 67 ++++++++++ .../jpa/repository/query/DeclaredQuery.java | 23 ++-- .../query/DefaultDeclaredQuery.java | 68 ---------- .../query/DefaultQueryEnhancer.java | 10 +- .../query/EmptyIntrospectedQuery.java | 10 ++ .../repository/query/IntrospectedQuery.java | 8 +- .../query/JSqlParserQueryEnhancer.java | 10 +- .../data/jpa/repository/query/JpqlQuery.java | 38 ++++++ .../data/jpa/repository/query/NamedQuery.java | 5 - .../jpa/repository/query/NativeQuery.java | 38 ++++++ .../query/ParameterBinderFactory.java | 4 +- .../jpa/repository/query/QueryEnhancer.java | 2 +- .../query/QueryEnhancerFactories.java | 10 +- .../query/QueryEnhancerFactory.java | 2 +- .../query/QueryEnhancerSelector.java | 2 +- .../jpa/repository/query/StringQuery.java | 122 ++++++++---------- .../jpa/repository/query/StructuredQuery.java | 24 ++++ .../query/DefaultQueryEnhancerUnitTests.java | 2 +- .../ExpressionBasedStringQueryUnitTests.java | 6 +- .../JSqlParserQueryEnhancerUnitTests.java | 20 +-- ...rIndexedQueryParameterSetterUnitTests.java | 8 +- .../query/QueryEnhancerFactoryUnitTests.java | 4 +- .../query/QueryEnhancerTckTests.java | 8 +- .../query/QueryEnhancerUnitTests.java | 6 +- .../query/StringQueryUnitTests.java | 9 +- 27 files changed, 304 insertions(+), 206 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java index fb524d76bf..ecbb4eb238 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java @@ -55,7 +55,7 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%' OR p.description LIKE "cost overrun" """; - query = DeclaredQuery.ofJpql(s); + query = DeclaredQuery.jpqlQuery(s); enhancer = QueryEnhancerFactory.forQuery(query).create(query); } } diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java index aeb1764c5c..a5c9cdce23 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java @@ -56,7 +56,7 @@ public void doSetup() throws IOException { select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; - enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.ofNative(s)); + enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.nativeQuery(s)); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java new file mode 100644 index 0000000000..66e95a93c5 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java @@ -0,0 +1,67 @@ +/* + * 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 bindings; + private final boolean usesJdbcStyleParameters; + + public BindableQuery(DeclaredQuery source, String bindableQueryString, List 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 getBindings() { + return Collections.unmodifiableList(bindings); + } +} 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 ca32d1f46b..152c40c385 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 @@ -23,33 +23,28 @@ * @author Mark Paluch * @since 2.0.3 */ -public interface DeclaredQuery { +public interface DeclaredQuery extends StructuredQuery { /** * Creates a DeclaredQuery for a JPQL query. * - * @param query the JPQL query string. - * @return + * @param jpql the JPQL query string. + * @return new instance of {@link DeclaredQuery}. */ - static DeclaredQuery ofJpql(String query) { - return new DefaultDeclaredQuery(query, false); + static DeclaredQuery jpqlQuery(String jpql) { + return new JpqlQuery(jpql); } /** * Creates a DeclaredQuery for a native query. * - * @param query the native query string. - * @return + * @param sql the native query string. + * @return new instance of {@link DeclaredQuery}. */ - static DeclaredQuery ofNative(String query) { - return new DefaultDeclaredQuery(query, true); + static DeclaredQuery nativeQuery(String sql) { + return new NativeQuery(sql); } - /** - * Returns the query string. - */ - String getQueryString(); - /** * Return whether the query is a native query of not. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java deleted file mode 100644 index a24512a994..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultDeclaredQuery.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2024 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; - -/** - * @author Mark Paluch - */ -class DefaultDeclaredQuery implements DeclaredQuery { - - private final String query; - private final boolean nativeQuery; - - DefaultDeclaredQuery(String query, boolean nativeQuery) { - this.query = query; - this.nativeQuery = nativeQuery; - } - - @Override - public String getQueryString() { - return query; - } - - @Override - public boolean isNativeQuery() { - return nativeQuery; - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (!(object instanceof DefaultDeclaredQuery that)) { - return false; - } - if (nativeQuery != that.nativeQuery) { - return false; - } - return ObjectUtils.nullSafeEquals(query, that.query); - } - - @Override - public int hashCode() { - int result = ObjectUtils.nullSafeHashCode(query); - result = 31 * result + (nativeQuery ? 1 : 0); - return result; - } - - @Override - public String toString() { - return (isNativeQuery() ? "[native] " : "[JPQL] ") + getQueryString(); - } -} 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 1fe6236621..3d4aba2859 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 @@ -29,13 +29,13 @@ */ public class DefaultQueryEnhancer implements QueryEnhancer { - private final DeclaredQuery query; + private final StructuredQuery query; private final boolean hasConstructorExpression; private final @Nullable String alias; private final String projection; private final Set joinAliases; - public DefaultQueryEnhancer(DeclaredQuery query) { + public DefaultQueryEnhancer(StructuredQuery query) { this.query = query; this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); this.alias = QueryUtils.detectAlias(query.getQueryString()); @@ -60,7 +60,9 @@ public String rewrite(QueryRewriteInformation rewriteInformation) { @Override public String createCountQueryFor(@Nullable String countProjection) { - return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery()); + + boolean nativeQuery = this.query instanceof DeclaredQuery dc ? dc.isNativeQuery() : true; + return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, nativeQuery); } @Override @@ -84,7 +86,7 @@ public Set getJoinAliases() { } @Override - public DeclaredQuery getQuery() { + public StructuredQuery getQuery() { return this.query; } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index c51f0c4ca4..ec92ee81cf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java @@ -63,6 +63,11 @@ public boolean isDefaultProjection() { return false; } + @Override + public String getQueryString() { + return ""; + } + @Override public List getParameterBindings() { return Collections.emptyList(); @@ -82,4 +87,9 @@ public String applySorting(Sort sort) { public boolean usesJdbcStyleParameters() { return false; } + + @Override + public DeclaredQuery getDeclaredQuery() { + return DeclaredQuery.nativeQuery(""); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java index 427dbcc03b..4a29bce6c8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/IntrospectedQuery.java @@ -24,7 +24,13 @@ * @author Diego Krupitza * @since 2.0.3 */ -interface IntrospectedQuery extends DeclaredQuery { +interface IntrospectedQuery extends StructuredQuery { + + DeclaredQuery getDeclaredQuery(); + + default String getQueryString() { + return getDeclaredQuery().getQueryString(); + } /** * @return whether the underlying query has at least one named parameter. 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 f68443adda..8e052c9eec 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 @@ -70,7 +70,7 @@ */ public class JSqlParserQueryEnhancer implements QueryEnhancer { - private final DeclaredQuery query; + private final StructuredQuery query; private final Statement statement; private final ParsedType parsedType; private final boolean hasConstructorExpression; @@ -83,7 +83,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { /** * @param query the query we want to enhance. Must not be {@literal null}. */ - public JSqlParserQueryEnhancer(DeclaredQuery query) { + public JSqlParserQueryEnhancer(StructuredQuery query) { this.query = query; this.statement = parseStatement(query.getQueryString(), Statement.class); @@ -295,7 +295,7 @@ public Set getSelectionAliases() { } @Override - public DeclaredQuery getQuery() { + public StructuredQuery getQuery() { return this.query; } @@ -373,8 +373,8 @@ public String createCountQueryFor(@Nullable String countProjection) { return createCountQueryFor(selectBody, countProjection, primaryAlias); } - private static String createCountQueryFor(PlainSelect selectBody, @Nullable String countProjection, - @Nullable String primaryAlias) { + private static String createCountQueryFor(StructuredQuery query, PlainSelect selectBody, + @Nullable String countProjection, @Nullable String primaryAlias) { // remove order by selectBody.setOrderByElements(null); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java new file mode 100644 index 0000000000..6a8f3cce03 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java @@ -0,0 +1,38 @@ +/* + * 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 + */ +final class JpqlQuery implements DeclaredQuery { + + private final String jpql; + + JpqlQuery(String jpql) { + this.jpql = jpql; + } + + @Override + public boolean isNativeQuery() { + return false; + } + + @Override + public String getQueryString() { + return jpql; + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index 6f4138760c..b81820c6f4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -185,16 +185,11 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc EntityManager em = getEntityManager(); TypedQuery countQuery; - String cacheKey; if (namedCountQueryIsPresent) { - cacheKey = countQueryName; countQuery = em.createNamedQuery(countQueryName, Long.class); - } else { String countQueryString = entityQuery.get().deriveCountQuery(countProjection).getQueryString(); - countQueryString = potentiallyRewriteQuery(countQueryString, accessor.getSort(), accessor.getPageable()); - cacheKey = countQueryString; countQuery = em.createQuery(countQueryString, Long.class); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java new file mode 100644 index 0000000000..6ba9f81ba6 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java @@ -0,0 +1,38 @@ +/* + * 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 + */ +final class NativeQuery implements DeclaredQuery { + + private final String sql; + + NativeQuery(String sql) { + this.sql = sql; + } + + @Override + public boolean isNativeQuery() { + return true; + } + + @Override + public String getQueryString() { + return sql; + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index 8abf7d461d..fc34606f45 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -92,7 +92,6 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Introspe Assert.notNull(parser, "SpelExpressionParser must not be null"); Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null"); - List bindings = query.getParameterBindings(); QueryParameterSetterFactory expressionSetterFactory = QueryParameterSetterFactory.parsing(parser, evaluationContextProvider); @@ -101,7 +100,8 @@ static ParameterBinder createQueryAwareBinder(JpaParameters parameters, Introspe boolean usesPaging = query instanceof EntityQuery eq && eq.usesPaging(); - return new ParameterBinder(parameters, createSetters(bindings, query, expressionSetterFactory, basicSetterFactory), + // TODO: lets maybe obtain the bindable query and pass that on to create the setters? + return new ParameterBinder(parameters, createSetters(query.getParameterBindings(), query, expressionSetterFactory, basicSetterFactory), !usesPaging); } 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 ff9f44c44a..528426f82f 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 @@ -66,7 +66,7 @@ public interface QueryEnhancer { * * @return non-null {@link DeclaredQuery} that wraps the query. */ - DeclaredQuery getQuery(); + StructuredQuery getQuery(); /** * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java index b88a6953f0..07ed8642c3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java @@ -57,7 +57,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(StructuredQuery query) { return new DefaultQueryEnhancer(query); } }, @@ -69,7 +69,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(StructuredQuery query) { if (jSqlParserPresent) { return new JSqlParserQueryEnhancer(query); } @@ -85,7 +85,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(StructuredQuery query) { return JpaQueryEnhancer.forHql(query.getQueryString()); } }, @@ -96,7 +96,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(StructuredQuery query) { return JpaQueryEnhancer.forEql(query.getQueryString()); } }, @@ -107,7 +107,7 @@ public boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(DeclaredQuery query) { + public QueryEnhancer create(StructuredQuery query) { return JpaQueryEnhancer.forJpql(query.getQueryString()); } } 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 a3e7b5f06d..26bdf4b5b2 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 @@ -40,7 +40,7 @@ public interface QueryEnhancerFactory { * @param query the query to be enhanced and introspected. * @return */ - QueryEnhancer create(DeclaredQuery query); + QueryEnhancer create(StructuredQuery query); /** * Creates a new {@link QueryEnhancerFactory} for the given {@link DeclaredQuery}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java index 75bee83f1d..93268c6387 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java @@ -66,7 +66,7 @@ class DefaultQueryEnhancerSelector implements QueryEnhancerSelector { private final QueryEnhancerFactory nativeQuery; private final QueryEnhancerFactory jpql; - public DefaultQueryEnhancerSelector() { + DefaultQueryEnhancerSelector() { this(DEFAULT_NATIVE, DEFAULT_JPQL); } 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 39af6fb1e3..e15bcb2e33 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 @@ -64,11 +64,8 @@ */ class StringQuery implements EntityQuery { - private final String query; - private final List bindings; + private final BindableQuery bindableQuery; private final boolean containsPageableInSpel; - private final boolean usesJdbcStyleParameters; - private final boolean isNative; private final QueryEnhancerFactory queryEnhancerFactory; private final QueryEnhancer queryEnhancer; private final boolean hasNamedParameters; @@ -78,7 +75,7 @@ class StringQuery implements EntityQuery { * * @param query must not be {@literal null} or empty. */ - public StringQuery(String query, boolean isNative) { + StringQuery(String query, boolean isNative) { this(query, isNative, QueryEnhancerSelector.DEFAULT_SELECTOR, it -> {}); } @@ -91,29 +88,15 @@ public StringQuery(String query, boolean isNative) { Assert.hasText(query, "Query must not be null or empty"); - this.isNative = isNative; - this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); this.queryEnhancerFactory = factory; - Metadata queryMeta = new Metadata(); - this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - this.bindings, queryMeta); + DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); + this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancer = factory.create(this); - - parameterPostProcessor.accept(this.bindings); - - boolean hasNamedParameters = false; - for (ParameterBinding parameterBinding : getParameterBindings()) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - hasNamedParameters = true; - break; - } - } - - this.hasNamedParameters = hasNamedParameters; + parameterPostProcessor.accept(this.bindableQuery.getBindings()); + this.queryEnhancer = factory.create(this.bindableQuery); + this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings()); } /** @@ -125,29 +108,32 @@ public StringQuery(String query, boolean isNative) { Assert.hasText(query, "Query must not be null or empty"); - this.isNative = isNative; - this.bindings = new ArrayList<>(); this.containsPageableInSpel = query.contains("#pageable"); + DeclaredQuery source = isNative ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - Metadata queryMeta = new Metadata(); - this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - this.bindings, queryMeta); - - this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancerFactory = selector.select(this); - this.queryEnhancer = queryEnhancerFactory.create(this); + this.bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - parameterPostProcessor.accept(this.bindings); - - boolean hasNamedParameters = false; - for (ParameterBinding parameterBinding : getParameterBindings()) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - hasNamedParameters = true; - break; - } - } + 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; + this.containsPageableInSpel = containsPageableInSpel; } QueryEnhancer getQueryEnhancer() { @@ -158,16 +144,21 @@ QueryEnhancer getQueryEnhancer() { * Returns whether we have found some like bindings. */ boolean hasParameterBindings() { - return !bindings.isEmpty(); + return this.bindableQuery.hasBindings(); } String getProjection() { return this.queryEnhancer.getProjection(); } + @Override + public String getQueryString() { + return bindableQuery.getQueryString(); + } + @Override public List getParameterBindings() { - return bindings; + return this.bindableQuery.getBindings(); } @Override @@ -177,14 +168,14 @@ public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) // JPA parameter markers and not the original expressions anymore. return new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), // - this.isNative, queryEnhancerFactory, derivedBindings -> { + 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 : bindings) { + for (ParameterBinding binding : getParameterBindings()) { Predicate identifier = binding::bindsTo; Predicate notCompatible = Predicate.not(binding::isCompatibleWith); @@ -206,12 +197,7 @@ public String applySorting(Sort sort) { @Override public boolean usesJdbcStyleParameters() { - return usesJdbcStyleParameters; - } - - @Override - public String getQueryString() { - return query; + return bindableQuery.usesJdbcStyleParameters(); } public @Nullable String getAlias() { @@ -239,8 +225,17 @@ public boolean usesPaging() { } @Override - public boolean isNativeQuery() { - return isNative; + public DeclaredQuery getDeclaredQuery() { + return bindableQuery; + } + + private static boolean containsNamedParameter(List bindings) { + for (ParameterBinding parameterBinding : bindings) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + return true; + } + } + return false; } /** @@ -376,8 +371,7 @@ enum ParameterBindingParser { * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns * the cleaned up query. */ - String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings, - Metadata queryMeta) { + BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(DeclaredQuery query) { IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query)); boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels(); @@ -385,11 +379,11 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que /* * Prefer indexed access over named parameters if only SpEL Expression parameters are present. */ - if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { + if (!parametersShouldBeAccessedByIndex && query.getQueryString().contains("?#{")) { parametersShouldBeAccessedByIndex = true; } - ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, + ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query.getQueryString(), parametersShouldBeAccessedByIndex, parameterLabels); String resultingQuery = parsedQuery.getQueryString(); @@ -412,14 +406,14 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que String match = matcher.group(0); if (JDBC_STYLE_PARAM.matcher(match).find()) { - queryMeta.usesJdbcStyleParameters = true; + jdbcStyle = true; } if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { usesJpaStyleParameters = true; } - if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) { + if (usesJpaStyleParameters && jdbcStyle) { throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); } @@ -467,7 +461,7 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que } replacement = targetBinding.hasName() ? ":" + targetBinding.getName() - : ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?" + : ((!usesJpaStyleParameters && jdbcStyle) ? "?" : "?" + targetBinding.getPosition()); String result; String substring = matcher.group(2); @@ -484,7 +478,7 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que resultingQuery = result; } - return resultingQuery; + return new BindableQuery(query, resultingQuery, bindings, jdbcStyle); } private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, @@ -592,9 +586,7 @@ static ParameterBindingType of(String typeSource) { } } - static class Metadata { - private boolean usesJdbcStyleParameters = false; - } + /** * Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java new file mode 100644 index 0000000000..2ebfcb0549 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java @@ -0,0 +1,24 @@ +/* + * 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(); +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java index e0488df118..9a5c9ff30f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java @@ -43,7 +43,7 @@ void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {} @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative("SELECT e FROM Employee e")); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java index 8e8528a4bd..a235543017 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java @@ -111,7 +111,7 @@ void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { + "AND (n.updatedAt >= ?#{#networkRequest.updatedTime.startDateTime}) AND (n.updatedAt <=?#{#networkRequest.updatedTime.endDateTime})", metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), false, CONFIG.getSelector()); - assertThat(query.isNativeQuery()).isFalse(); + assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse(); } @Test @@ -120,7 +120,7 @@ void shouldDetectSimpleNativeQueriesWithSpelAsNonNative() { StringQuery query = new ExpressionBasedStringQuery("select n from #{#entityName} n", metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); - assertThat(query.isNativeQuery()).isFalse(); + assertThat(query.getDeclaredQuery().isNativeQuery()).isFalse(); } @Test @@ -129,7 +129,7 @@ void shouldDetectSimpleNativeQueriesWithoutSpelAsNative() { StringQuery query = new ExpressionBasedStringQuery("select u from User u", metadata, CONFIG.getValueExpressionDelegate().getValueExpressionParser(), true, CONFIG.getSelector()); - assertThat(query.isNativeQuery()).isTrue(); + assertThat(query.getDeclaredQuery().isNativeQuery()).isTrue(); } @Test // GH-3041 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index a3977b8a64..52787f910f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -44,7 +44,7 @@ QueryEnhancer createQueryEnhancer(DeclaredQuery query) { @Test // GH-3546 void shouldApplySorting() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql("SELECT e FROM Employee e")); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery("SELECT e FROM Employee e")); String sql = enhancer.applySorting(Sort.by("foo", "bar")); @@ -54,7 +54,7 @@ void shouldApplySorting() { @Test // GH-3707 void countQueriesShouldConsiderPrimaryTableAlias() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(""" + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(""" SELECT DISTINCT a.*, b.b1 FROM TableA a JOIN TableB b ON a.b = b.b @@ -83,7 +83,7 @@ void setOperationListWorks() { + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -106,7 +106,7 @@ void complexSetOperationListWorks() { + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); @@ -133,7 +133,7 @@ void deeplyNestedcomplexSetOperationListWorks() { + "\t;"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); @@ -153,7 +153,7 @@ void valuesStatementsWorks() { String setQuery = "VALUES (1, 2, 'test')"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNullOrEmpty(); assertThat(stringQuery.getProjection()).isNullOrEmpty(); @@ -174,7 +174,7 @@ void withStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -197,7 +197,7 @@ void multipleWithStatementsWorks() { + "select day, value from sample_data as a"; StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); @@ -217,7 +217,7 @@ void multipleWithStatementsWorks() { void truncateStatementShouldWork() { StringQuery stringQuery = new StringQuery("TRUNCATE TABLE foo", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(stringQuery.getAlias()).isNull(); assertThat(stringQuery.getProjection()).isEmpty(); @@ -235,7 +235,7 @@ void truncateStatementShouldWork() { void mergeStatementWorksWithJSqlParser(String query, String alias) { StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery); assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); assertThat(QueryUtils.detectAlias(query)).isNull(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java index e85ff114f1..d438cdf9a6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedOrIndexedQueryParameterSetterUnitTests.java @@ -89,7 +89,7 @@ void strictErrorHandlingThrowsExceptionForAllVariationsOfParameters() { softly .assertThatThrownBy( - () -> setter.setParameter(BindableQuery.from(query), methodArguments, STRICT)) // + () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, STRICT)) // .describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", // parameter.getClass(), // parameter.getName(), // @@ -118,7 +118,7 @@ void lenientErrorHandlingThrowsNoExceptionForAllVariationsOfParameters() { softly .assertThatCode( - () -> setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT)) // + () -> setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT)) // .describedAs("p-type: %s, p-name: %s, p-position: %s, temporal: %s", // parameter.getClass(), // parameter.getName(), // @@ -149,7 +149,7 @@ void lenientSetsParameterWhenSuccessIsUnsure() { temporalType // ); - setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT); + setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT); if (temporalType == null) { verify(query).setParameter(eq(11), any(Date.class)); @@ -179,7 +179,7 @@ void parameterNotSetWhenSuccessImpossible() { temporalType // ); - setter.setParameter(BindableQuery.from(query), methodArguments, LENIENT); + setter.setParameter(QueryParameterSetter.BindableQuery.from(query), methodArguments, LENIENT); if (temporalType == null) { verify(query, never()).setParameter(anyInt(), any(Date.class)); 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 7456e047c2..aaccc4cad4 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 @@ -34,7 +34,7 @@ void createsParsingImplementationForNonNativeQuery() { StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query); assertThat(queryEnhancer) // .isInstanceOf(JpaQueryEnhancer.class); @@ -49,7 +49,7 @@ void createsJSqlImplementationForNativeQuery() { StringQuery query = new StringQuery("select * from User", true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query); assertThat(queryEnhancer) // .isInstanceOf(JSqlParserQueryEnhancer.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java index 4b4bb8dfe0..7a0f4e1783 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java @@ -35,7 +35,7 @@ abstract class QueryEnhancerTckTests { @MethodSource("nativeCountQueries") // GH-2773 void shouldDeriveNativeCountQuery(String query, String expected) { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query)); String countQueryFor = enhancer.createCountQueryFor(); // lenient cleanup to allow for rendering variance @@ -119,7 +119,7 @@ static Stream nativeCountQueries() { @MethodSource("jpqlCountQueries") void shouldDeriveJpqlCountQuery(String query, String expected) { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofJpql(query)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery(query)); String countQueryFor = enhancer.createCountQueryFor(null); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -178,7 +178,7 @@ static Stream jpqlCountQueries() { @MethodSource("nativeQueriesWithVariables") void shouldDeriveNativeCountQueryWithVariable(String query, String expected) { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.ofNative(query)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.nativeQuery(query)); String countQueryFor = enhancer.createCountQueryFor(); assertThat(countQueryFor).isEqualToIgnoringCase(expected); @@ -205,7 +205,7 @@ void findProjectionClauseWithIncludedFrom() { StringQuery query = new StringQuery("select x, frommage, y from t", true); - assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); + assertThat(createQueryEnhancer(query.getDeclaredQuery()).getProjection()).isEqualTo("x, frommage, y"); } abstract QueryEnhancer createQueryEnhancer(DeclaredQuery query); 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 66dbcca20d..0e5f44cd8b 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 @@ -632,7 +632,7 @@ void modifyingQueriesAreDetectedCorrectly() { assertThat(modiQuery.hasConstructorExpression()).isEqualTo(constructorExpressionNotConsideringQueryType); assertThat(countQueryForNotConsiderQueryType).isEqualToIgnoringCase(modifyingQuery); - assertThat(QueryEnhancerFactory.forQuery(modiQuery).create(modiQuery).createCountQueryFor()) + assertThat(QueryEnhancerFactory.forQuery(modiQuery.getDeclaredQuery()).create(modiQuery.getDeclaredQuery()).createCountQueryFor()) .isEqualToIgnoringCase(modifyingQuery); } @@ -641,7 +641,7 @@ void modifyingQueriesAreDetectedCorrectly() { void insertStatementIsProcessedSameAsDefault(String insertQuery) { StringQuery stringQuery = new StringQuery(insertQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery).create(stringQuery); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery.getDeclaredQuery()).create(stringQuery.getDeclaredQuery()); Sort sorting = Sort.by("day").descending(); @@ -697,7 +697,7 @@ private static void assertCountQuery(StringQuery originalQuery, String countQuer } private static QueryEnhancer getEnhancer(IntrospectedQuery query) { - return QueryEnhancerFactory.forQuery(query).create(query); + return QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query.getDeclaredQuery()); } } 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 134ae29417..3c18eda1fb 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,7 +17,6 @@ import static org.assertj.core.api.Assertions.*; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,6 +28,7 @@ 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; /** @@ -925,11 +925,10 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) { - List bindings = new ArrayList<>(); - StringQuery.ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - bindings, new StringQuery.Metadata()); + DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); + BindableQuery bindableQuery = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(source); - assertThat(bindings.stream().anyMatch(it -> it.getIdentifier().hasName())) // + assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) // .describedAs(String.format("<%s> (%s)", query, label)) // .isEqualTo(expected); } From 8039dbda26e4989561fa4f395d1779df28546d3d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 Mar 2025 15:19:59 +0100 Subject: [PATCH 50/94] 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 --- .../repository/query/HqlParserBenchmarks.java | 7 +- .../JSqlParserQueryEnhancerBenchmarks.java | 7 +- .../repository/JpaSpecificationExecutor.java | 11 - .../data/jpa/repository/NativeQuery.java | 1 + .../data/jpa/repository/Query.java | 1 + .../config/EnableJpaRepositories.java | 1 + .../query/AbstractStringBasedJpaQuery.java | 98 ++-- .../jpa/repository/query/BindableQuery.java | 67 --- .../jpa/repository/query/DeclaredQueries.java | 148 ++++++ .../jpa/repository/query/DeclaredQuery.java | 41 +- .../repository/query/DefaultEntityQuery.java | 159 ++++++ .../query/DefaultQueryEnhancer.java | 32 +- .../query/EmptyIntrospectedQuery.java | 53 +- .../jpa/repository/query/EntityQuery.java | 83 ++- .../query/JSqlParserQueryEnhancer.java | 25 +- .../query/JpaQueryConfiguration.java | 1 + .../repository/query/JpaQueryEnhancer.java | 61 +-- .../query/JpaQueryLookupStrategy.java | 31 +- .../jpa/repository/query/JpaQueryMethod.java | 54 +- .../data/jpa/repository/query/NamedQuery.java | 13 +- .../jpa/repository/query/NativeJpaQuery.java | 33 +- .../query/ParameterBinderFactory.java | 10 +- ...ectedQuery.java => ParametrizedQuery.java} | 42 +- ...tringQuery.java => PreprocessedQuery.java} | 490 ++++++++---------- .../jpa/repository/query/QueryEnhancer.java | 68 +-- .../query/QueryEnhancerFactories.java | 23 +- .../query/QueryEnhancerFactory.java | 8 +- .../query/QueryEnhancerSelector.java | 4 +- .../query/QueryParameterSetterFactory.java | 14 +- .../{NativeQuery.java => QueryProvider.java} | 29 +- .../data/jpa/repository/query/QueryUtils.java | 12 +- .../jpa/repository/query/SimpleJpaQuery.java | 14 +- .../jpa/repository/query/StructuredQuery.java | 24 - ...edStringQuery.java => TemplatedQuery.java} | 57 +- ...ctStringBasedJpaQueryIntegrationTests.java | 5 +- .../AbstractStringBasedJpaQueryUnitTests.java | 9 +- ....java => DefaultEntityQueryUnitTests.java} | 157 +++--- .../query/DefaultQueryEnhancerUnitTests.java | 11 +- .../EqlParserQueryEnhancerUnitTests.java | 2 +- .../query/EqlQueryTransformerTests.java | 9 +- .../HqlParserQueryEnhancerUnitTests.java | 2 +- .../query/HqlQueryTransformerTests.java | 9 +- .../JSqlParserQueryEnhancerUnitTests.java | 130 ++--- .../JpqlParserQueryEnhancerUnitTests.java | 2 +- .../query/JpqlQueryTransformerTests.java | 10 +- .../query/NativeJpaQueryUnitTests.java | 11 +- .../ParameterBindingParserUnitTests.java | 3 +- .../query/QueryEnhancerFactoryUnitTests.java | 9 +- .../query/QueryEnhancerTckTests.java | 8 +- .../query/QueryEnhancerUnitTests.java | 285 +++++----- .../QueryParameterSetterFactoryUnitTests.java | 11 +- .../query/SimpleJpaQueryUnitTests.java | 29 +- ...ests.java => TemplatedQueryUnitTests.java} | 68 +-- .../repository/query/TestEntityQuery.java} | 30 +- .../modules/ROOT/pages/jpa/query-methods.adoc | 129 ++++- 55 files changed, 1472 insertions(+), 1179 deletions(-) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{IntrospectedQuery.java => ParametrizedQuery.java} (64%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{StringQuery.java => PreprocessedQuery.java} (61%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{NativeQuery.java => QueryProvider.java} (63%) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/{ExpressionBasedStringQuery.java => TemplatedQuery.java} (63%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/{StringQueryUnitTests.java => DefaultEntityQueryUnitTests.java} (85%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/{ExpressionBasedStringQueryUnitTests.java => TemplatedQueryUnitTests.java} (71%) rename spring-data-jpa/src/{main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java => test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java} (53%) diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java index ecbb4eb238..d1465ed1bc 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/HqlParserBenchmarks.java @@ -27,6 +27,8 @@ 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 static class BenchmarkParameters { DeclaredQuery query; Sort sort = Sort.by("foo"); QueryEnhancer enhancer; + QueryEnhancer.QueryRewriteInformation rewriteInformation; @Setup(Level.Iteration) public void doSetup() { @@ -57,12 +60,14 @@ OR TREAT(p AS SmallProject).name LIKE 'Persist%' 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); } } diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java index a5c9cdce23..f4121c28ed 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerBenchmarks.java @@ -29,6 +29,8 @@ 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 static class BenchmarkParameters { 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 void doSetup() throws IOException { 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); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index ffd6f55529..536ff5bca2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -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 @@ default List findAll(PredicateSpecification spec) { * Returns a {@link Page} of entities matching the given {@link Specification}. *

      * Supports counting the total number of entities matching the {@link Specification}. - *

      * * @param spec can be {@literal null}, if no {@link Specification} is given all entities matching {@code } will be * selected. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java index d10c90b68c..d12036c74b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java @@ -94,4 +94,5 @@ * Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query. */ String sqlResultSetMapping() default ""; + } 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 12ff41bb71..4405d29bbb 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 @@ -90,4 +90,5 @@ * @since 3.0 */ Class queryRewriter() default QueryRewriter.IdentityQueryRewriter.class; + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java index 68a173f059..22f32ed2de 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java @@ -178,4 +178,5 @@ * @since 4.0 */ Class queryEnhancerSelector() default QueryEnhancerSelector.DefaultQueryEnhancerSelector.class; + } 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 30c47bf8ec..013d0c312e 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 @@ -20,9 +20,9 @@ import java.util.Objects; -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; @@ -32,7 +32,6 @@ import org.springframework.data.util.Lazy; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; -import org.springframework.util.StringUtils; /** * Base class for {@link String} based JPA queries. @@ -49,8 +48,8 @@ */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final StringQuery query; - private final Lazy countQuery; + private final EntityQuery query; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -64,25 +63,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()); @@ -108,21 +124,25 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri "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 = processor.getReturnedType(); - 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. return parameterBinder.get().bindAndPrepare(query, accessor); } - String getSortedQueryString(Sort sort, ReturnedType returnedType) { + QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) { return querySortRewriter.getSorted(query, sort, returnedType); } @@ -131,7 +151,7 @@ protected ParameterBinder createBinder() { return createBinder(query); } - protected ParameterBinder createBinder(IntrospectedQuery query) { + protected ParameterBinder createBinder(ParametrizedQuery query) { return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), query, valueExpressionDelegate, valueExpressionContextProvider); } @@ -164,7 +184,7 @@ public EntityQuery getQuery() { /** * @return the countQuery */ - public IntrospectedQuery getCountQuery() { + public ParametrizedQuery getCountQuery() { return countQuery.get(); } @@ -172,11 +192,11 @@ public IntrospectedQuery getCountQuery() { * 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); @@ -205,8 +225,8 @@ protected String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nulla : 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())); } @@ -214,7 +234,7 @@ String applySorting(CachableQuery cachableQuery) { * Query Sort Rewriter interface. */ interface QuerySortRewriter { - String getSorted(StringQuery query, Sort sort, ReturnedType returnedType); + QueryProvider getSorted(EntityQuery query, Sort sort, ReturnedType returnedType); } /** @@ -224,28 +244,28 @@ enum SimpleQuerySortRewriter implements QuerySortRewriter { 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; } } @@ -254,22 +274,22 @@ public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) */ class CachingQuerySortRewriter implements QuerySortRewriter { - private final ConcurrentLruCache queryCache = new ConcurrentLruCache<>(16, + private final ConcurrentLruCache 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)); @@ -285,12 +305,12 @@ public String getSorted(StringQuery query, Sort sort, ReturnedType returnedType) */ 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(); @@ -298,7 +318,7 @@ static class CachableQuery { this.returnedType = returnedType; } - StringQuery getDeclaredQuery() { + EntityQuery getDeclaredQuery() { return query; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java deleted file mode 100644 index 66e95a93c5..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BindableQuery.java +++ /dev/null @@ -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 bindings; - private final boolean usesJdbcStyleParameters; - - public BindableQuery(DeclaredQuery source, String bindableQueryString, List 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 getBindings() { - return Collections.unmodifiableList(bindings); - } -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java new file mode 100644 index 0000000000..2f6db9c5f7 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQueries.java @@ -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 + "]"; + } + + } + +} 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 152c40c385..2cea734dbc 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 @@ -17,13 +17,17 @@ /** * Interface defining the contract to represent a declared query. + *

      + * 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 @@ static DeclaredQuery jpqlQuery(String jpql) { * @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 true if native query otherwise false + * @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); + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java new file mode 100644 index 0000000000..bde36d1535 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java @@ -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. + *

      + * 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 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 getParameterBindings() { + return query.getBindings(); + } + + } + +} 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 3d4aba2859..456c3139b3 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 @@ -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 @@ * @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 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 String rewrite(QueryRewriteInformation rewriteInformation) { @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 String getProjection() { } @Override - public Set getJoinAliases() { - return this.joinAliases; - } - - @Override - public StructuredQuery getQuery() { + public QueryProvider getQuery() { return this.query; } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index ec92ee81cf..a0ef2363b6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java @@ -18,39 +18,43 @@ 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 hasNamedParameter() { + public boolean hasParameterBindings() { return false; } @Override - public String getQueryString() { - return ""; + public boolean usesJdbcStyleParameters() { + return false; } - public @Nullable String getAlias() { - return null; + @Override + public boolean hasNamedParameter() { + return false; } @Override - public boolean isNativeQuery() { - return false; + public List getParameterBindings() { + return Collections.emptyList(); + } + + public @Nullable String getAlias() { + return null; } @Override @@ -69,27 +73,18 @@ public String getQueryString() { } @Override - public List getParameterBindings() { - return Collections.emptyList(); - } - - @Override - public IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection) { - return EMPTY_QUERY; + public ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection) { + return INSTANCE; } @Override - public String applySorting(Sort sort) { - return ""; + public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation) { + return this; } @Override - public boolean usesJdbcStyleParameters() { - return false; + public String toString() { + return ""; } - @Override - public DeclaredQuery getDeclaredQuery() { - return DeclaredQuery.nativeQuery(""); - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java index b959d3810e..b28fa9f10d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -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. + *

      + * 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 -> {}); - } - - /** - * 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 -> {}); - } + 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, QueryEnhancerSelector selector) { - return ObjectUtils.isEmpty(query) ? EmptyIntrospectedQuery.EMPTY_QUERY - : new StringQuery(query, true, selector, parameterBindings -> {}); + return new DefaultEntityQuery(preparsed, enhancerFactory); } /** @@ -84,6 +57,14 @@ static EntityQuery introspectNativeQuery(String query, QueryEnhancerSelector sel */ boolean isDefaultProjection(); + /** + * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. + * @since 2.0.6 + */ + default boolean usesPaging() { + 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 @@ -92,16 +73,16 @@ static EntityQuery introspectNativeQuery(String query, QueryEnhancerSelector sel * @param countQueryProjection an optional return type for the query. * @return a new {@literal IntrospectedQuery} instance. */ - IntrospectedQuery deriveCountQuery(@Nullable String countQueryProjection); - - String applySorting(Sort sort); + ParametrizedQuery deriveCountQuery(@Nullable String countQueryProjection); /** - * @return whether paging is implemented in the query itself, e.g. using SpEL expressions. - * @since 2.0.6 + * 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. */ - default boolean usesPaging() { - return false; - } + QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInformation); } 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 8e052c9eec..82cae525c1 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 @@ -70,7 +70,7 @@ */ 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; @@ -83,7 +83,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); @@ -285,35 +285,20 @@ public String getProjection() { return this.projection; } - @Override - public Set getJoinAliases() { - return joinAliases; - } - public Set 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"); @@ -373,8 +358,8 @@ public String createCountQueryFor(@Nullable String countProjection) { return createCountQueryFor(selectBody, countProjection, primaryAlias); } - 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); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java index 7bce8dc8f7..788c977f25 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryConfiguration.java @@ -54,4 +54,5 @@ public EscapeCharacter getEscapeCharacter() { public ValueExpressionDelegate getValueExpressionDelegate() { return valueExpressionDelegate; } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index ff4b6efb7d..04d134c0ad 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -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.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 implements QueryEnhancer { private final Q queryInformation; private final String projection; private final SortedQueryRewriteFunction sortFunction; - private final BiFunction> countQueryFunction; + private final BiFunction<@Nullable String, Q, ParseTreeVisitor> countQueryFunction; JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector, SortedQueryRewriteFunction sortFunction, - BiFunction> countQueryFunction) { + BiFunction<@Nullable String, Q, ParseTreeVisitor> countQueryFunction) { this.context = context; this.sortFunction = sortFunction; @@ -142,7 +140,7 @@ static void configureParser(String query, String grammar, Lexer lexer, Parser pa } /** - * 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 @@ public static JpaQueryEnhancer forJpql(String query) { } /** - * 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 @@ public static JpaQueryEnhancer forHql(String query) { } /** - * 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 @@ public boolean hasConstructorExpression() { } /** - * 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 @@ public boolean hasConstructorExpression() { } /** - * 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 getJoinAliases() { - return Set.of(); - } - /** * Look up the {@link DeclaredQuery} from the query parser. */ @@ -232,17 +218,6 @@ public DeclaredQuery getQuery() { 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 @@ public String rewrite(QueryRewriteInformation rewriteInformation) { .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. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index b032000ba3..719e838fe0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -32,7 +32,6 @@ 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 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfigurat 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 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfigurat 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 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfigurat * * @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); } /** 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 25f50b9f22..9a702d6464 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 @@ -27,9 +27,9 @@ 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 org.springframework.data.jpa.repository.query.Meta getQueryMetaAttributes 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 String getRequiredAnnotatedQuery() throws IllegalStateException { 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 String getRequiredAnnotatedQuery() throws IllegalStateException { 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 @@ 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() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index b81820c6f4..de26c392b7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -80,7 +80,7 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelecto 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 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelecto 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)); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 4c2fefe23f..35045c5e25 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -19,16 +19,15 @@ 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 annotation = annotations.get(NativeQuery.class); + 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 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); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java index fc34606f45..00aef26195 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinderFactory.java @@ -78,13 +78,13 @@ static ParameterBinder createBinder(JpaParameters parameters, List getBindings(JpaParameters parameters) { private static Iterable createSetters(List parameterBindings, QueryParameterSetterFactory... factories) { - return createSetters(parameterBindings, EmptyIntrospectedQuery.EMPTY_QUERY, factories); + return createSetters(parameterBindings, EmptyIntrospectedQuery.INSTANCE, factories); } private static Iterable createSetters(List parameterBindings, - IntrospectedQuery query, QueryParameterSetterFactory... strategies) { + ParametrizedQuery query, QueryParameterSetterFactory... strategies) { List setters = new ArrayList<>(parameterBindings.size()); for (ParameterBinding parameterBinding : parameterBindings) { @@ -141,7 +141,7 @@ private static Iterable createSetters(List + * 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(); - } - - /** - * @return whether the underlying query has at least one named parameter. - */ - boolean hasNamedParameter(); - - /** - * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query. - */ - boolean isDefaultProjection(); +interface ParametrizedQuery extends QueryProvider { /** - * Returns the {@link ParameterBinding}s registered. + * @return whether the underlying query has at least one parameter. */ - List 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 @@ default String getQueryString() { */ boolean usesJdbcStyleParameters(); + /** + * @return whether the underlying query has at least one named parameter. + */ + boolean hasNamedParameter(); + + /** + * @return the registered {@link ParameterBinding}s. + */ + List getParameterBindings(); + } 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/PreprocessedQuery.java similarity index 61% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java index e15bcb2e33..0c5061b529 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/PreprocessedQuery.java @@ -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 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.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,268 +43,126 @@ 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. + *

      + * 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). + *

      + * 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 bindings; + private final boolean usesJdbcStyleParameters; private final boolean containsPageableInSpel; - private final QueryEnhancerFactory queryEnhancerFactory; - private final QueryEnhancer queryEnhancer; - private final boolean hasNamedParameters; - - /** - * 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> parameterPostProcessor) { - - Assert.hasText(query, "Query must not be null or empty"); + private final boolean hasNamedBindings; - 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()); + private PreprocessedQuery(DeclaredQuery query, List bindings, boolean usesJdbcStyleParameters, + boolean containsPageableInSpel) { + this.source = query; + this.bindings = bindings; + this.usesJdbcStyleParameters = usesJdbcStyleParameters; + this.containsPageableInSpel = containsPageableInSpel; + this.hasNamedBindings = containsNamedParameter(bindings); } - /** - * 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> 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); + private static boolean containsNamedParameter(List bindings) { - this.queryEnhancerFactory = selector.select(source); - this.queryEnhancer = queryEnhancerFactory.create(this.bindableQuery); - parameterPostProcessor.accept(this.bindableQuery.getBindings()); - this.hasNamedParameters = containsNamedParameter(this.bindableQuery.getBindings()); + for (ParameterBinding parameterBinding : bindings) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin() + .isMethodArgument()) { + return true; + } + } + return false; } /** - * internal copy constructor + * 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 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; - this.containsPageableInSpel = containsPageableInSpel; - } - - QueryEnhancer getQueryEnhancer() { - return queryEnhancer; - } - - /** - * Returns whether we have found some like bindings. + * @param declaredQuery the source query to parse. + * @return a parsed {@link PreprocessedQuery}. */ - boolean hasParameterBindings() { - return this.bindableQuery.hasBindings(); - } - - String getProjection() { - return this.queryEnhancer.getProjection(); - } - - @Override - public String getQueryString() { - return bindableQuery.getQueryString(); - } - - @Override - public List 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 identifier = binding::bindsTo; - Predicate notCompatible = Predicate.not(binding::isCompatibleWith); - - // replace incompatible bindings - if ( derivedBindings.removeIf( - it -> identifier.test(it) && notCompatible.test(it))) { - derivedBindings.add(binding); - } - } - } + public static PreprocessedQuery parse(DeclaredQuery declaredQuery) { + return ParameterBindingParser.INSTANCE.parse(declaredQuery.getQueryString(), declaredQuery::rewrite, + parameterBindings -> { }); } @Override - public String applySorting(Sort sort) { - return queryEnhancer.applySorting(sort); - } - - @Override - public boolean usesJdbcStyleParameters() { - return bindableQuery.usesJdbcStyleParameters(); - } - - public @Nullable String getAlias() { - return queryEnhancer.detectAlias(); + public String getQueryString() { + return source.getQueryString(); } @Override - public boolean hasConstructorExpression() { - return queryEnhancer.hasConstructorExpression(); + public boolean isNative() { + return source.isNative(); } - @Override - public boolean isDefaultProjection() { - return getProjection().equalsIgnoreCase(getAlias()); + boolean hasBindings() { + return !bindings.isEmpty(); } - @Override - public boolean hasNamedParameter() { - return hasNamedParameters; + boolean hasNamedBindings() { + return this.hasNamedBindings; } - @Override - public boolean usesPaging() { + boolean containsPageableInSpel() { return containsPageableInSpel; } - @Override - public DeclaredQuery getDeclaredQuery() { - return bindableQuery; + boolean usesJdbcStyleParameters() { + return usesJdbcStyleParameters; } - private static boolean containsNamedParameter(List bindings) { - for (ParameterBinding parameterBinding : bindings) { - if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { - return true; - } - } - return false; + List getBindings() { + return Collections.unmodifiableList(bindings); } /** - * Value object to track and allocate used parameter index labels in a query. + * 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 */ - static class IndexedParameterLabels { - - private final TreeSet usedLabels; - private final boolean sequential; - - public IndexedParameterLabels(Set usedLabels) { - - this.usedLabels = usedLabels instanceof TreeSet ts ? ts : new TreeSet(usedLabels); - this.sequential = isSequential(usedLabels); - } - - private static boolean isSequential(Set 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."); - } + @Override + public PreprocessedQuery rewrite(String newQueryString) { - usedLabels.add(index); + return ParameterBindingParser.INSTANCE.parse(newQueryString, source::rewrite, derivedBindings -> { - return index; - } + // 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)) { - private int attemptAllocate(int attempts) { + for (ParameterBinding binding : bindings) { - for (int i = 0; i < attempts; i++) { + Predicate identifier = binding::bindsTo; + Predicate notCompatible = Predicate.not(binding::isCompatibleWith); - if (usedLabels.contains(i + 1)) { - continue; + // replace incompatible bindings + if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) { + derivedBindings.add(binding); + } } - - return i + 1; } + }); + } - return -1; - } - - public boolean hasLabels() { - return !usedLabels.isEmpty(); - } + @Override + public String toString() { + return "ParametrizedQuery[" + source + ", " + bindings + ']'; } /** @@ -333,7 +186,7 @@ enum ParameterBindingParser { 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 @@ enum ParameterBindingParser { * 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 declaredQueryFactory, + Consumer> parameterBindingPostProcessor) { IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query)); boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels(); + List 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 @@ BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(Dec 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 @@ BindableQuery parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(Dec 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 bindingFactory = switch (ParameterBindingType.of(typeSource)) { + ParameterBinding.BindingIdentifier targetBinding = queryParameter; + Function 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 @@ static ParameterBindingType of(String typeSource) { } } - - /** * 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 methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); + private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); private final Consumer registration; @@ -611,21 +477,22 @@ public ParameterBindings(List bindings, Consumer bindingFactory, IndexedParameterLabels parameterLabels) { + ParameterBinding.BindingIdentifier register(ParameterBinding.BindingIdentifier identifier, + ParameterBinding.ParameterOrigin origin, + Function 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 bindingsForOrigin = getBindings(methodArgument); if (!isBound(identifier)) { @@ -645,7 +512,7 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, } } - BindingIdentifier syntheticIdentifier; + ParameterBinding.BindingIdentifier syntheticIdentifier; if (identifier.hasName() && methodArgument.hasName()) { int index = 0; @@ -654,9 +521,10 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, 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 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, } 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 getBindings(BindingIdentifier identifier) { + private List getBindings(ParameterBinding.BindingIdentifier identifier) { return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); } @@ -678,4 +547,79 @@ public void register(ParameterBinding parameterBinding) { registration.accept(parameterBinding); } } + + /** + * Value object to track and allocate used parameter index labels in a query. + */ + static class IndexedParameterLabels { + + private final TreeSet usedLabels; + private final boolean sequential; + + public IndexedParameterLabels(Set usedLabels) { + + this.usedLabels = usedLabels instanceof TreeSet ts ? ts : new TreeSet(usedLabels); + this.sequential = isSequential(usedLabels); + } + + private static boolean isSequential(Set 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(); + } + } + } 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 528426f82f..2810f957c0 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 @@ -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 @@ * * @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 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 countProjection. + * 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 @@ default String createCountQueryFor() { /** * Interface to describe the information needed to rewrite a query. * - * @since 3.5 + * @since 4.0 */ interface QueryRewriteInformation { @@ -129,6 +104,7 @@ interface QueryRewriteInformation { * @return type expected to be returned by the query. */ ReturnedType getReturnedType(); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java index 07ed8642c3..face0778a0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactories.java @@ -25,10 +25,11 @@ * 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 boolean supports(DeclaredQuery query) { } @Override - public QueryEnhancer create(StructuredQuery query) { + public QueryEnhancer create(QueryProvider query) { return new DefaultQueryEnhancer(query); } }, @@ -65,11 +66,12 @@ public QueryEnhancer create(StructuredQuery query) { 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 QueryEnhancer create(StructuredQuery query) { 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 static QueryEnhancerFactory eql() { public static QueryEnhancerFactory jpql() { return BuiltinQueryEnhancerFactories.JPQL; } + } 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 26bdf4b5b2..0233798594 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 @@ -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}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java index 93268c6387..fd5f1da6ae 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancerSelector.java @@ -21,9 +21,10 @@ * Interface declaring a strategy to select a {@link QueryEnhancer} for a given {@link DeclaredQuery query}. *

      * 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 QueryEnhancerFactory select(DeclaredQuery query) { } } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index 3a9d2af875..6d6196b8ef 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -20,9 +20,9 @@ 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 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar } @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 @@ private static class ExpressionBasedQueryParameterSetterFactory extends QueryPar 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 @@ private static class BasicQueryParameterSetterFactory extends QueryParameterSett } @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 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { } @Override - public @Nullable QueryParameterSetter create(ParameterBinding binding, IntrospectedQuery query) { + public @Nullable QueryParameterSetter create(ParameterBinding binding, ParametrizedQuery query) { if (binding instanceof ParameterBinding.PartTreeParameterBinding ptb) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java similarity index 63% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java index 6ba9f81ba6..98de7da6eb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryProvider.java @@ -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 { - - private final String sql; - - NativeQuery(String sql) { - this.sql = sql; - } +public interface QueryProvider { - @Override - public boolean isNativeQuery() { - return true; - } + /** + * Return the query string. + * + * @return the query string. + */ + String getQueryString(); - @Override - public String getQueryString() { - return sql; - } } 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 1619dedb86..b16d2ef5dd 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 @@ -445,10 +445,8 @@ private static String toJpaDirection(Order order) { * * @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 static Query applyAndBind(String queryString, Iterable entities, E * * @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 static String createCountQueryFor(String originalQuery) { * @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); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index b913061ad6..b042318b13 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java @@ -18,11 +18,9 @@ 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); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java deleted file mode 100644 index 2ebfcb0549..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StructuredQuery.java +++ /dev/null @@ -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(); -} 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/TemplatedQuery.java similarity index 63% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/TemplatedQuery.java index b6c93b5604..487a7b11f8 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/TemplatedQuery.java @@ -23,12 +23,11 @@ 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. *

      * Currently, the following template variables are available: *

        @@ -42,7 +41,7 @@ * @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 @@ private static String renderQueryIfExpressionOrReturnQuery(String query, JpaEnti 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 @@ private static boolean containsExpression(String query) { 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()); - } - } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java index 204471b6d9..3d77980fb6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryIntegrationTests.java @@ -68,9 +68,10 @@ void createsNormalQueryForJpaManagedReturnTypes() throws Exception { 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()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index adc489cc98..953203134f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java @@ -150,7 +150,7 @@ public EntityManager get() { } @Override - protected String applySorting(CachableQuery query) { + protected QueryProvider applySorting(CachableQuery query) { captureInvocation("applySorting", query); @@ -158,12 +158,13 @@ protected String applySorting(CachableQuery query) { } @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; } 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/DefaultEntityQueryUnitTests.java similarity index 85% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java index 3c18eda1fb..874ff77c99 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/DefaultEntityQueryUnitTests.java @@ -28,11 +28,10 @@ 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 @@ -43,13 +42,13 @@ * @author Mark Paluch * @author Aleksei Elin */ -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); @@ -66,8 +65,8 @@ void doesNotConsiderPlainLikeABinding() { @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()) @@ -90,7 +89,7 @@ void detectsPositionalLikeBindings() { @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(); @@ -116,7 +115,8 @@ void detectsAnonymousLikeBindings() { @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"); @@ -133,7 +133,7 @@ void detectsNamedLikeBindings() { @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); @@ -164,7 +164,7 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() { @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); @@ -197,7 +197,7 @@ void rewritesNamedLikeToUniqueParametersRetainingCountQuery() { @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); @@ -224,7 +224,7 @@ void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() { @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(); @@ -238,7 +238,7 @@ void rewritesPositionalLikeToUniqueParametersIfNecessary() { @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); @@ -246,7 +246,8 @@ void reusesNamedLikeBindingsWherePossible() { 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()) @@ -256,7 +257,7 @@ void reusesNamedLikeBindingsWherePossible() { @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); @@ -264,7 +265,7 @@ void reusesPositionalLikeBindingsWherePossible() { 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"); @@ -273,7 +274,7 @@ void reusesPositionalLikeBindingsWherePossible() { @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); @@ -295,8 +296,8 @@ void shouldRewritePositionalBindingsWithParameterReuse() { @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) @@ -309,8 +310,8 @@ void createsDistinctBindingsForIndexedSpel() { @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) @@ -322,7 +323,7 @@ void createsDistinctBindingsForNamedSpel() { 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); @@ -337,7 +338,7 @@ void detectsNamedInParameterBindings() { 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); @@ -354,7 +355,7 @@ void detectsMultipleNamedInParameterBindings() { 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)"); @@ -375,7 +376,7 @@ void deriveCountQueryWithNamedInRetainsOrigin() { 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)"); @@ -396,7 +397,7 @@ void deriveCountQueryWithPositionalInRetainsOrigin() { 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); @@ -410,7 +411,7 @@ void detectsPositionalInParameterBindings() { @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(); @@ -423,7 +424,7 @@ void allowsReuseOfParameterWithInAndRegularBinding() { 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); @@ -442,7 +443,7 @@ void allowsReuseOfParameterWithInAndRegularBinding() { 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"); } @@ -451,7 +452,7 @@ void detectsPositionalInParameterBindingsAndExpressions() { 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"); } @@ -459,17 +460,17 @@ void detectsPositionalInParameterBindingsAndExpressionsWithReuse() { @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); @@ -484,17 +485,17 @@ void countQueryDerivationRetainsNamedExpressionParameters() { @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); @@ -510,7 +511,7 @@ void countQueryDerivationRetainsIndexedExpressionParameters() { 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); @@ -526,13 +527,13 @@ void detectsMultiplePositionalInParameterBindings() { @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 bindings = query.getParameterBindings(); assertThat(bindings).hasSize(1); @@ -543,8 +544,10 @@ void treatsGreaterThanBindingAsSimpleBinding() { @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 bindings = query.getParameterBindings(); @@ -559,7 +562,8 @@ void removesLikeBindingsFromQueryIfQueryContainsSimpleBinding() { @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 bindings = query.getParameterBindings(); @@ -570,7 +574,7 @@ void detectsInBindingWithParentheses() { @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 bindings = query.getParameterBindings(); @@ -581,7 +585,7 @@ void detectsInBindingWithSpecialFrenchCharactersInParentheses() { @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 bindings = query.getParameterBindings(); @@ -592,7 +596,7 @@ void detectsInBindingWithSpecialCharactersInParentheses() { @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 bindings = query.getParameterBindings(); @@ -603,7 +607,7 @@ void detectsInBindingWithSpecialAsianCharactersInParentheses() { @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 bindings = query.getParameterBindings(); @@ -614,7 +618,7 @@ void detectsInBindingWithSpecialCharactersAndWordCharactersMixedInParentheses() @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(); @@ -625,7 +629,7 @@ void shouldReplaceAllNamedExpressionParametersWithInClause() { @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(); @@ -636,8 +640,8 @@ void shouldReplaceExpressionWithLikeParameters() { @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"); @@ -653,12 +657,11 @@ void shouldReplaceAllPositionExpressionParametersWithInClause() { @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(); } /** @@ -669,14 +672,16 @@ void detectsConstructorExpressions() { 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 bindings = query.getParameterBindings(); assertThat(bindings).isNotEmpty(); @@ -703,7 +708,7 @@ void getProjection() { 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); } @@ -727,7 +732,7 @@ void getAlias() { 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); } @@ -780,7 +785,7 @@ void ignoresQuotedNamedParameterLookAlike() { 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(); @@ -800,16 +805,18 @@ void failOnMixedBindingsWithoutIndex() { 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 testQueries = Arrays.asList( // @@ -820,7 +827,7 @@ void makesUsageOfJdbcStyleParameterAvailable() { for (String testQuery : testQueries) { - assertThat(new StringQuery(testQuery, false) // + assertThat(new TestEntityQuery(testQuery, false) // .usesJdbcStyleParameters()) // .describedAs(testQuery) // .describedAs(testQuery) // @@ -832,7 +839,7 @@ void makesUsageOfJdbcStyleParameterAvailable() { 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(); @@ -852,7 +859,7 @@ void isNotDefaultProjection() { "select a, b from C"); for (String queryString : queriesWithoutDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isFalse(); } @@ -869,7 +876,7 @@ void isNotDefaultProjection() { ); for (String queryString : queriesWithDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isTrue(); } @@ -879,7 +886,7 @@ void isNotDefaultProjection() { 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(); @@ -891,7 +898,7 @@ void questionMarkInStringLiteralWithParameters() { 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) // @@ -902,7 +909,7 @@ void usingPipesWithNamedParameter() { 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) // @@ -911,9 +918,8 @@ void usingGreaterThanWithNamedParameter() { 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) // @@ -926,7 +932,8 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) { 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)) // diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java index 9a5c9ff30f..7dd6dd757c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java @@ -21,6 +21,8 @@ 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 @@ void shouldApplySorting() { 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 @@ void shouldApplySorting() { @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"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java index 5303378b84..dbe4d45a9f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java @@ -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()); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java index 61436aae55..8f93859699 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -29,6 +29,8 @@ 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 @@ void applySortingAccountsForNewlinesInSubselect() { 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 @@ private void assertCountQuery(String originalQuery, String countQuery) { } 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) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java index 916db5e06a..f25e9fc2ee 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java @@ -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()); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index d9634ea91c..cd2c3987fc 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -33,6 +33,8 @@ 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 @@ void applySortingAccountsForNewlinesInSubselect() { 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 @@ private void assertCountQuery(String originalQuery, String countQuery) { } 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) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index 52787f910f..4a0be8de58 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -25,6 +25,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.ReturnedType; /** * TCK Tests for {@link JSqlParserQueryEnhancer}. @@ -46,7 +48,8 @@ void shouldApplySorting() { 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"); } @@ -62,7 +65,7 @@ void countQueriesShouldConsiderPrimaryTableAlias() { 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"); } @@ -82,16 +85,16 @@ void setOperationListWorks() { + "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(); @@ -105,16 +108,16 @@ void complexSetOperationListWorks() { + "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(); @@ -132,16 +135,16 @@ void deeplyNestedcomplexSetOperationListWorks() { + "\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(); @@ -152,16 +155,15 @@ void valuesStatementsWorks() { 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(); @@ -173,18 +175,18 @@ void withStatementsWorks() { 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()).isEqualToIgnoringCase( + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase( "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(); @@ -196,18 +198,18 @@ void multipleWithStatementsWorks() { 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()).isEqualToIgnoringCase( + assertThat(queryEnhancer.createCountQueryFor(null)).isEqualToIgnoringCase( "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(); @@ -216,15 +218,15 @@ void multipleWithStatementsWorks() { @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(); @@ -232,15 +234,14 @@ void truncateStatementShouldWork() { @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(); @@ -257,4 +258,9 @@ static Stream mergeStatementWorksSource() { null)); } + private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { + return new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java index 32f9e965a9..44256fe4c9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java @@ -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()); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index 1a38f729e2..39ed9b6d9d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -29,6 +29,8 @@ 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 @@ void applySortingAccountsForNewlinesInSubselect() { 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 @@ private void assertCountQuery(String originalQuery, String countQuery) { } 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) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java index fa44d2ca11..c17cc49f94 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NativeJpaQueryUnitTests.java @@ -30,7 +30,6 @@ 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 @@ void shouldApplySorting() { 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 { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java index edcaf0e4ea..f765860a27 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterBindingParserUnitTests.java @@ -18,7 +18,6 @@ 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 @@ void identificationOfParameters() { 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)) // 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 aaccc4cad4..2f52341214 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 @@ -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 @@ void createsParsingImplementationForNonNativeQuery() { @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); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java index 7a0f4e1783..98e19b6cb7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java @@ -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 @@ static Stream jpqlCountQueries() { 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 @@ static Stream nativeQueriesWithVariables() { // 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); 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 0e5f44cd8b..da113f567b 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 @@ -20,7 +20,6 @@ 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.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 @@ * @author Diego Krupitza * @author Geoffrey Deremetz * @author Krzysztof Krason + * @author Mark Paluch */ class QueryEnhancerUnitTests { @@ -78,7 +81,7 @@ void allowsShortJpaSyntax() { @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 @@ void detectsAliasWithUCorrectly(IntrospectedQuery query, String alias) { public static Stream 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 @@ void allowsFullyQualifiedEntityNamesInQuery() { @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 @@ void appliesIgnoreCaseOrderingCorrectly() { 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 @@ void appendsIgnoreCaseOrderingCorrectly() { 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 @@ void projectsCountQueriesForQueriesWithSubSelects() { @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 @@ void doesNotPrefixSortsIfFunction() { 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 @@ void findsExistingOrderByIndependentOfCase() { @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 @@ void createCountQueryFromDeleteQuery() { @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 @@ void createCountQueryFromTheGivenCountProjection() { @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 @@ void detectsAliasInQueryContainingLineBreaks() { @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 @@ void detectsConstructorExpressionInDistinctQuery() { @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 @@ void detectsComplexConstructorExpression() { @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 @@ void detectsConstructorExpressionWithLineBreaks() { @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 aliases = QueryUtils.getOuterJoinAliases(queryString); - - StringQuery nativeQuery = new StringQuery(queryString, true); - Set 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 @@ void discoversAliasWithComplexFunction() { @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 @@ void detectsAliasWithGroupAndOrderBy() { @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 @@ void appliesSortCorrectlyForFieldAliases() { @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 @@ void appliesOrderingCorrectlyForFieldAliasWithIgnoreCase() { @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 @@ void appliesSortCorrectlyForFunctionAliases() { @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 @@ void appliesSortCorrectlyForSimpleField() { @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 @@ void detectsAliasWithGroupAndOrderByWithLineBreaks() { @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 @@ void findProjectionClauseWithDistinct(IntrospectedQuery query, String expected) public static Stream 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 @@ void findProjectionClauseWithSubselectNative() { // 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 aliases) { - - StringQuery nativeQuery = new StringQuery(queryString, true); - StringQuery nonNativeQuery = new StringQuery(queryString, false); - - Set nativeJoinAliases = getEnhancer(nativeQuery).getJoinAliases(); - Set 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 @@ void correctApplySortOnComplexNestedFunctionQuery() { + " 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 @@ void modifyingQueriesAreDetectedCorrectly() { 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 @@ void insertStatementIsProcessedSameAsDefault(String insertQuery) { 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 @@ public static Stream detectsJoinAliasesCorrectlySource() { } 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(DefaultEntityQuery originalQuery, String countQuery) { + assertThat(getEnhancer(originalQuery).createCountQueryFor(null)).isEqualToIgnoringCase(countQuery); } - private static void assertCountQuery(StringQuery originalQuery, String countQuery) { - assertThat(getEnhancer(originalQuery).createCountQueryFor()).isEqualToIgnoringCase(countQuery); + private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { + return new DefaultQueryRewriteInformation(sort, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); } - private static QueryEnhancer getEnhancer(IntrospectedQuery query) { - return QueryEnhancerFactory.forQuery(query.getDeclaredQuery()).create(query.getDeclaredQuery()); + private static QueryEnhancer getEnhancer(DeclaredQuery query) { + return QueryEnhancer.create(query); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java index 34d3ab2397..d4fb9a761d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactoryUnitTests.java @@ -53,7 +53,7 @@ void before() { @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 @@ void exceptionWhenQueryContainNamedParametersAndMethodParametersAreNotNamed() { 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 @@ void exceptionWhenBasicQueryContainsInsufficientAmountOfParameters() { 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"); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 5887eab53b..188166d3bd 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -121,7 +121,8 @@ void prefersDeclaredCountQueryOverCreatingOne() throws Exception { 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 @@ void doesNotApplyPaginationToCountQuery() throws Exception { 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 @@ void discoversNativeQuery() throws Exception { 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 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception { 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 @@ void resolvesExpressionInCountQuery() throws Exception { 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 @@ private AbstractJpaQuery createJpaQuery(Method method) { 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 countQueryString) { + private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional 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 { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java similarity index 71% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java index a235543017..6581b628f7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java @@ -33,7 +33,7 @@ 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 @@ */ @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 @@ void setUp() { 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 @@ void renderAliasInExpressionQueryCorrectly() { @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 @@ void shouldDetectBindParameterCountCorrectly() { @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 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { @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 @@ void namedExpressionsShouldCreateLikeBindings() { @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 @@ void indexedExpressionsShouldCreateLikeBindings() { @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 @@ void doesTemplatingWhenEntityNameSpelIsPresent() { @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 @@ void doesNoTemplatingWhenEntityNameSpelIsNotPresent() { @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); + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java similarity index 53% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java index 6a8f3cce03..25c0848908 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQuery.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TestEntityQuery.java @@ -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 { - - private final String jpql; - - JpqlQuery(String jpql) { - this.jpql = jpql; - } +class TestEntityQuery extends DefaultEntityQuery { - @Override - public boolean isNativeQuery() { - return false; - } + /** + * Creates a new {@link DefaultEntityQuery} from the given JPQL query. + * + * @param query must not be {@literal null} or empty. + */ + TestEntityQuery(String query, boolean isNative) { - @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))); + } } diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 8dd229918c..c66dc18642 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -308,17 +308,6 @@ public interface UserRepository extends JpaRepository { ---- ==== -[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 { 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 { + + @NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias", + queryRewriter = MyQueryRewriter.class) + List findByNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", + queryRewriter = MyQueryRewriter.class) + List 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, QueryRewriter { + + @Query(value = "select original_user_alias.* from SD_USER original_user_alias", + nativeQuery = true, + queryRewriter = MyRepository.class) + List findByNativeQuery(String param); + + @Query(value = "select original_user_alias from User original_user_alias", + queryRewriter = MyRepository.class) + List 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 8’s 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: From b2f212d5296c10f1e9cd2d80147a5b3f884973f3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Mar 2025 10:06:27 +0100 Subject: [PATCH 51/94] Apply QueryRewriter to count queries as well. We now use QueryRewriter to post-process count queries as well. Previously, only the actual result query has been processed. Closes #3801 --- .../query/AbstractStringBasedJpaQuery.java | 1 - .../query/JpaQueryLookupStrategy.java | 2 +- .../data/jpa/repository/query/NamedQuery.java | 17 ++++++++++------- .../repository/query/NamedQueryUnitTests.java | 9 +++++++-- .../modules/ROOT/pages/jpa/query-methods.adoc | 2 ++ 5 files changed, 20 insertions(+), 11 deletions(-) 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 013d0c312e..013cb9d94e 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 @@ -105,7 +105,6 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Decl }); this.countParameterBinder = Lazy.of(() -> this.createBinder(this.countQuery.get())); - this.queryRewriter = queryConfiguration.getQueryRewriter(method); JpaParameters parameters = method.getParameters(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 719e838fe0..37f2e27d2a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -169,7 +169,7 @@ protected RepositoryQuery resolveQuery(JpaQueryMethod method, JpaQueryConfigurat configuration); } - RepositoryQuery query = NamedQuery.lookupFrom(method, em, configuration.getSelector()); + RepositoryQuery query = NamedQuery.lookupFrom(method, em, configuration); return query != null ? query : NO_QUERY; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index de26c392b7..5bf986d4ba 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -56,11 +56,12 @@ final class NamedQuery extends AbstractJpaQuery { private final @Nullable String countProjection; private final boolean namedCountQueryIsPresent; private final Lazy entityQuery; + private final QueryRewriter queryRewriter; /** * Creates a new {@link NamedQuery}. */ - private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelector selector, QueryRewriter queryRewriter) { + private NamedQuery(JpaQueryMethod method, EntityManager em, JpaQueryConfiguration queryConfiguration) { super(method, em); @@ -68,7 +69,7 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelecto this.countQueryName = method.getNamedCountQueryName(); QueryExtractor extractor = method.getQueryExtractor(); this.countProjection = method.getCountQueryProjection(); - this.queryRewriter = queryRewriter; + this.queryRewriter = queryConfiguration.getQueryRewriter(method); Parameters parameters = method.getParameters(); @@ -104,7 +105,7 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, QueryEnhancerSelecto declaredQuery = DeclaredQuery.jpqlQuery(queryString); } - this.entityQuery = Lazy.of(() -> EntityQuery.create(declaredQuery, selector)); + this.entityQuery = Lazy.of(() -> EntityQuery.create(declaredQuery, queryConfiguration.getSelector())); } /** @@ -138,9 +139,10 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { * @param method must not be {@literal null}. * @param em must not be {@literal null}. * @param selector must not be {@literal null}. + * @param queryConfiguration must not be {@literal null}. */ public static @Nullable RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em, - QueryEnhancerSelector selector) { + JpaQueryConfiguration queryConfiguration) { String queryName = method.getNamedQueryName(); @@ -158,7 +160,7 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { method.isNativeQuery() ? "NativeQuery" : "Query")); } - RepositoryQuery query = new NamedQuery(method, em, selector); + RepositoryQuery query = new NamedQuery(method, em, queryConfiguration); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Found named query '%s'", queryName)); } @@ -193,6 +195,7 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc } else { String countQueryString = entityQuery.get().deriveCountQuery(countProjection).getQueryString(); + countQueryString = potentiallyRewriteQuery(countQueryString, accessor.getSort(), accessor.getPageable()); countQuery = em.createQuery(countQueryString, Long.class); } @@ -235,9 +238,9 @@ protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor acc * @param pageable * @return */ - private String potentiallyRewriteQuery(String originalQuery, Sort sort, Pageable pageable) { + private String potentiallyRewriteQuery(String originalQuery, Sort sort, @Nullable Pageable pageable) { - return pageable.isPaged() // + return pageable != null && pageable.isPaged() // ? queryRewriter.rewrite(originalQuery, pageable) // : queryRewriter.rewrite(originalQuery, sort); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java index 79df5c5198..71bd266f05 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/NamedQueryUnitTests.java @@ -41,6 +41,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryCreationException; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; /** @@ -55,6 +56,9 @@ @MockitoSettings(strictness = Strictness.LENIENT) class NamedQueryUnitTests { + private static final JpaQueryConfiguration CONFIG = new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT); + @Mock RepositoryMetadata metadata; @Mock QueryExtractor extractor; @Mock EntityManager em; @@ -89,7 +93,8 @@ void rejectsPersistenceProviderIfIncapableOfExtractingQueriesAndPagebleBeingUsed JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, projectionFactory, extractor); when(em.createNamedQuery(queryMethod.getNamedCountQueryName())).thenThrow(new IllegalArgumentException()); - assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR, QueryRewriter.IdentityQueryRewriter.INSTANCE)); + assertThatExceptionOfType(QueryCreationException.class) + .isThrownBy(() -> NamedQuery.lookupFrom(queryMethod, em, CONFIG)); } @Test // DATAJPA-142 @@ -101,7 +106,7 @@ void doesNotRejectPersistenceProviderIfNamedCountQueryIsAvailable() { TypedQuery countQuery = mock(TypedQuery.class); when(em.createNamedQuery(eq(queryMethod.getNamedCountQueryName()), eq(Long.class))).thenReturn(countQuery); - NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em, QueryEnhancerSelector.DEFAULT_SELECTOR, QueryRewriter.IdentityQueryRewriter.INSTANCE); + NamedQuery query = (NamedQuery) NamedQuery.lookupFrom(queryMethod, em, CONFIG); query.doCreateCountQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[1])); verify(em, times(1)).createNamedQuery(queryMethod.getNamedCountQueryName(), Long.class); diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index c66dc18642..13fddd3efb 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -376,6 +376,8 @@ Sometimes, no matter how many features you try to apply, it seems impossible to 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. +Query rewriting applies to the actual query and, when applicable, to count queries. +Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery`. .Declare a QueryRewriter using `@Query` ==== From ad76963cdfef99033bb879369f9579cfa98fb1f1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 09:45:03 +0200 Subject: [PATCH 52/94] Upgrade to Hibernate 7.0.0.Beta5. Closes #3836 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4334372575..f8fa3729a3 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.2 5.0.0-B05 5.0.0-SNAPSHOT - 7.0.0.Beta3 + 7.0.0.Beta5 7.0.0-SNAPSHOT 2.7.4

        2.3.232

        From 9f1dfa8857e68dbdf3e87bde0389f3cfae3ad475 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 09:47:15 +0200 Subject: [PATCH 53/94] Upgrade to Eclipselink 5.0.0-B07. Closes #3837 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8fa3729a3..32729e8b19 100755 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ 4.13.2 - 5.0.0-B05 + 5.0.0-B07 5.0.0-SNAPSHOT 7.0.0.Beta5 7.0.0-SNAPSHOT From 540f3fbaf12010529d73ba8f3b9a08153a7d06ad Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 23 Sep 2024 08:15:32 +0200 Subject: [PATCH 54/94] Add support for AOT repositories. Closes #3830 --- pom.xml | 5 + spring-data-envers/pom.xml | 6 + spring-data-jpa/pom.xml | 21 +- .../aot/generated/AotMetaModel.java | 124 ++++ .../aot/generated/AotQueryCreator.java | 62 ++ .../aot/generated/AotStringQuery.java | 106 +++ .../aot/generated/JpaCodeBlocks.java | 291 +++++++++ .../generated/JpaRepsoitoryContributor.java | 115 ++++ .../config/JpaRepositoryConfigExtension.java | 142 +++- .../jpa/repository/query/JpaQueryCreator.java | 12 +- .../repository/query/ParameterBinding.java | 4 +- .../query/ParameterBindingParser.java | 427 ++++++++++++ .../repository/query/PreprocessedQuery.java | 5 +- .../data/jpa/repository/query/QueryUtils.java | 2 +- .../support/JpaRepositoryFactoryBean.java | 5 +- .../java/com/example/UserDtoProjection.java | 39 ++ .../test/java/com/example/UserRepository.java | 140 ++++ .../JpaRepositoryContributorUnitTests.java | 614 ++++++++++++++++++ .../generated/StubRepositoryInformation.java | 126 ++++ .../generated/TestJpaAotRepsitoryContext.java | 108 +++ .../query/DefaultEntityQueryUnitTests.java | 1 + .../jpa/repository/sample/NameOnlyDto.java | 2 +- .../src/test/resources/logback.xml | 2 + 23 files changed, 2341 insertions(+), 18 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java create mode 100644 spring-data-jpa/src/test/java/com/example/UserDtoProjection.java create mode 100644 spring-data-jpa/src/test/java/com/example/UserRepository.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java diff --git a/pom.xml b/pom.xml index 32729e8b19..f6b6179301 100755 --- a/pom.xml +++ b/pom.xml @@ -145,6 +145,11 @@ ${spring} provided + + org.jboss.logging + jboss-logging + 3.6.1.Final + diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..43c08369f6 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -60,6 +60,12 @@ ${project.version} + + org.jboss.logging + jboss-logging + 3.6.1.Final + + org.hibernate.orm diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index b6470bdc89..12a089e3e4 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -88,12 +88,16 @@ true + + org.junit.platform + junit-platform-launcher + test + - org.junit.platform - junit-platform-launcher + org.springframework + spring-core-test test - org.hsqldb hsqldb @@ -239,6 +243,12 @@ true + + org.jboss.logging + jboss-logging + 3.6.1.Final + + @@ -370,6 +380,11 @@ jakarta.persistence-api ${jakarta-persistence-api} + + org.jboss.logging + jboss-logging + 3.6.1.Final + diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java new file mode 100644 index 0000000000..98929eead0 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java @@ -0,0 +1,124 @@ +/* + * Copyright 2024-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.aot.generated; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.metamodel.EmbeddableType; +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.Metamodel; +import jakarta.persistence.spi.ClassTransformer; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; +import org.springframework.data.util.Lazy; +import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; +import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; + +/** + * @author Christoph Strobl + */ +public class AotMetaModel implements Metamodel { + + private final String persistenceUnit; + private final Set> managedTypes; + private final Lazy entityManagerFactory = Lazy.of(this::init); + private final Lazy metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel()); + private final Lazy entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); + + public AotMetaModel(Set> managedTypes) { + this("dynamic-tests", managedTypes); + } + + private AotMetaModel(String persistenceUnit, Set> managedTypes) { + this.persistenceUnit = persistenceUnit; + this.managedTypes = managedTypes; + } + + public static AotMetaModel hibernateModel(Class... types) { + return new AotMetaModel(Set.of(types)); + } + + public static AotMetaModel hibernateModel(String persistenceUnit, Class... types) { + return new AotMetaModel(persistenceUnit, Set.of(types)); + } + + public EntityType entity(Class cls) { + return metamodel.get().entity(cls); + } + + @Override + public EntityType entity(String s) { + return metamodel.get().entity(s); + } + + public ManagedType managedType(Class cls) { + return metamodel.get().managedType(cls); + } + + public EmbeddableType embeddable(Class cls) { + return metamodel.get().embeddable(cls); + } + + public Set> getManagedTypes() { + return metamodel.get().getManagedTypes(); + } + + public Set> getEntities() { + return metamodel.get().getEntities(); + } + + public Set> getEmbeddables() { + return metamodel.get().getEmbeddables(); + } + + public EntityManager entityManager() { + return entityManager.get(); + } + + EntityManagerFactory init() { + + MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { + @Override + public ClassLoader getNewTempClassLoader() { + return new SimpleThrowawayClassLoader(this.getClass().getClassLoader()); + } + + @Override + public void addTransformer(ClassTransformer classTransformer) { + // just ingnore it + } + }; + + persistenceUnitInfo.setPersistenceUnitName(persistenceUnit); + this.managedTypes.stream().map(Class::getName).forEach(persistenceUnitInfo::addManagedClassName); + + persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName()); + + return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(persistenceUnitInfo) { + @Override + public List getManagedClassNames() { + return persistenceUnitInfo.getManagedClassNames(); + } + }, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build(); + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java new file mode 100644 index 0000000000..f6c22ec8fb --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java @@ -0,0 +1,62 @@ +/* + * 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 + * + * http://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.aot.generated; + +import jakarta.persistence.metamodel.Metamodel; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.query.JpaParameters; +import org.springframework.data.jpa.repository.query.JpaQueryCreator; +import org.springframework.data.jpa.repository.query.ParameterMetadataProvider; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class AotQueryCreator { + + Metamodel metamodel; + + public AotQueryCreator(Metamodel metamodel) { + this.metamodel = metamodel; + } + + AotStringQuery createQuery(PartTree partTree, ReturnedType returnedType, + AotRepositoryMethodGenerationContext context) { + + ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod()); + JpaParameters parameters = new JpaParameters(parametersSource); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, + JpqlQueryTemplates.UPPER); + + JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, + JpqlQueryTemplates.UPPER, metamodel); + AotStringQuery query = AotStringQuery.bindable(queryCreator.createQuery(), metadataProvider.getBindings()); + + if (partTree.isLimiting()) { + query.setLimit(partTree.getResultLimit()); + } + query.setCountQuery(context.annotationValue(Query.class, "countQuery")); + return query; + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java new file mode 100644 index 0000000000..147eb0a37c --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java @@ -0,0 +1,106 @@ +/* + * 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.aot.generated; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.domain.Limit; +import org.springframework.data.jpa.repository.query.ParameterBinding; +import org.springframework.data.jpa.repository.query.ParameterBindingParser; +import org.springframework.data.jpa.repository.query.ParameterBindingParser.Metadata; +import org.springframework.data.jpa.repository.query.QueryUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +class AotStringQuery { + + private final String raw; + private final String sanitized; + private @Nullable String countQuery; + private final List parameterBindings; + private final Metadata parameterMetadata; + private Limit limit; + private boolean nativeQuery; + + public AotStringQuery(String raw, String sanitized, List parameterBindings, + Metadata parameterMetadata) { + this.raw = raw; + this.sanitized = sanitized; + this.parameterBindings = parameterBindings; + this.parameterMetadata = parameterMetadata; + } + + static AotStringQuery of(String raw) { + + List bindings = new ArrayList<>(); + Metadata metadata = new Metadata(); + String targetQuery = ParameterBindingParser.INSTANCE + .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(raw, bindings, metadata); + + return new AotStringQuery(raw, targetQuery, bindings, metadata); + } + + static AotStringQuery nativeQuery(String raw) { + AotStringQuery q = of(raw); + q.nativeQuery = true; + return q; + } + + static AotStringQuery bindable(String query, List bindings) { + return new AotStringQuery(query, query, bindings, new Metadata()); + } + + public String getQueryString() { + return sanitized; + } + + public String getCountQuery(@Nullable String projection) { + + if (StringUtils.hasText(countQuery)) { + return countQuery; + } + return QueryUtils.createCountQueryFor(sanitized, StringUtils.hasText(projection) ? projection : null, nativeQuery); + } + + public List parameterBindings() { + return this.parameterBindings; + } + + boolean isLimited() { + return limit != null && limit.isLimited(); + } + + Limit getLimit() { + return limit; + } + + public void setLimit(Limit limit) { + this.limit = limit; + } + + public boolean isNativeQuery() { + return nativeQuery; + } + + public void setCountQuery(@Nullable String countQuery) { + this.countQuery = StringUtils.hasText(countQuery) ? countQuery : null; + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java new file mode 100644 index 0000000000..cf1489a786 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java @@ -0,0 +1,291 @@ +/* + * 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.aot.generated; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; + +import java.util.List; +import java.util.Optional; +import java.util.function.LongSupplier; +import java.util.regex.Pattern; + +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.ParameterBinding; +import org.springframework.data.jpa.repository.query.QueryEnhancer; +import org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation; +import org.springframework.data.jpa.repository.query.QueryEnhancerFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class JpaCodeBlocks { + + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + + static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryBlockBuilder(context); + } + + static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryExecutionBlockBuilder(context); + } + + static class QueryExecutionBlockBuilder { + + AotRepositoryMethodGenerationContext context; + private String queryVariableName; + + public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + QueryExecutionBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + + if (context.isDeleteMethod()) { + + builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); + builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class)); + if (context.returnsSingleValue()) { + if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { + builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType()); + } else { + builder.addStatement("return resultList.isEmpty() ? null : resultList.iterator().next()"); + } + } else { + builder.addStatement("return resultList"); + } + } else if (context.isExistsMethod()) { + builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); + } else { + + if (context.returnsSingleValue()) { + if (context.returnsOptionalValue()) { + builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, + actualReturnType, queryVariableName); + } else { + builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnType(), queryVariableName); + } + } else if (context.returnsPage()) { + builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", + PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, + context.getPageableParameterName()); + } else if (context.returnsSlice()) { + builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, + queryVariableName); + builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", + context.getPageableParameterName(), context.getPageableParameterName()); + builder.addStatement( + "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", + SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + } else { + builder.addStatement("return ($T) query.getResultList()", context.getReturnType()); + } + } + + return builder.build(); + + } + } + + static class QueryBlockBuilder { + + private final AotRepositoryMethodGenerationContext context; + private String queryVariableName; + private AotStringQuery query; + + public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + QueryBlockBuilder usingQueryVariableName(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + QueryBlockBuilder filter(String queryString) { + return filter(AotStringQuery.of(queryString)); + } + + QueryBlockBuilder filter(AotStringQuery query) { + this.query = query; + return this; + } + + CodeBlock build() { + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add("\n"); + String queryStringNameVariableName = "%sString".formatted(queryVariableName); + builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, query.getQueryString()); + + String countQueryStringNameVariableName = null; + String countQuyerVariableName = null; + if (context.returnsPage()) { + countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName)); + countQuyerVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName)); + String projection = context.annotationValue(org.springframework.data.jpa.repository.Query.class, + "countProjection"); + builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, + query.getCountQuery(projection)); + } + + // sorting + // TODO: refactor into sort builder + { + String sortParameterName = context.getSortParameterName(); + if (sortParameterName == null && context.getPageableParameterName() != null) { + sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); + } + + if (StringUtils.hasText(sortParameterName)) { + builder.beginControlFlow("if($L.isSorted())", sortParameterName); + + if(query.isNativeQuery()) { + builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class, + queryStringNameVariableName); + } else { + builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class, + queryStringNameVariableName); + } + + String enhancerVarName = "%sEnhancer".formatted(queryStringNameVariableName); + builder.addStatement("$T $L = $T.forQuery(declaredQuery).create(declaredQuery)", QueryEnhancer.class, enhancerVarName, QueryEnhancerFactory.class); + + builder.addStatement("$L = $L.rewrite(new $T() { public $T getSort() { return $L; } public $T getReturnedType() { return $T.of($T.class, $T.class, new $T());} })", queryStringNameVariableName, enhancerVarName, QueryRewriteInformation.class, + Sort.class, sortParameterName, ReturnedType.class, ReturnedType.class, + context.getRepositoryInformation().getDomainType(), actualReturnType, SpelAwareProxyProjectionFactory.class); + + builder.endControlFlow(); + } + } + + addQueryBlock(builder, queryVariableName, queryStringNameVariableName, query.isNativeQuery()); + + if (context.isExistsMethod()) { + builder.addStatement("$L.setMaxResults(1)", queryVariableName); + } else { + + { + String limitParameterName = context.getLimitParameterName(); + + if (StringUtils.hasText(limitParameterName)) { + builder.beginControlFlow("if($L.isLimited())", limitParameterName); + builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limitParameterName); + builder.endControlFlow(); + } else if (query.isLimited()) { + builder.addStatement("$L.setMaxResults($L)", queryVariableName, query.getLimit().max()); + } + } + + { + String pageableParamterName = context.getPageableParameterName(); + if (StringUtils.hasText(pageableParamterName)) { + builder.beginControlFlow("if($L.isPaged())", pageableParamterName); + builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName, + pageableParamterName); + if (context.returnsSlice() && !context.returnsPage()) { + builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageableParamterName); + } else { + builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageableParamterName); + } + builder.endControlFlow(); + } + } + } + + if (StringUtils.hasText(countQueryStringNameVariableName)) { + builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); + addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, query.isNativeQuery()); + builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName); + + // end control flow does not work well with lambdas + builder.unindent(); + builder.add("};\n"); + } + + return builder.build(); + } + + private void addQueryBlock(Builder builder, String queryVariableName, String queryStringNameVariableName, + boolean nativeQuery) { + + builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), nativeQuery ? "createNativeQuery" : "createQuery", + queryStringNameVariableName); + + for (ParameterBinding binding : query.parameterBindings()) { + + Object prepare = binding.prepare("s"); + if (prepare instanceof String prepared && !prepared.equals("s")) { + String format = prepared.replaceAll("%", "%%").replace("s", "%s"); + if (binding.getIdentifier().hasPosition()) { + builder.addStatement("$L.setParameter($L, $S.formatted($L))", queryVariableName, + binding.getIdentifier().getPosition(), format, + context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1)); + } else { + builder.addStatement("$L.setParameter($S, $S.formatted($L))", queryVariableName, + binding.getIdentifier().getName(), format, binding.getIdentifier().getName()); + } + } else { + if (binding.getIdentifier().hasPosition()) { + builder.addStatement("$L.setParameter($L, $L)", queryVariableName, binding.getIdentifier().getPosition(), + context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1)); + } else { + builder.addStatement("$L.setParameter($S, $L)", queryVariableName, binding.getIdentifier().getName(), + binding.getIdentifier().getName()); + } + } + } + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java new file mode 100644 index 0000000000..57660bccc1 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java @@ -0,0 +1,115 @@ +/* + * Copyright 2024 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.aot.generated; + +import jakarta.persistence.EntityManager; + +import java.util.regex.Pattern; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.RepositoryContributor; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public class JpaRepsoitoryContributor extends RepositoryContributor { + + AotQueryCreator queryCreator; + AotMetaModel metaModel; + + public JpaRepsoitoryContributor(AotRepositoryContext repositoryContext) { + super(repositoryContext); + + metaModel = new AotMetaModel(repositoryContext.getResolvedTypes()); + this.queryCreator = new AotQueryCreator(metaModel); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + constructorBuilder.addParameter("entityManager", TypeName.get(EntityManager.class)); + } + + @Override + protected AotRepositoryMethodBuilder contributeRepositoryMethod( + AotRepositoryMethodGenerationContext generationContext) { + + { + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); + if (queryAnnotation != null) { + if (StringUtils.hasText(queryAnnotation.value()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { + return null; + } + } + } + + return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { + + Query query = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); + if (query != null && StringUtils.hasText(query.value())) { + + AotStringQuery aotStringQuery = query.nativeQuery() ? AotStringQuery.nativeQuery(query.value()) + : AotStringQuery.of(query.value()); + aotStringQuery.setCountQuery(query.countQuery()); + body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + + body.addCode( + + JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(aotStringQuery).build()); + } else { + + PartTree partTree = new PartTree(context.getMethod().getName(), + context.getRepositoryInformation().getDomainType()); + + CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + + Class actualReturnType = context.getRepositoryInformation().getDomainType(); + try { + actualReturnType = isProjecting + ? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader()) + : context.getRepositoryInformation().getDomainType(); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + ReturnedType returnedType = ReturnedType.of(actualReturnType, + context.getRepositoryInformation().getDomainType(), projectionFactory); + AotStringQuery stringQuery = queryCreator.createQuery(partTree, returnedType, context); + + body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + body.addCode( + JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(stringQuery).build()); + } + body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).referencing("query").build()); + }); + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 7abdd4758e..44127c452d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -15,7 +15,9 @@ */ package org.springframework.data.jpa.repository.config; -import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*; +import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.EM_BEAN_DEFINITION_REGISTRAR_POST_PROCESSOR_BEAN_NAME; +import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_CONTEXT_BEAN_NAME; +import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_MAPPING_CONTEXT_BEAN_NAME; import jakarta.persistence.Entity; import jakarta.persistence.MappedSuperclass; @@ -41,23 +43,34 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; import org.springframework.dao.DataAccessException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.data.aot.AotContext; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.aot.generated.JpaRepsoitoryContributor; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor; import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; +import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.ImplementationDetectionConfiguration; +import org.springframework.data.repository.config.ImplementationLookupConfiguration; +import org.springframework.data.repository.config.RepositoryConfiguration; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; +import org.springframework.data.util.Streamable; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -193,7 +206,6 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf contextDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); return contextDefinition; - }, registry, JPA_CONTEXT_BEAN_NAME, source); registerIfNotAlreadyRegistered(() -> new RootBeanDefinition(JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME), registry, @@ -211,7 +223,6 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf builder.addConstructorArgValue(value); return builder.getBeanDefinition(); - }, registry, JpaEvaluationContextExtension.class.getName(), source); } @@ -316,8 +327,131 @@ static boolean isActive(@Nullable ClassLoader classLoader) { */ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor { - protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + // don't register domain types nor annotations. + + if (!AotContext.aotGeneratedRepositoriesEnabled()) { + return null; + } + + return new JpaRepsoitoryContributor(repositoryContext); + } + + @Nullable + @Override + protected RepositoryConfiguration getRepositoryMetadata(RegisteredBean bean) { + RepositoryConfiguration configuration = super.getRepositoryMetadata(bean); + if (!configuration.getRepositoryBaseClassName().isEmpty()) { + return configuration; + } + return new Meh<>(configuration); + } + } + + /** + * I'm just a dirty hack so we can refine the {@link #getRepositoryBaseClassName()} method as we cannot instantiate + * the bean safely to extract it form the repository factory in data commons. So we either have a configurable + * {@link RepositoryConfiguration} return from + * {@link RepositoryRegistrationAotProcessor#getRepositoryMetadata(RegisteredBean)} or change the arrangement and + * maybe move the type out of the factoy. + * + * @param + */ + static class Meh implements RepositoryConfiguration { + + private RepositoryConfiguration configuration; + + public Meh(RepositoryConfiguration configuration) { + this.configuration = configuration; + } + + @Nullable + @Override + public Object getSource() { + return configuration.getSource(); + } + + @Override + public T getConfigurationSource() { + return (T) configuration.getConfigurationSource(); + } + + @Override + public boolean isLazyInit() { + return configuration.isLazyInit(); + } + + @Override + public boolean isPrimary() { + return configuration.isPrimary(); + } + + @Override + public Streamable getBasePackages() { + return configuration.getBasePackages(); + } + + @Override + public Streamable getImplementationBasePackages() { + return configuration.getImplementationBasePackages(); + } + + @Override + public String getRepositoryInterface() { + return configuration.getRepositoryInterface(); + } + + @Override + public Optional getQueryLookupStrategyKey() { + return Optional.ofNullable(configuration.getQueryLookupStrategyKey()); + } + + @Override + public Optional getNamedQueriesLocation() { + return configuration.getNamedQueriesLocation(); + } + + @Override + public Optional getRepositoryBaseClassName() { + String name = SimpleJpaRepository.class.getName(); + return Optional.of(name); + } + + @Override + public String getRepositoryFactoryBeanClassName() { + return configuration.getRepositoryFactoryBeanClassName(); + } + + @Override + public String getImplementationBeanName() { + return configuration.getImplementationBeanName(); + } + + @Override + public String getRepositoryBeanName() { + return configuration.getRepositoryBeanName(); + } + + @Override + public Streamable getExcludeFilters() { + return configuration.getExcludeFilters(); + } + + @Override + public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { + return configuration.toImplementationDetectionConfiguration(factory); + } + + @Override + public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) { + return configuration.toLookupConfiguration(factory); + } + + @Nullable + @Override + public String getResourceDescription() { + return configuration.getResourceDescription(); } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 9a828a9b3f..3eec07e417 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -63,7 +63,7 @@ * @author Christoph Strobl * @author Jinmyeong Kim */ -class JpaQueryCreator extends AbstractQueryCreator implements JpqlQueryCreator { +public class JpaQueryCreator extends AbstractQueryCreator implements JpqlQueryCreator { private final ReturnedType returnedType; private final ParameterMetadataProvider provider; @@ -86,15 +86,21 @@ class JpaQueryCreator extends AbstractQueryCreator getFrom() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index dda3211cd9..8b40751cd6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -46,7 +46,7 @@ * @author Mark Paluch * @author Christoph Strobl */ -class ParameterBinding { +public class ParameterBinding { private final BindingIdentifier identifier; private final ParameterOrigin origin; @@ -462,7 +462,7 @@ static Type getLikeTypeFrom(String expression) { * @author Mark Paluch * @since 3.1.2 */ - sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed { + public sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed { /** * Creates an identifier for the given {@code name}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java new file mode 100644 index 0000000000..371016577c --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java @@ -0,0 +1,427 @@ +/* + * 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 static java.util.regex.Pattern.CASE_INSENSITIVE; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A parser that extracts the parameter bindings from a given query string. + * + * @author Thomas Darimont + */ +public enum ParameterBindingParser { + + INSTANCE; + + private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; + public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; + // .....................................................................^ not followed by a hash or a letter. + // .................................................................^ zero or more digits. + // .............................................................^ start with a question mark. + private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); + private static final Pattern PARAMETER_BINDING_PATTERN; + private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit] + private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit] + 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"; + private static final int INDEXED_PARAMETER_GROUP = 4; + private static final int NAMED_PARAMETER_GROUP = 6; + private static final int COMPARISION_TYPE_GROUP = 1; + + public static class Metadata { + private boolean usesJdbcStyleParameters = false; + + public boolean usesJdbcStyleParameters() { + return usesJdbcStyleParameters; + } + } + + /** + * 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}. + * + * @author Mark Paluch + * @since 3.1.2 + */ + static class ParameterBindings { + + private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); + + private final Consumer registration; + private int syntheticParameterIndex; + + public ParameterBindings(List bindings, Consumer registration, + int syntheticParameterIndex) { + + for (ParameterBinding binding : bindings) { + this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding))); + } + + this.registration = registration; + this.syntheticParameterIndex = syntheticParameterIndex; + } + + /** + * Return whether the identifier is already bound. + * + * @param identifier + * @return + */ + public boolean isBound(BindingIdentifier identifier) { + return !getBindings(identifier).isEmpty(); + } + + BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, + Function bindingFactory) { + + Assert.isInstanceOf(MethodInvocationArgument.class, origin); + + BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier(); + List bindingsForOrigin = getBindings(methodArgument); + + if (!isBound(identifier)) { + + ParameterBinding binding = bindingFactory.apply(identifier); + registration.accept(binding); + bindingsForOrigin.add(binding); + return binding.getIdentifier(); + } + + ParameterBinding binding = bindingFactory.apply(identifier); + + for (ParameterBinding existing : bindingsForOrigin) { + + if (existing.isCompatibleWith(binding)) { + return existing.getIdentifier(); + } + } + + BindingIdentifier syntheticIdentifier; + if (identifier.hasName() && methodArgument.hasName()) { + + int index = 0; + String newName = methodArgument.getName(); + while (existsBoundParameter(newName)) { + index++; + newName = methodArgument.getName() + "_" + index; + } + syntheticIdentifier = BindingIdentifier.of(newName); + } else { + syntheticIdentifier = BindingIdentifier.of(++syntheticParameterIndex); + } + + ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); + registration.accept(newBinding); + bindingsForOrigin.add(newBinding); + return newBinding.getIdentifier(); + } + + private boolean existsBoundParameter(String key) { + return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream) + .anyMatch(it -> key.equals(it.getName())); + } + + private List getBindings(BindingIdentifier identifier) { + return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); + } + + public void register(ParameterBinding parameterBinding) { + registration.accept(parameterBinding); + } + } + + static { + + List keywords = new ArrayList<>(); + + for (ParameterBindingType type : ParameterBindingType.values()) { + if (type.getKeyword() != null) { + keywords.add(type.getKeyword()); + } + } + + StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords + builder.append(")?"); + builder.append("(?: )?"); // some whitespace + builder.append("\\(?"); // optional braces around parameters + builder.append("("); + builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index + builder.append("|"); // or + + // named parameter and the parameter name + builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?"); + + builder.append(")"); + builder.append("\\)?"); // optional braces around parameters + + PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); + } + + /** + * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns + * the cleaned up query. + */ + public String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings, + Metadata queryMeta) { + + int greatestParameterIndex = tryFindGreatestParameterIndexIn(query); + boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1; + + /* + * Prefer indexed access over named parameters if only SpEL Expression parameters are present. + */ + if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { + parametersShouldBeAccessedByIndex = true; + greatestParameterIndex = 0; + } + + ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, + parametersShouldBeAccessedByIndex, + greatestParameterIndex); + + String resultingQuery = parsedQuery.getQueryString(); + Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); + + int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; + int syntheticParameterIndex = expressionParameterIndex + parsedQuery.size(); + + ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings), + syntheticParameterIndex); + int currentIndex = 0; + + boolean usesJpaStyleParameters = false; + + while (matcher.find()) { + + if (parsedQuery.isQuoted(matcher.start())) { + continue; + } + + String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP); + String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP); + Integer parameterIndex = getParameterIndex(parameterIndexString); + + String match = matcher.group(0); + if (JDBC_STYLE_PARAM.matcher(match).find()) { + queryMeta.usesJdbcStyleParameters = true; + } + + if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { + usesJpaStyleParameters = true; + } + + if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) { + throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); + } + + String typeSource = matcher.group(COMPARISION_TYPE_GROUP); + Assert.isTrue(parameterIndexString != null || parameterName != null, + () -> String.format("We need either a name or an index; Offending query string: %s", query)); + ValueExpression expression = parsedQuery + .getParameter(parameterName == null ? parameterIndexString : parameterName); + String replacement = null; + + expressionParameterIndex++; + if ("".equals(parameterIndexString)) { + parameterIndex = expressionParameterIndex; + } + + BindingIdentifier queryParameter; + if (parameterIndex != null) { + queryParameter = BindingIdentifier.of(parameterIndex); + } else { + queryParameter = BindingIdentifier.of(parameterName); + } + ParameterOrigin origin = ObjectUtils.isEmpty(expression) + ? ParameterOrigin.ofParameter(parameterName, parameterIndex) + : ParameterOrigin.ofExpression(expression); + + BindingIdentifier targetBinding = queryParameter; + Function bindingFactory = switch (ParameterBindingType.of(typeSource)) { + case LIKE -> { + + Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + yield (identifier) -> new 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. + default -> (identifier) -> new ParameterBinding(identifier, origin); + }; + + if (origin.isExpression()) { + parameterBindings.register(bindingFactory.apply(queryParameter)); + } else { + targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory); + } + + replacement = targetBinding.hasName() ? ":" + targetBinding.getName() + : ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?" + : "?" + targetBinding.getPosition()); + String result; + String substring = matcher.group(2); + + int index = resultingQuery.indexOf(substring, currentIndex); + if (index < 0) { + result = resultingQuery; + } else { + currentIndex = index + replacement.length(); + result = resultingQuery.substring(0, index) + replacement + + resultingQuery.substring(index + substring.length()); + } + + resultingQuery = result; + } + + return resultingQuery; + } + + private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, + boolean parametersShouldBeAccessedByIndex, + int greatestParameterIndex) { + + /* + * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to + * not mix-up with the actual parameter indices. + */ + int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; + + BiFunction indexToParameterName = parametersShouldBeAccessedByIndex + ? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1) + : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1); + + String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; + + BiFunction parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; + ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(), + indexToParameterName, parameterNameToReplacement); + + return rewriter.parse(queryWithSpel); + } + + @Nullable + private static Integer getParameterIndex(@Nullable String parameterIndexString) { + + if (parameterIndexString == null || parameterIndexString.isEmpty()) { + return null; + } + return Integer.valueOf(parameterIndexString); + } + + private static int tryFindGreatestParameterIndexIn(String query) { + + Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); + + int greatestParameterIndex = -1; + while (parameterIndexMatcher.find()) { + + String parameterIndexString = parameterIndexMatcher.group(1); + Integer parameterIndex = getParameterIndex(parameterIndexString); + if (parameterIndex != null) { + greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex); + } + } + + return greatestParameterIndex; + } + + private static void checkAndRegister(ParameterBinding binding, List bindings) { + + bindings.stream() // + .filter(it -> it.bindsTo(binding)) // + .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); + + if (!bindings.contains(binding)) { + bindings.add(binding); + } + } + + /** + * An enum for the different types of bindings. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private enum ParameterBindingType { + + // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace + // character, while = does not. + LIKE("like "), IN("in "), AS_IS(null); + + private final @Nullable String keyword; + + ParameterBindingType(@Nullable String keyword) { + this.keyword = keyword; + } + + /** + * Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a + * keyword. + * + * @return the keyword + */ + @Nullable + public String getKeyword() { + return keyword; + } + + /** + * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in + * case no other {@link ParameterBindingType} could be found. + */ + static ParameterBindingType of(String typeSource) { + + if (!StringUtils.hasText(typeSource)) { + return AS_IS; + } + + for (ParameterBindingType type : values()) { + if (type.name().equalsIgnoreCase(typeSource.trim())) { + return type; + } + } + + throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s", typeSource)); + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java index 0c5061b529..c9171b2038 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static java.util.regex.Pattern.*; +import static java.util.regex.Pattern.CASE_INSENSITIVE; import java.util.ArrayList; import java.util.Collection; @@ -34,6 +34,7 @@ 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.repository.query.ValueExpressionQueryRewriter; import org.springframework.data.repository.query.parser.Part; import org.springframework.util.Assert; @@ -463,7 +464,7 @@ static ParameterBindingType of(String typeSource) { */ private static class ParameterBindings { - private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); + private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); private final Consumer registration; 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 b16d2ef5dd..749853f36f 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 @@ -578,7 +578,7 @@ static String createCountQueryFor(String originalQuery, @Nullable String countPr * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. * @since 2.7.8 */ - static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) { + public static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) { Assert.hasText(originalQuery, "OriginalQuery must not be null or empty"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index ebb24268d1..8e8200a371 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -105,11 +105,12 @@ public void setEntityPathResolver(ObjectProvider resolver) { * fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is * available. * - * @param factory may be {@literal null}. + * @param resolver may be {@literal null}. */ @Autowired - public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) { + public void setQueryMethodFactory(ObjectProvider resolver) { // TODO: nullable insteand of ObjectProvider + JpaQueryMethodFactory factory = resolver.getIfAvailable(); if (factory != null) { this.queryMethodFactory = factory; } diff --git a/spring-data-jpa/src/test/java/com/example/UserDtoProjection.java b/spring-data-jpa/src/test/java/com/example/UserDtoProjection.java new file mode 100644 index 0000000000..2605f553f2 --- /dev/null +++ b/spring-data-jpa/src/test/java/com/example/UserDtoProjection.java @@ -0,0 +1,39 @@ +/* + * 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 com.example; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class UserDtoProjection { + + private final String firstname; + private final String emailAddress; + + public UserDtoProjection(String firstname, String emailAddress) { + this.firstname = firstname; + this.emailAddress = emailAddress; + } + + public String getFirstname() { + return firstname; + } + + public String getEmailAddress() { + return emailAddress; + } +} diff --git a/spring-data-jpa/src/test/java/com/example/UserRepository.java b/spring-data-jpa/src/test/java/com/example/UserRepository.java new file mode 100644 index 0000000000..8c3e9135e9 --- /dev/null +++ b/spring-data-jpa/src/test/java/com/example/UserRepository.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 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 com.example; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Limit; +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.sample.User; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +/** + * @author Christoph Strobl + */ +public interface UserRepository extends CrudRepository { + + List findUserNoArgumentsBy(); + + User findOneByEmailAddress(String emailAddress); + + Optional findOptionalOneByEmailAddress(String emailAddress); + + Long countUsersByLastname(String lastname); + + Boolean existsUserByLastname(String lastname); + + List findByLastnameStartingWith(String lastname); + + List findTop2ByLastnameStartingWith(String lastname); + + List findByLastnameStartingWithOrderByEmailAddress(String lastname); + + List findByLastnameStartingWith(String lastname, Limit limit); + + List findByLastnameStartingWith(String lastname, Sort sort); + + List findByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + List findByLastnameStartingWith(String lastname, Pageable page); + + Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); + + Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); + + /* Annotated Queries */ + + @Query("select u from User u where u.emailAddress = ?1") + User findAnnotatedQueryByEmailAddress(String username); + + @Query("select u from User u where u.lastname like ?1%") + List findAnnotatedQueryByLastname(String lastname); + + @Query("select u from User u where u.lastname like :lastname%") + List findAnnotatedQueryByLastnameParamter(String lastname); + + @Query(""" + select u + from User u + where u.lastname LIKE ?1%""") + List findAnnotatedMultilineQueryByLastname(String username); + + @Query("select u from User u where u.lastname like ?1%") + List findAnnotatedQueryByLastname(String lastname, Limit limit); + + @Query("select u from User u where u.lastname like ?1%") + List findAnnotatedQueryByLastname(String lastname, Sort sort); + + @Query("select u from User u where u.lastname like ?1%") + List findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort); + + @Query("select u from User u where u.lastname like ?1%") + List findAnnotatedQueryByLastname(String lastname, Pageable pageable); + + @Query("select u from User u where u.lastname like ?1%") + Page findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable); + + @Query("select u from User u where u.lastname like ?1%") + Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + + // modifying + + User deleteByEmailAddress(String username); + + Long deleteReturningDeleteCountByEmailAddress(String username); + + @Modifying + @Query("delete from User u where u.emailAddress = ?1") + User deleteAnnotatedQueryByEmailAddress(String username); + + // native queries + + @Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User", + nativeQuery = true) + Page findByNativeQueryWithPageable(Pageable pageable); + + // projections + + + + List findUserProjectionByLastnameStartingWith(String lastname); + + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + + // old ones + + @Query("select u from User u where u.firstname = ?1") + List findAllUsingAnnotatedJpqlQuery(String firstname); + + List findByLastname(String lastname); + + List findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit); + + List findByLastname(String lastname, Sort sort); + + List findByLastname(String lastname, Pageable page); + + List findByLastnameOrderByFirstname(String lastname); + + User findByEmailAddress(String emailAddress); +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java new file mode 100644 index 0000000000..b6471ea1a9 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java @@ -0,0 +1,614 @@ +/* + * Copyright 2024 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.aot.generated; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.persistence.EntityManager; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.util.Lazy; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import com.example.UserDtoProjection; +import com.example.UserRepository; + +/** + * @author Christoph Strobl + */ +class JpaRepositoryContributorUnitTests { + + private static Verifyer generated; + + @BeforeAll + static void beforeAll() { + + TestJpaAotRepsitoryContext aotContext = new TestJpaAotRepsitoryContext(UserRepository.class, null); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + + new JpaRepsoitoryContributor(aotContext).contribute(generationContext); + + AbstractBeanDefinition emBeanDefinition = BeanDefinitionBuilder + .rootBeanDefinition("org.springframework.orm.jpa.SharedEntityManagerCreator") + .setFactoryMethod("createSharedEntityManager").addConstructorArgReference("entityManagerFactory") + .setLazyInit(true).getBeanDefinition(); + + AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder + .genericBeanDefinition("com.example.UserRepositoryImpl__Aot") + .addConstructorArgReference("jpaSharedEM_entityManagerFactory").getBeanDefinition(); + + + /* + alter the RepositoryFactory so we can write generated calsses into a supplier and then write some custom code for instantiation + on JpaRepositoryFactoryBean + + beanDefinition.getPropertyValues().addPropertyValue("aotImplementation", new Function() { + + public Instance apply(BeanFactory beanFactor) { + EntityManager em = beanFactory.getBean(EntityManger.class); + return new com.example.UserRepositoryImpl__Aot(em); + } + }); + */ + + // register a dedicated factory that can read stuff + // don't write to spring.factories or uas another name for it + // maybe write the code directly to a repo fragment + // repo does not have to be a bean, but can be a method called by some component + // pass list to entiy manager to have stuff in memory have to list written out directly when creating the bean + + generated = generateContext(generationContext) // + .registerBeansFrom(new ClassPathResource("infrastructure.xml")) + .register("jpaSharedEM_entityManagerFactory", emBeanDefinition) + .register("aotUserRepository", aotGeneratedRepository); + } + + @BeforeEach + public void beforeEach() { + + generated.doWithBean(EntityManager.class, em -> { + + em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate(); + + User luke = new User("Luke", "Skywalker", "luke@jedi.org"); + em.persist(luke); + + User leia = new User("Leia", "Organa", "leia@resistance.gov"); + em.persist(leia); + + User han = new User("Han", "Solo", "han@smuggler.net"); + em.persist(han); + + User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net"); + em.persist(chewbacca); + + User yoda = new User("Yoda", "n/a", "yoda@jedi.org"); + em.persist(yoda); + + User vader = new User("Anakin", "Skywalker", "vader@empire.com"); + em.persist(vader); + + User kylo = new User("Ben", "Solo", "kylo@new-empire.com"); + em.persist(kylo); + }); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + generated.verify(methodInvoker -> { + + User user = methodInvoker.invoke("findByEmailAddress", "luke@jedi.org").onBean("aotUserRepository"); + assertThat(user.getLastname()).isEqualTo("Skywalker"); + }); + } + + @Test + void testFindDerivedFinderOptionalEntity() { + + generated.verify(methodInvoker -> { + + Optional user = methodInvoker.invoke("findOptionalOneByEmailAddress", "yoda@jedi.org") + .onBean("aotUserRepository"); + assertThat(user).isNotNull().containsInstanceOf(User.class) + .hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda")); + }); + } + + @Test + void testDerivedCount() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @Test + void testDerivedExists() { + + generated.verify(methodInvoker -> { + + Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(exists).isTrue(); + }); + } + + @Test + void testDerivedFinderWithoutArguments() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository"); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); + }); + } + + @Test + void testDerivedFinderReturningList() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com", + "kylo@new-empire.com", "han@smuggler.net"); + }); + } + + @Test + void testLimitedDerivedFinder() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testSortedDerivedFinder() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWithOrderByEmailAddress", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", + "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testDerivedFinderWithLimitArgument() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testDerivedFinderWithSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", + "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testDerivedFinderWithSortAndLimit() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress"), Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + }); + } + + @Test + void testDerivedFinderReturningListWithPageable() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker + .invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + }); + } + + @Test + void testDerivedFinderReturningPage() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + }); + } + + @Test + void testDerivedFinderReturningSlice() { + + generated.verify(methodInvoker -> { + + Slice slice = methodInvoker + .invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + }); + } + + @Test + void testAnnotatedFinderReturningSingleValueWithQuery() { + + generated.verify(methodInvoker -> { + + User user = methodInvoker.invoke("findAnnotatedQueryByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository"); + assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda"); + }); + } + + @Test + void testAnnotatedFinderReturningListWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastnameParamter", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testAnnotatedMultilineFinderWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testAnnotatedFinderWithQueryAndLimit() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testAnnotatedFinderWithQueryAndSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("emailAddress")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", + "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testAnnotatedFinderWithQueryLimitAndSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("emailAddress")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + }); + } + + @Test + void testAnnotatedFinderReturningListWithPageable() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker + .invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + }); + } + + @Test + void testAnnotatedFinderReturningPage() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + }); + } + + @Test + void testAnnotatedFinderReturningSlice() { + + generated.verify(methodInvoker -> { + + Slice slice = methodInvoker + .invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + }); + } + + @Test + void testDerivedFinderReturningListOfProjections() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(UserDtoProjection::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + }); + } + + @Test + void testDerivedFinderReturningPageOfProjections() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) + .onBean("aotUserRepository"); + + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + }); + } + + // modifying + + @Test + void testDerivedDeleteSingle() { + + generated.verifyInTx(methodInvoker -> { + + User result = methodInvoker.invoke("deleteByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository"); + + assertThat(result).isNotNull().extracting(User::getEmailAddress).isEqualTo("yoda@jedi.org"); + }).doWithBean(EntityManager.class, em -> { + Object yodaShouldBeGone = em + .createQuery("SELECT u FROM %s u WHERE u.emailAddress = 'yoda@jedi.org'".formatted(User.class.getName())) + .getSingleResultOrNull(); + assertThat(yodaShouldBeGone).isNull(); + }); + } + + // native queries + + @Test + void nativeQuery() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findByNativeQueryWithPageable", PageRequest.of(0, 2)) + .onBean("aotUserRepository"); + + assertThat(page.getTotalElements()).isEqualTo(7); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).containsExactly("Anakin", "Ben"); + }); + } + + // old stuff below + + // TODO: + void todo() { + + // Query q; + // q.setMaxResults() + // q.setFirstResult() + + // 1 build some more stuff from below + // 2 set up boot sample project in data samples + + // query hints + // first and max result for pagination + // entity graphs + // native queries + // delete + // @Modifying + // flush / clear + } + + static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) { + return new GeneratedContextBuilder(generationContext); + } + + static class GeneratedContextBuilder implements Verifyer { + + TestGenerationContext generationContext; + Map beanDefinitions = new LinkedHashMap<>(); + Resource xmlBeanDefinitions; + Lazy lazyFactory; + + public GeneratedContextBuilder(TestGenerationContext generationContext) { + + this.generationContext = generationContext; + this.lazyFactory = Lazy.of(() -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + + freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); + if (xmlBeanDefinitions != null) { + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(freshBeanFactory); + beanDefinitionReader.loadBeanDefinitions(xmlBeanDefinitions); + } + + for (Entry entry : beanDefinitions.entrySet()) { + freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue()); + } + }); + return freshBeanFactory; + }); + } + + GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) { + this.beanDefinitions.put(name, beanDefinition); + return this; + } + + GeneratedContextBuilder registerBeansFrom(Resource xmlBeanDefinitions) { + this.xmlBeanDefinitions = xmlBeanDefinitions; + return this; + } + + public Verifyer verify(Consumer methodInvoker) { + methodInvoker.accept(new GeneratedContext(lazyFactory)); + return this; + } + + } + + interface Verifyer { + Verifyer verify(Consumer methodInvoker); + + default Verifyer verifyInTx(Consumer methodInvoker) { + + verify(ctx -> { + + PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class); + new TransactionTemplate(txMgr).execute(action -> { + verify(methodInvoker); + return "ok"; + }); + }); + + return this; + } + + default void doWithBean(Class type, Consumer runit) { + verify(ctx -> { + + boolean isEntityManager = type == EntityManager.class; + T bean = ctx.delegate.get().getBean(type); + + if (!isEntityManager) { + runit.accept(bean); + } else { + + PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class); + new TransactionTemplate(txMgr).execute(action -> { + runit.accept(bean); + return "ok"; + }); + + } + }); + } + } + + static class GeneratedContext { + + private Supplier delegate; + + public GeneratedContext(Supplier defaultListableBeanFactory) { + this.delegate = defaultListableBeanFactory; + } + + InvocationBuilder invoke(String method, Object... arguments) { + + return new InvocationBuilder() { + @Override + public T onBean(String beanName) { + DefaultListableBeanFactory defaultListableBeanFactory = delegate.get(); + + Object bean = defaultListableBeanFactory.getBean(beanName); + return ReflectionTestUtils.invokeMethod(bean, method, arguments); + } + }; + } + + interface InvocationBuilder { + T onBean(String beanName); + } + + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java new file mode 100644 index 0000000000..ad1273b8c5 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024 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.aot.generated; + +import java.lang.reflect.Method; +import java.util.Set; + +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class StubRepositoryInformation implements RepositoryInformation { + + private final RepositoryMetadata metadata; + private final RepositoryComposition baseComposition; + + public StubRepositoryInformation(Class repositoryInterface, @Nullable RepositoryComposition composition) { + + this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); + this.baseComposition = composition != null ? composition + : RepositoryComposition.of(RepositoryFragment.structural(SimpleJpaRepository.class)); + } + + @Override + public TypeInformation getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } + + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return false; + } + + @Override + public Set> getAlternativeDomainTypes() { + return null; + } + + @Override + public boolean isReactiveRepository() { + return false; + } + + @Override + public Set> getFragments() { + return null; + } + + @Override + public boolean isBaseClassMethod(Method method) { + return baseComposition.findMethod(method).isPresent(); + } + + @Override + public boolean isCustomMethod(Method method) { + return false; + } + + @Override + public boolean isQueryMethod(Method method) { + return false; + } + + @Override + public Streamable getQueryMethods() { + return null; + } + + @Override + public Class getRepositoryBaseClass() { + return SimpleJpaRepository.class; + } + + @Override + public Method getTargetClassMethod(Method method) { + return null; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java new file mode 100644 index 0000000000..433a6e602d --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 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.aot.generated; + +import jakarta.persistence.Entity; +import jakarta.persistence.MappedSuperclass; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.ClassFile; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class TestJpaAotRepsitoryContext implements AotRepositoryContext { + + private final StubRepositoryInformation repositoryInformation; + + public TestJpaAotRepsitoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return null; + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return null; + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return null; + } + + @Override + public String getBeanName() { + return "dummyRepository"; + } + + @Override + public Set getBasePackages() { + return Set.of("org.springframework.data.dummy.repository.aot"); + } + + @Override + public Set> getIdentifyingAnnotations() { + return Set.of(Entity.class, MappedSuperclass.class); + } + + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + @Override + public Set> getResolvedAnnotations() { + return Set.of(); + } + + @Override + public Set> getResolvedTypes() { + return Set.of(User.class, Role.class); + } + + public List getRequiredContextFiles() { + return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); + } + + static ClassFile classFileForType(Class type) { + + String name = type.getName(); + ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); + + try { + return ClassFile.of(name, cpr.getContentAsByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java index 874ff77c99..57ba338b0c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java @@ -28,6 +28,7 @@ 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.ParameterBindingParser.Metadata; import org.springframework.data.repository.query.parser.Part.Type; /** diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/NameOnlyDto.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/NameOnlyDto.java index 13ee35f497..b64c4de2f4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/NameOnlyDto.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/NameOnlyDto.java @@ -16,7 +16,7 @@ package org.springframework.data.jpa.repository.sample; // DATAJPA-1334 -class NameOnlyDto { +public class NameOnlyDto { private String firstname; private String lastname; diff --git a/spring-data-jpa/src/test/resources/logback.xml b/spring-data-jpa/src/test/resources/logback.xml index 19bb933f9c..780ba5e8fd 100644 --- a/spring-data-jpa/src/test/resources/logback.xml +++ b/spring-data-jpa/src/test/resources/logback.xml @@ -19,6 +19,8 @@ + + From 5561fd181018ba946543f77d6f9412ed0836fdc2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 24 Mar 2025 15:52:23 +0100 Subject: [PATCH 55/94] Refactoring. Introduce AotRepositoryFragmentSupport, adopt to FragmentCreationContext. Reduce visibility. Refactor CodeBlocks builder. Simplify query rewriting and use base class methods. Use typed verifier through a JDK proxy to avoid reflective frontend. Revise testing to a plain old Spring test but testing the AOT fragment through its interface by forwarding reflective calls to the AOT fragment. Refactor AotQuery into AotQueries to support a wider range of possible queries. See #3830 --- .../aot/generated/AotMetaModel.java | 4 +- .../repository/aot/generated/AotQueries.java | 58 ++ .../repository/aot/generated/AotQuery.java | 61 ++ .../aot/generated/AotQueryCreator.java | 30 +- .../AotRepositoryFragmentSupport.java | 118 ++++ .../aot/generated/AotStringQuery.java | 106 --- .../aot/generated/JpaCodeBlocks.java | 161 ++--- .../generated/JpaRepositoryContributor.java | 206 ++++++ .../generated/JpaRepsoitoryContributor.java | 115 ---- .../aot/generated/StringAotQuery.java | 131 ++++ .../config/JpaRepositoryConfigExtension.java | 8 +- .../query/JpaCountQueryCreator.java | 19 + .../repository/query/PreprocessedQuery.java | 6 +- .../AotFragmentTestConfigurationSupport.java | 140 ++++ ...RepositoryContributorIntegrationTests.java | 356 ++++++++++ .../JpaRepositoryContributorUnitTests.java | 614 ------------------ .../generated/StubRepositoryInformation.java | 3 +- ....java => TestJpaAotRepositoryContext.java} | 10 +- .../aot/generated}/UserDtoProjection.java | 2 +- .../aot/generated}/UserRepository.java | 15 +- 20 files changed, 1198 insertions(+), 965 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/{TestJpaAotRepsitoryContext.java => TestJpaAotRepositoryContext.java} (89%) rename spring-data-jpa/src/test/java/{com/example => org/springframework/data/jpa/repository/aot/generated}/UserDtoProjection.java (94%) rename spring-data-jpa/src/test/java/{com/example => org/springframework/data/jpa/repository/aot/generated}/UserRepository.java (92%) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java index 98929eead0..797e7a45a4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java @@ -37,7 +37,7 @@ /** * @author Christoph Strobl */ -public class AotMetaModel implements Metamodel { +class AotMetaModel implements Metamodel { private final String persistenceUnit; private final Set> managedTypes; @@ -105,7 +105,7 @@ public ClassLoader getNewTempClassLoader() { @Override public void addTransformer(ClassTransformer classTransformer) { - // just ingnore it + // just ignore it } }; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java new file mode 100644 index 0000000000..c7d8051bd1 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java @@ -0,0 +1,58 @@ +/* + * 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.aot.generated; + +import jakarta.validation.constraints.Null; + +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.QueryEnhancer; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.util.StringUtils; + +/** + * Value object capturing queries used for repository query methods. + * + * @author Mark Paluch + * @since 4.0 + */ +record AotQueries(AotQuery result, AotQuery count) { + + /** + * Derive a count query from the given query. + */ + public static AotQueries from(StringAotQuery query, @Null String countProjection, QueryEnhancerSelector selector) { + + QueryEnhancer queryEnhancer = selector.select(query.getQuery()).create(query.getQuery()); + + String derivedCountQuery = queryEnhancer + .createCountQueryFor(StringUtils.hasText(countProjection) ? countProjection : null); + + DeclaredQuery countQuery = query.getQuery().rewrite(derivedCountQuery); + return new AotQueries(query, StringAotQuery.of(countQuery)); + } + + /** + * Create new {@code AotQueries} for the given queries. + */ + public static AotQueries from(AotQuery result, AotQuery count) { + return new AotQueries(result, count); + } + + public boolean isNative() { + return result().isNative(); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java new file mode 100644 index 0000000000..2f48b43887 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java @@ -0,0 +1,61 @@ +/* + * 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.aot.generated; + +import java.util.List; + +import org.springframework.data.domain.Limit; +import org.springframework.data.jpa.repository.query.ParameterBinding; + +/** + * AOT query value object along with its parameter bindings. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 4.0 + */ +abstract class AotQuery { + + private final List parameterBindings; + + AotQuery(List parameterBindings) { + this.parameterBindings = parameterBindings; + } + + /** + * @return whether the query is a {@link jakarta.persistence.EntityManager#createNativeQuery native} one. + */ + public abstract boolean isNative(); + + public List getParameterBindings() { + return parameterBindings; + } + + /** + * @return the preliminary query limit. + */ + public Limit getLimit() { + return Limit.unlimited(); + } + + /** + * @return whether the query is limited (e.g. {@code findTop10By}). + */ + public boolean isLimited() { + return getLimit().isLimited(); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java index f6c22ec8fb..98254aff9b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java @@ -17,22 +17,11 @@ import jakarta.persistence.metamodel.Metamodel; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.query.EscapeCharacter; -import org.springframework.data.jpa.repository.query.JpaParameters; -import org.springframework.data.jpa.repository.query.JpaQueryCreator; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider; -import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; -import org.springframework.data.repository.query.ParametersSource; -import org.springframework.data.repository.query.ReturnedType; -import org.springframework.data.repository.query.parser.PartTree; - /** * @author Christoph Strobl * @since 2025/01 */ -public class AotQueryCreator { +class AotQueryCreator { Metamodel metamodel; @@ -40,23 +29,6 @@ public AotQueryCreator(Metamodel metamodel) { this.metamodel = metamodel; } - AotStringQuery createQuery(PartTree partTree, ReturnedType returnedType, - AotRepositoryMethodGenerationContext context) { - - ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod()); - JpaParameters parameters = new JpaParameters(parametersSource); - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - JpqlQueryTemplates.UPPER); - JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, - JpqlQueryTemplates.UPPER, metamodel); - AotStringQuery query = AotStringQuery.bindable(queryCreator.createQuery(), metadataProvider.getBindings()); - - if (partTree.isLimiting()) { - query.setLimit(partTree.getResultLimit()); - } - query.setCountQuery(context.annotationValue(Query.class, "countQuery")); - return query; - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java new file mode 100644 index 0000000000..dd1deeec2b --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java @@ -0,0 +1,118 @@ +/* + * 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.aot.generated; + +import java.lang.reflect.Method; + +import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; +import org.springframework.data.expression.ValueEvaluationContextProvider; +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.JpaParameters; +import org.springframework.data.jpa.repository.query.QueryEnhancer; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.ConcurrentLruCache; + +/** + * @author Mark Paluch + */ +public class AotRepositoryFragmentSupport { + + private final RepositoryMetadata repositoryMetadata; + + private final ValueExpressionDelegate valueExpressions; + + private final ProjectionFactory projectionFactory; + + private final ConcurrentLruCache enhancers; + + private final ConcurrentLruCache expressions; + + private final ConcurrentLruCache contextProviders; + + protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector, + RepositoryFactoryBeanSupport.FragmentCreationContext context) { + this(selector, context.getRepositoryMetadata(), context.getValueExpressionDelegate(), + context.getProjectionFactory()); + } + + protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector, RepositoryMetadata repositoryMetadata, + ValueExpressionDelegate valueExpressions, ProjectionFactory projectionFactory) { + + this.repositoryMetadata = repositoryMetadata; + this.valueExpressions = valueExpressions; + this.projectionFactory = projectionFactory; + this.enhancers = new ConcurrentLruCache<>(32, query -> selector.select(query).create(query)); + this.expressions = new ConcurrentLruCache<>(32, valueExpressions::parse); + this.contextProviders = new ConcurrentLruCache<>(32, it -> valueExpressions + .createValueContextProvider(new JpaParameters(ParametersSource.of(repositoryMetadata, it)))); + } + + /** + * Rewrite a {@link DeclaredQuery} to apply {@link Sort} and {@link Class} projection. + * + * @param query + * @param sort + * @param returnedType + * @return + */ + protected String rewriteQuery(DeclaredQuery query, Sort sort, Class returnedType) { + + QueryEnhancer queryStringEnhancer = this.enhancers.get(query); + return queryStringEnhancer.rewrite(new DefaultQueryRewriteInformation(sort, + ReturnedType.of(returnedType, repositoryMetadata.getDomainType(), projectionFactory))); + } + + /** + * Evaluate a Value Expression. + * + * @param method + * @param expressionString + * @param args + * @return + */ + protected @Nullable Object evaluateExpression(Method method, String expressionString, Object... args) { + + ValueExpression expression = this.expressions.get(expressionString); + ValueEvaluationContextProvider contextProvider = this.contextProviders.get(method); + + return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies())); + } + + private record DefaultQueryRewriteInformation(Sort sort, + ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation { + + @Override + public Sort getSort() { + return sort(); + } + + @Override + public ReturnedType getReturnedType() { + return returnedType(); + } + + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java deleted file mode 100644 index 147eb0a37c..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java +++ /dev/null @@ -1,106 +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.aot.generated; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.data.domain.Limit; -import org.springframework.data.jpa.repository.query.ParameterBinding; -import org.springframework.data.jpa.repository.query.ParameterBindingParser; -import org.springframework.data.jpa.repository.query.ParameterBindingParser.Metadata; -import org.springframework.data.jpa.repository.query.QueryUtils; -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -class AotStringQuery { - - private final String raw; - private final String sanitized; - private @Nullable String countQuery; - private final List parameterBindings; - private final Metadata parameterMetadata; - private Limit limit; - private boolean nativeQuery; - - public AotStringQuery(String raw, String sanitized, List parameterBindings, - Metadata parameterMetadata) { - this.raw = raw; - this.sanitized = sanitized; - this.parameterBindings = parameterBindings; - this.parameterMetadata = parameterMetadata; - } - - static AotStringQuery of(String raw) { - - List bindings = new ArrayList<>(); - Metadata metadata = new Metadata(); - String targetQuery = ParameterBindingParser.INSTANCE - .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(raw, bindings, metadata); - - return new AotStringQuery(raw, targetQuery, bindings, metadata); - } - - static AotStringQuery nativeQuery(String raw) { - AotStringQuery q = of(raw); - q.nativeQuery = true; - return q; - } - - static AotStringQuery bindable(String query, List bindings) { - return new AotStringQuery(query, query, bindings, new Metadata()); - } - - public String getQueryString() { - return sanitized; - } - - public String getCountQuery(@Nullable String projection) { - - if (StringUtils.hasText(countQuery)) { - return countQuery; - } - return QueryUtils.createCountQueryFor(sanitized, StringUtils.hasText(projection) ? projection : null, nativeQuery); - } - - public List parameterBindings() { - return this.parameterBindings; - } - - boolean isLimited() { - return limit != null && limit.isLimited(); - } - - Limit getLimit() { - return limit; - } - - public void setLimit(Limit limit) { - this.limit = limit; - } - - public boolean isNativeQuery() { - return nativeQuery; - } - - public void setCountQuery(@Nullable String countQuery) { - this.countQuery = StringUtils.hasText(countQuery) ? countQuery : null; - } -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java index cf1489a786..7bfdb07173 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java @@ -24,15 +24,9 @@ import java.util.regex.Pattern; import org.springframework.data.domain.SliceImpl; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.ParameterBinding; -import org.springframework.data.jpa.repository.query.QueryEnhancer; -import org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation; -import org.springframework.data.jpa.repository.query.QueryEnhancerFactory; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; -import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; @@ -60,7 +54,7 @@ static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethod static class QueryExecutionBlockBuilder { AotRepositoryMethodGenerationContext context; - private String queryVariableName; + private String queryVariableName = "query"; public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; @@ -130,11 +124,14 @@ CodeBlock build() { } } + /** + * Builder for the actual query code block. + */ static class QueryBlockBuilder { private final AotRepositoryMethodGenerationContext context; - private String queryVariableName; - private AotStringQuery query; + private String queryVariableName = "query"; + private AotQueries queries; public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; @@ -146,126 +143,130 @@ QueryBlockBuilder usingQueryVariableName(String queryVariableName) { return this; } - QueryBlockBuilder filter(String queryString) { - return filter(AotStringQuery.of(queryString)); - } - - QueryBlockBuilder filter(AotStringQuery query) { - this.query = query; + QueryBlockBuilder filter(AotQueries query) { + this.queries = query; return this; } CodeBlock build() { boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); Object actualReturnType = isProjecting ? context.getActualReturnType() - : context.getRepositoryInformation().getDomainType(); + : context.getRepositoryInformation().getDomainType(); CodeBlock.Builder builder = CodeBlock.builder(); builder.add("\n"); String queryStringNameVariableName = "%sString".formatted(queryVariableName); + + StringAotQuery query = (StringAotQuery) queries.result(); builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, query.getQueryString()); String countQueryStringNameVariableName = null; String countQuyerVariableName = null; + if (context.returnsPage()) { + countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName)); countQuyerVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName)); - String projection = context.annotationValue(org.springframework.data.jpa.repository.Query.class, - "countProjection"); + + StringAotQuery countQuery = (StringAotQuery) queries.count(); builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, - query.getCountQuery(projection)); + countQuery.getQueryString()); } // sorting // TODO: refactor into sort builder - { - String sortParameterName = context.getSortParameterName(); - if (sortParameterName == null && context.getPageableParameterName() != null) { - sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); - } - if (StringUtils.hasText(sortParameterName)) { - builder.beginControlFlow("if($L.isSorted())", sortParameterName); + String sortParameterName = context.getSortParameterName(); + if (sortParameterName == null && context.getPageableParameterName() != null) { + sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); + } - if(query.isNativeQuery()) { - builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class, - queryStringNameVariableName); - } else { - builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class, - queryStringNameVariableName); - } + if (StringUtils.hasText(sortParameterName)) { + applySorting(builder, sortParameterName, queryStringNameVariableName, actualReturnType); + } - String enhancerVarName = "%sEnhancer".formatted(queryStringNameVariableName); - builder.addStatement("$T $L = $T.forQuery(declaredQuery).create(declaredQuery)", QueryEnhancer.class, enhancerVarName, QueryEnhancerFactory.class); + addQueryBlock(builder, queryVariableName, queryStringNameVariableName, queries.result()); - builder.addStatement("$L = $L.rewrite(new $T() { public $T getSort() { return $L; } public $T getReturnedType() { return $T.of($T.class, $T.class, new $T());} })", queryStringNameVariableName, enhancerVarName, QueryRewriteInformation.class, - Sort.class, sortParameterName, ReturnedType.class, ReturnedType.class, - context.getRepositoryInformation().getDomainType(), actualReturnType, SpelAwareProxyProjectionFactory.class); + applyLimits(builder); - builder.endControlFlow(); - } + if (StringUtils.hasText(countQueryStringNameVariableName)) { + + builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); + addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, queries.count()); + builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName); + + // end control flow does not work well with lambdas + builder.unindent(); + builder.add("};\n"); } - addQueryBlock(builder, queryVariableName, queryStringNameVariableName, query.isNativeQuery()); + return builder.build(); + } - if (context.isExistsMethod()) { - builder.addStatement("$L.setMaxResults(1)", queryVariableName); + private void applySorting(Builder builder, String sort, String queryString, Object actualReturnType) { + + builder.beginControlFlow("if ($L.isSorted())", sort); + + if (queries.isNative()) { + builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class, + queryString); } else { + builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class, + queryString); + } - { - String limitParameterName = context.getLimitParameterName(); + builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); - if (StringUtils.hasText(limitParameterName)) { - builder.beginControlFlow("if($L.isLimited())", limitParameterName); - builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limitParameterName); - builder.endControlFlow(); - } else if (query.isLimited()) { - builder.addStatement("$L.setMaxResults($L)", queryVariableName, query.getLimit().max()); - } - } + builder.endControlFlow(); + } - { - String pageableParamterName = context.getPageableParameterName(); - if (StringUtils.hasText(pageableParamterName)) { - builder.beginControlFlow("if($L.isPaged())", pageableParamterName); - builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName, - pageableParamterName); - if (context.returnsSlice() && !context.returnsPage()) { - builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageableParamterName); - } else { - builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageableParamterName); - } - builder.endControlFlow(); - } - } + private void applyLimits(Builder builder) { + + if (context.isExistsMethod()) { + builder.addStatement("$L.setMaxResults(1)", queryVariableName); + + return; } - if (StringUtils.hasText(countQueryStringNameVariableName)) { - builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); - addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, query.isNativeQuery()); - builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName); + String limit = context.getLimitParameterName(); - // end control flow does not work well with lambdas - builder.unindent(); - builder.add("};\n"); + if (StringUtils.hasText(limit)) { + builder.beginControlFlow("if ($L.isLimited())", limit); + builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limit); + builder.endControlFlow(); + } else if (queries.result().isLimited()) { + builder.addStatement("$L.setMaxResults($L)", queryVariableName, queries.result().getLimit().max()); } - return builder.build(); + String pageable = context.getPageableParameterName(); + + if (StringUtils.hasText(pageable)) { + + builder.beginControlFlow("if ($L.isPaged())", pageable); + builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName, pageable); + if (context.returnsSlice() && !context.returnsPage()) { + builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageable); + } else { + builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageable); + } + builder.endControlFlow(); + } } private void addQueryBlock(Builder builder, String queryVariableName, String queryStringNameVariableName, - boolean nativeQuery) { + AotQuery query) { builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), nativeQuery ? "createNativeQuery" : "createQuery", + context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery", queryStringNameVariableName); - for (ParameterBinding binding : query.parameterBindings()) { + for (ParameterBinding binding : query.getParameterBindings()) { Object prepare = binding.prepare("s"); + if (prepare instanceof String prepared && !prepared.equals("s")) { String format = prepared.replaceAll("%", "%%").replace("s", "%s"); if (binding.getIdentifier().hasPosition()) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java new file mode 100644 index 0000000000..2d4a92bacd --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java @@ -0,0 +1,206 @@ +/* + * Copyright 2024 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.aot.generated; + +import jakarta.persistence.EntityManager; + +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; +import org.springframework.data.jpa.repository.NativeQuery; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.jpa.repository.query.JpaCountQueryCreator; +import org.springframework.data.jpa.repository.query.JpaParameters; +import org.springframework.data.jpa.repository.query.JpaQueryCreator; +import org.springframework.data.jpa.repository.query.ParameterMetadataProvider; +import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryImplementationMetadata; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.RepositoryContributor; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.javapoet.TypeName; +import org.springframework.javapoet.TypeSpec; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @author Mark Paluch + */ +public class JpaRepositoryContributor extends RepositoryContributor { + + private final CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory(); + private final AotQueryCreator queryCreator; + private final AotMetaModel metaModel; + + public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { + super(repositoryContext); + + this.metaModel = new AotMetaModel(repositoryContext.getResolvedTypes()); + this.queryCreator = new AotQueryCreator(metaModel); + } + + @Override + protected void customizeFile(RepositoryInformation information, AotRepositoryImplementationMetadata metadata, + TypeSpec.Builder builder) { + builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class)); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + + constructorBuilder.addParameter("entityManager", EntityManager.class); + constructorBuilder.addParameter("context", RepositoryFactoryBeanSupport.FragmentCreationContext.class); + + // TODO: Pick up the configured QueryEnhancerSelector + constructorBuilder.customize((repositoryInformation, builder) -> { + builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class); + }); + } + + @Override + protected AotRepositoryMethodBuilder contributeRepositoryMethod( + AotRepositoryMethodGenerationContext generationContext) { + + QueryEnhancerSelector selector = QueryEnhancerSelector.DEFAULT_SELECTOR; + + // no stored procedures for now. + if (AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Procedure.class) != null) { + return null; + } + + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); + if (queryAnnotation != null) { + if (StringUtils.hasText(queryAnnotation.value()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { + return null; + } + } + + // TODO: Named query via EntityManager, NamedQuery via properties, also for count queries. + + return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { + + MergedAnnotations annotations = MergedAnnotations.from(context.getMethod()); + + MergedAnnotation query = annotations.get(Query.class); + MergedAnnotation nativeQuery = annotations.get(NativeQuery.class); + MergedAnnotation queryHints = annotations.get(QueryHints.class); + + body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + + AotQueries aotQueries; + if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { + aotQueries = buildStringQuery(selector, query); + } else { + aotQueries = buildPartTreeQuery(context, query); + } + + body.addCode(JpaCodeBlocks.queryBlockBuilder(context).filter(aotQueries).build()); + body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).build()); + }); + } + + private AotQueries buildStringQuery(QueryEnhancerSelector selector, MergedAnnotation query) { + + Function queryFunction = query.getBoolean("nativeQuery") ? StringAotQuery::nativeQuery + : StringAotQuery::jpqlQuery; + + StringAotQuery aotStringQuery = queryFunction.apply(query.getString("value")); + String countQuery = query.getString("countQuery"); + + if (StringUtils.hasText(countQuery)) { + return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery)); + } + + String countProjection = query.getString("countProjection"); + return AotQueries.from(aotStringQuery, countProjection, selector); + } + + private AotQueries buildPartTreeQuery(AotRepositoryMethodGenerationContext context, MergedAnnotation query) { + + PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); + // TODO make configurable + JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + + Class actualReturnType; + try { + actualReturnType = isProjecting + ? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader()) + : context.getRepositoryInformation().getDomainType(); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + ReturnedType returnedType = ReturnedType.of(actualReturnType, context.getRepositoryInformation().getDomainType(), + projectionFactory); + + ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod()); + JpaParameters parameters = new JpaParameters(parametersSource); + + AotQuery partTreeQuery = createQuery(partTree, returnedType, parameters, templates); + + if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { + return AotQueries.from(partTreeQuery, StringAotQuery.jpqlQuery(query.getString("countQuery"))); + } + + AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, parameters, templates); + return AotQueries.from(partTreeQuery, partTreeCountQuery); + } + + private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, + JpqlQueryTemplates templates) { + + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, + templates); + JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metaModel); + + return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), + partTree.getResultLimit()); + } + + private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, + JpqlQueryTemplates templates) { + + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, + templates); + JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, + metaModel); + + return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), null); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java deleted file mode 100644 index 57660bccc1..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2024 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.aot.generated; - -import jakarta.persistence.EntityManager; - -import java.util.regex.Pattern; - -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; -import org.springframework.data.repository.aot.generate.RepositoryContributor; -import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.query.ReturnedType; -import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.javapoet.TypeName; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * @author Christoph Strobl - */ -public class JpaRepsoitoryContributor extends RepositoryContributor { - - AotQueryCreator queryCreator; - AotMetaModel metaModel; - - public JpaRepsoitoryContributor(AotRepositoryContext repositoryContext) { - super(repositoryContext); - - metaModel = new AotMetaModel(repositoryContext.getResolvedTypes()); - this.queryCreator = new AotQueryCreator(metaModel); - } - - @Override - protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { - constructorBuilder.addParameter("entityManager", TypeName.get(EntityManager.class)); - } - - @Override - protected AotRepositoryMethodBuilder contributeRepositoryMethod( - AotRepositoryMethodGenerationContext generationContext) { - - { - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); - if (queryAnnotation != null) { - if (StringUtils.hasText(queryAnnotation.value()) - && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { - return null; - } - } - } - - return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { - - Query query = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); - if (query != null && StringUtils.hasText(query.value())) { - - AotStringQuery aotStringQuery = query.nativeQuery() ? AotStringQuery.nativeQuery(query.value()) - : AotStringQuery.of(query.value()); - aotStringQuery.setCountQuery(query.countQuery()); - body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - - body.addCode( - - JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(aotStringQuery).build()); - } else { - - PartTree partTree = new PartTree(context.getMethod().getName(), - context.getRepositoryInformation().getDomainType()); - - CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory(); - - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); - - Class actualReturnType = context.getRepositoryInformation().getDomainType(); - try { - actualReturnType = isProjecting - ? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader()) - : context.getRepositoryInformation().getDomainType(); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - - ReturnedType returnedType = ReturnedType.of(actualReturnType, - context.getRepositoryInformation().getDomainType(), projectionFactory); - AotStringQuery stringQuery = queryCreator.createQuery(partTree, returnedType, context); - - body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - body.addCode( - JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(stringQuery).build()); - } - body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).referencing("query").build()); - }); - } -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java new file mode 100644 index 0000000000..c9a0d318f2 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java @@ -0,0 +1,131 @@ +/* + * 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.aot.generated; + +import java.util.List; + +import org.springframework.data.domain.Limit; +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.ParameterBinding; +import org.springframework.data.jpa.repository.query.PreprocessedQuery; + +/** + * An AOT query represented by a string. + * + * @author Mark Paluch + * @since 4.0 + */ +abstract class StringAotQuery extends AotQuery { + + private StringAotQuery(List parameterBindings) { + super(parameterBindings); + } + + static StringAotQuery of(DeclaredQuery query) { + + if (query instanceof PreprocessedQuery pq) { + return new DeclaredAotQuery(pq); + } + + return new DeclaredAotQuery(PreprocessedQuery.parse(query)); + } + + static StringAotQuery jpqlQuery(String queryString) { + return of(DeclaredQuery.jpqlQuery(queryString)); + } + + public static StringAotQuery jpqlQuery(String queryString, List bindings, Limit resultLimit) { + return new LimitedAotQuery(queryString, bindings, resultLimit); + } + + static StringAotQuery nativeQuery(String queryString) { + return of(DeclaredQuery.nativeQuery(queryString)); + } + + public abstract DeclaredQuery getQuery(); + + public abstract String getQueryString(); + + @Override + public String toString() { + return getQueryString(); + } + + /** + * @author Christoph Strobl + * @author Mark Paluch + */ + static class DeclaredAotQuery extends StringAotQuery { + + private final PreprocessedQuery query; + + DeclaredAotQuery(PreprocessedQuery query) { + super(query.getBindings()); + this.query = query; + } + + @Override + public String getQueryString() { + return query.getQueryString(); + } + + @Override + public boolean isNative() { + return query.isNative(); + } + + public PreprocessedQuery getQuery() { + return query; + } + + } + + /** + * @author Mark Paluch + */ + static class LimitedAotQuery extends StringAotQuery { + + private final String queryString; + private final Limit limit; + + LimitedAotQuery(String queryString, List parameterBindings, Limit limit) { + super(parameterBindings); + this.queryString = queryString; + this.limit = limit; + } + + @Override + public DeclaredQuery getQuery() { + return DeclaredQuery.jpqlQuery(queryString); + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public boolean isNative() { + return false; + } + + @Override + public Limit getLimit() { + return limit; + } + + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 44127c452d..742387add6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -15,9 +15,7 @@ */ package org.springframework.data.jpa.repository.config; -import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.EM_BEAN_DEFINITION_REGISTRAR_POST_PROCESSOR_BEAN_NAME; -import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_CONTEXT_BEAN_NAME; -import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_MAPPING_CONTEXT_BEAN_NAME; +import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*; import jakarta.persistence.Entity; import jakarta.persistence.MappedSuperclass; @@ -54,7 +52,7 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.data.aot.AotContext; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.aot.generated.JpaRepsoitoryContributor; +import org.springframework.data.jpa.repository.aot.generated.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor; import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; @@ -335,7 +333,7 @@ protected RepositoryContributor contribute(AotRepositoryContext repositoryContex return null; } - return new JpaRepsoitoryContributor(repositoryContext); + return new JpaRepositoryContributor(repositoryContext); } @Nullable diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java index 886cb5b4dd..c0f5c49d73 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.query; import jakarta.persistence.EntityManager; +import jakarta.persistence.metamodel.Metamodel; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; @@ -53,6 +54,24 @@ public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterM this.returnedType = returnedType; } + /** + * Creates a new {@link JpaCountQueryCreator} + * + * @param tree + * @param returnedType + * @param provider + * @param templates + * @param metamodel + */ + public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider, + JpqlQueryTemplates templates, Metamodel metamodel) { + + super(tree, returnedType, provider, templates, metamodel); + + this.distinct = tree.isDistinct(); + this.returnedType = returnedType; + } + @Override protected JpqlQueryBuilder.Select buildQuery(Sort sort) { JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(returnedType.getDomainType()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java index c9171b2038..6f36ac80a3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.*; import java.util.ArrayList; import java.util.Collection; @@ -60,7 +60,7 @@ * @author Mark Paluch * @since 4.0 */ -final class PreprocessedQuery implements DeclaredQuery { +public final class PreprocessedQuery implements DeclaredQuery { private final DeclaredQuery source; private final List bindings; @@ -127,7 +127,7 @@ boolean usesJdbcStyleParameters() { return usesJdbcStyleParameters; } - List getBindings() { + public List getBindings() { return Collections.unmodifiableList(bindings); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java new file mode 100644 index 0000000000..15e2606118 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java @@ -0,0 +1,140 @@ +/* + * 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.aot.generated; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.orm.jpa.SharedEntityManagerCreator; +import org.springframework.util.ReflectionUtils; + +/** + * Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface. + *

        + * This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT + * fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method + * invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy. + * + * @author Mark Paluch + */ +@ImportResource("classpath:/infrastructure.xml") +class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { + + private final Class repositoryInterface; + private final TestJpaAotRepositoryContext repositoryContext; + + public AotFragmentTestConfigurationSupport(Class repositoryInterface) { + this.repositoryInterface = repositoryInterface; + this.repositoryContext = new TestJpaAotRepositoryContext<>(UserRepository.class, null); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + + new JpaRepositoryContributor(repositoryContext).contribute(generationContext); + + AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder + .genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") + .addConstructorArgReference("jpaSharedEM_entityManagerFactory") + .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); + + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + beanFactory.setBeanClassLoader(compiled.getClassLoader()); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository); + }); + + BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> { + + Object fragment = beanFactory.getBean("fragment"); + Object proxy = getFragmentFacadeProxy(fragment); + + return repositoryInterface.cast(proxy); + }).getBeanDefinition(); + + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade); + } + + private Object getFragmentFacadeProxy(Object fragment) { + + return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class[] { repositoryInterface }, + (p, method, args) -> { + + Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes()); + + if (target == null) { + throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target)); + } + + try { + return target.invoke(fragment, args); + } catch (ReflectiveOperationException e) { + ReflectionUtils.handleReflectionException(e); + } + + return null; + }); + } + + @Bean("jpaSharedEM_entityManagerFactory") + EntityManager sharedEntityManagerCreator(EntityManagerFactory emf) { + return SharedEntityManagerCreator.createSharedEntityManager(emf); + } + + private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( + TestJpaAotRepositoryContext repositoryContext) { + + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() { + @Override + public RepositoryMetadata getRepositoryMetadata() { + return repositoryContext.getRepositoryInformation(); + } + + @Override + public ValueExpressionDelegate getValueExpressionDelegate() { + return ValueExpressionDelegate.create(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return new SpelAwareProxyProjectionFactory(); + } + }; + + return creationContext; + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java new file mode 100644 index 0000000000..582b476277 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java @@ -0,0 +1,356 @@ +/* + * Copyright 2024 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.aot.generated; + +import static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.EntityManager; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link UserRepository} AOT fragment. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +@SpringJUnitConfig(classes = JpaRepositoryContributorIntegrationTests.JpaRepositoryContributorConfiguration.class) +@Transactional +class JpaRepositoryContributorIntegrationTests { + + @Autowired UserRepository fragment; + @Autowired EntityManager em; + + @Configuration + static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + public JpaRepositoryContributorConfiguration() { + super(UserRepository.class); + } + } + + @BeforeEach + void beforeEach() { + + em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate(); + + User luke = new User("Luke", "Skywalker", "luke@jedi.org"); + em.persist(luke); + + User leia = new User("Leia", "Organa", "leia@resistance.gov"); + em.persist(leia); + + User han = new User("Han", "Solo", "han@smuggler.net"); + em.persist(han); + + User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net"); + em.persist(chewbacca); + + User yoda = new User("Yoda", "n/a", "yoda@jedi.org"); + em.persist(yoda); + + User vader = new User("Anakin", "Skywalker", "vader@empire.com"); + em.persist(vader); + + User kylo = new User("Ben", "Solo", "kylo@new-empire.com"); + em.persist(kylo); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + User user = fragment.findByEmailAddress("luke@jedi.org"); + assertThat(user.getLastname()).isEqualTo("Skywalker"); + } + + @Test + void testFindDerivedFinderOptionalEntity() { + + Optional user = fragment.findOptionalOneByEmailAddress("yoda@jedi.org"); + assertThat(user).isNotNull().containsInstanceOf(User.class) + .hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda")); + } + + @Test + void testDerivedCount() { + + Long value = fragment.countUsersByLastname("Skywalker"); + assertThat(value).isEqualTo(2L); + } + + @Test + void testDerivedExists() { + + Boolean exists = fragment.existsUserByLastname("Skywalker"); + assertThat(exists).isTrue(); + } + + @Test + void testDerivedFinderWithoutArguments() { + + List users = fragment.findUserNoArgumentsBy(); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); + } + + @Test + void testDerivedFinderReturningList() { + + List users = fragment.findByLastnameStartingWith("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com", + "kylo@new-empire.com", "han@smuggler.net"); + } + + @Test + void testLimitedDerivedFinder() { + + List users = fragment.findTop2ByLastnameStartingWith("S"); + assertThat(users).hasSize(2); + } + + @Test + void testSortedDerivedFinder() { + + List users = fragment.findByLastnameStartingWithOrderByEmailAddress("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", + "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testDerivedFinderWithLimitArgument() { + + List users = fragment.findByLastnameStartingWith("S", Limit.of(2)); + assertThat(users).hasSize(2); + } + + @Test + void testDerivedFinderWithSort() { + + List users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress")); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", + "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testDerivedFinderWithSortAndLimit() { + + List users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress"), Limit.of(2)); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + } + + @Test + void testDerivedFinderReturningListWithPageable() { + + List users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("emailAddress"))); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + } + + @Test + void testDerivedFinderReturningPage() { + + Page page = fragment.findPageOfUsersByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + } + + @Test + void testDerivedFinderReturningSlice() { + + Slice slice = fragment.findSliceOfUserByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + } + + @Test + void testAnnotatedFinderReturningSingleValueWithQuery() { + + User user = fragment.findAnnotatedQueryByEmailAddress("yoda@jedi.org"); + assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda"); + } + + @Test + void testAnnotatedFinderReturningListWithQuery() { + + List users = fragment.findAnnotatedQueryByLastname("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() { + + List users = fragment.findAnnotatedQueryByLastnameParameter("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testAnnotatedMultilineFinderWithQuery() { + + List users = fragment.findAnnotatedMultilineQueryByLastname("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testAnnotatedFinderWithQueryAndLimit() { + + List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2)); + assertThat(users).hasSize(2); + } + + @Test + void testAnnotatedFinderWithQueryAndSort() { + + List users = fragment.findAnnotatedQueryByLastname("S", Sort.by("emailAddress")); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", + "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testAnnotatedFinderWithQueryLimitAndSort() { + + List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("emailAddress")); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + } + + @Test + void testAnnotatedFinderReturningListWithPageable() { + + List users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("emailAddress"))); + assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + } + + @Test + void testAnnotatedFinderReturningPage() { + + Page page = fragment.findAnnotatedQueryPageOfUsersByLastname("S", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + } + + @Test + void testPagingAnnotatedQueryWithSort() { + + Page page = fragment.findAnnotatedQueryPageWithStaticSort("S", PageRequest.of(0, 2, Sort.unsorted())); + + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("luke@jedi.org", + "vader@empire.com"); + } + + @Test + void testAnnotatedFinderReturningSlice() { + + Slice slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + } + + @Test + void testDerivedFinderReturningListOfProjections() { + + List users = fragment.findUserProjectionByLastnameStartingWith("S"); + assertThat(users).extracting(UserDtoProjection::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + } + + @Test + void testDerivedFinderReturningPageOfProjections() { + + // TODO: query.setParameter(1, "%s%%".formatted(lastname)); + Page page = fragment.findUserProjectionByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + } + + // modifying + + @Test + void testDerivedDeleteSingle() { + + User result = fragment.deleteByEmailAddress("yoda@jedi.org"); + + assertThat(result).isNotNull().extracting(User::getEmailAddress).isEqualTo("yoda@jedi.org"); + + Object yodaShouldBeGone = em + .createQuery("SELECT u FROM %s u WHERE u.emailAddress = 'yoda@jedi.org'".formatted(User.class.getName())) + .getSingleResultOrNull(); + assertThat(yodaShouldBeGone).isNull(); + } + + // native queries + + @Test + void nativeQuery() { + + Page page = fragment.findByNativeQueryWithPageable(PageRequest.of(0, 2)); + + assertThat(page.getTotalElements()).isEqualTo(7); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).containsExactly("Anakin", "Ben"); + } + + // old stuff below + + // TODO: + void todo() { + + // interface projections + // named queries + + // query hints + // entity graphs + // native queries + // delete + // @Modifying + // flush / clear + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java deleted file mode 100644 index b6471ea1a9..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright 2024 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.aot.generated; - -import static org.assertj.core.api.Assertions.assertThat; - -import jakarta.persistence.EntityManager; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.aot.test.generate.TestGenerationContext; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.core.test.tools.TestCompiler; -import org.springframework.data.domain.Limit; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.sample.User; -import org.springframework.data.util.Lazy; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; - -import com.example.UserDtoProjection; -import com.example.UserRepository; - -/** - * @author Christoph Strobl - */ -class JpaRepositoryContributorUnitTests { - - private static Verifyer generated; - - @BeforeAll - static void beforeAll() { - - TestJpaAotRepsitoryContext aotContext = new TestJpaAotRepsitoryContext(UserRepository.class, null); - TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); - - new JpaRepsoitoryContributor(aotContext).contribute(generationContext); - - AbstractBeanDefinition emBeanDefinition = BeanDefinitionBuilder - .rootBeanDefinition("org.springframework.orm.jpa.SharedEntityManagerCreator") - .setFactoryMethod("createSharedEntityManager").addConstructorArgReference("entityManagerFactory") - .setLazyInit(true).getBeanDefinition(); - - AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder - .genericBeanDefinition("com.example.UserRepositoryImpl__Aot") - .addConstructorArgReference("jpaSharedEM_entityManagerFactory").getBeanDefinition(); - - - /* - alter the RepositoryFactory so we can write generated calsses into a supplier and then write some custom code for instantiation - on JpaRepositoryFactoryBean - - beanDefinition.getPropertyValues().addPropertyValue("aotImplementation", new Function() { - - public Instance apply(BeanFactory beanFactor) { - EntityManager em = beanFactory.getBean(EntityManger.class); - return new com.example.UserRepositoryImpl__Aot(em); - } - }); - */ - - // register a dedicated factory that can read stuff - // don't write to spring.factories or uas another name for it - // maybe write the code directly to a repo fragment - // repo does not have to be a bean, but can be a method called by some component - // pass list to entiy manager to have stuff in memory have to list written out directly when creating the bean - - generated = generateContext(generationContext) // - .registerBeansFrom(new ClassPathResource("infrastructure.xml")) - .register("jpaSharedEM_entityManagerFactory", emBeanDefinition) - .register("aotUserRepository", aotGeneratedRepository); - } - - @BeforeEach - public void beforeEach() { - - generated.doWithBean(EntityManager.class, em -> { - - em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate(); - - User luke = new User("Luke", "Skywalker", "luke@jedi.org"); - em.persist(luke); - - User leia = new User("Leia", "Organa", "leia@resistance.gov"); - em.persist(leia); - - User han = new User("Han", "Solo", "han@smuggler.net"); - em.persist(han); - - User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net"); - em.persist(chewbacca); - - User yoda = new User("Yoda", "n/a", "yoda@jedi.org"); - em.persist(yoda); - - User vader = new User("Anakin", "Skywalker", "vader@empire.com"); - em.persist(vader); - - User kylo = new User("Ben", "Solo", "kylo@new-empire.com"); - em.persist(kylo); - }); - } - - @Test - void testFindDerivedFinderSingleEntity() { - - generated.verify(methodInvoker -> { - - User user = methodInvoker.invoke("findByEmailAddress", "luke@jedi.org").onBean("aotUserRepository"); - assertThat(user.getLastname()).isEqualTo("Skywalker"); - }); - } - - @Test - void testFindDerivedFinderOptionalEntity() { - - generated.verify(methodInvoker -> { - - Optional user = methodInvoker.invoke("findOptionalOneByEmailAddress", "yoda@jedi.org") - .onBean("aotUserRepository"); - assertThat(user).isNotNull().containsInstanceOf(User.class) - .hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda")); - }); - } - - @Test - void testDerivedCount() { - - generated.verify(methodInvoker -> { - - Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); - assertThat(value).isEqualTo(2L); - }); - } - - @Test - void testDerivedExists() { - - generated.verify(methodInvoker -> { - - Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository"); - assertThat(exists).isTrue(); - }); - } - - @Test - void testDerivedFinderWithoutArguments() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository"); - assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); - }); - } - - @Test - void testDerivedFinderReturningList() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com", - "kylo@new-empire.com", "han@smuggler.net"); - }); - } - - @Test - void testLimitedDerivedFinder() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository"); - assertThat(users).hasSize(2); - }); - } - - @Test - void testSortedDerivedFinder() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWithOrderByEmailAddress", "S") - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", - "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testDerivedFinderWithLimitArgument() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2)) - .onBean("aotUserRepository"); - assertThat(users).hasSize(2); - }); - } - - @Test - void testDerivedFinderWithSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress")) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", - "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testDerivedFinderWithSortAndLimit() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress"), Limit.of(2)) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); - }); - } - - @Test - void testDerivedFinderReturningListWithPageable() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker - .invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); - }); - } - - @Test - void testDerivedFinderReturningPage() { - - generated.verify(methodInvoker -> { - - Page page = methodInvoker - .invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - assertThat(page.getTotalElements()).isEqualTo(4); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", - "kylo@new-empire.com"); - }); - } - - @Test - void testDerivedFinderReturningSlice() { - - generated.verify(methodInvoker -> { - - Slice slice = methodInvoker - .invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - assertThat(slice.hasNext()).isTrue(); - assertThat(slice.getSize()).isEqualTo(2); - assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", - "kylo@new-empire.com"); - }); - } - - @Test - void testAnnotatedFinderReturningSingleValueWithQuery() { - - generated.verify(methodInvoker -> { - - User user = methodInvoker.invoke("findAnnotatedQueryByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository"); - assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda"); - }); - } - - @Test - void testAnnotatedFinderReturningListWithQuery() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", - "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastnameParamter", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", - "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testAnnotatedMultilineFinderWithQuery() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", - "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testAnnotatedFinderWithQueryAndLimit() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2)) - .onBean("aotUserRepository"); - assertThat(users).hasSize(2); - }); - } - - @Test - void testAnnotatedFinderWithQueryAndSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("emailAddress")) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com", - "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testAnnotatedFinderWithQueryLimitAndSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("emailAddress")) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); - }); - } - - @Test - void testAnnotatedFinderReturningListWithPageable() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker - .invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); - }); - } - - @Test - void testAnnotatedFinderReturningPage() { - - generated.verify(methodInvoker -> { - - Page page = methodInvoker - .invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - assertThat(page.getTotalElements()).isEqualTo(4); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", - "kylo@new-empire.com"); - }); - } - - @Test - void testAnnotatedFinderReturningSlice() { - - generated.verify(methodInvoker -> { - - Slice slice = methodInvoker - .invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - assertThat(slice.hasNext()).isTrue(); - assertThat(slice.getSize()).isEqualTo(2); - assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", - "kylo@new-empire.com"); - }); - } - - @Test - void testDerivedFinderReturningListOfProjections() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S") - .onBean("aotUserRepository"); - assertThat(users).extracting(UserDtoProjection::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", - "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); - }); - } - - @Test - void testDerivedFinderReturningPageOfProjections() { - - generated.verify(methodInvoker -> { - - Page page = methodInvoker - .invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress"))) - .onBean("aotUserRepository"); - - assertThat(page.getTotalElements()).isEqualTo(4); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", - "kylo@new-empire.com"); - }); - } - - // modifying - - @Test - void testDerivedDeleteSingle() { - - generated.verifyInTx(methodInvoker -> { - - User result = methodInvoker.invoke("deleteByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository"); - - assertThat(result).isNotNull().extracting(User::getEmailAddress).isEqualTo("yoda@jedi.org"); - }).doWithBean(EntityManager.class, em -> { - Object yodaShouldBeGone = em - .createQuery("SELECT u FROM %s u WHERE u.emailAddress = 'yoda@jedi.org'".formatted(User.class.getName())) - .getSingleResultOrNull(); - assertThat(yodaShouldBeGone).isNull(); - }); - } - - // native queries - - @Test - void nativeQuery() { - - generated.verify(methodInvoker -> { - - Page page = methodInvoker - .invoke("findByNativeQueryWithPageable", PageRequest.of(0, 2)) - .onBean("aotUserRepository"); - - assertThat(page.getTotalElements()).isEqualTo(7); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).containsExactly("Anakin", "Ben"); - }); - } - - // old stuff below - - // TODO: - void todo() { - - // Query q; - // q.setMaxResults() - // q.setFirstResult() - - // 1 build some more stuff from below - // 2 set up boot sample project in data samples - - // query hints - // first and max result for pagination - // entity graphs - // native queries - // delete - // @Modifying - // flush / clear - } - - static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) { - return new GeneratedContextBuilder(generationContext); - } - - static class GeneratedContextBuilder implements Verifyer { - - TestGenerationContext generationContext; - Map beanDefinitions = new LinkedHashMap<>(); - Resource xmlBeanDefinitions; - Lazy lazyFactory; - - public GeneratedContextBuilder(TestGenerationContext generationContext) { - - this.generationContext = generationContext; - this.lazyFactory = Lazy.of(() -> { - DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); - TestCompiler.forSystem().with(generationContext).compile(compiled -> { - - freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); - if (xmlBeanDefinitions != null) { - XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(freshBeanFactory); - beanDefinitionReader.loadBeanDefinitions(xmlBeanDefinitions); - } - - for (Entry entry : beanDefinitions.entrySet()) { - freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue()); - } - }); - return freshBeanFactory; - }); - } - - GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) { - this.beanDefinitions.put(name, beanDefinition); - return this; - } - - GeneratedContextBuilder registerBeansFrom(Resource xmlBeanDefinitions) { - this.xmlBeanDefinitions = xmlBeanDefinitions; - return this; - } - - public Verifyer verify(Consumer methodInvoker) { - methodInvoker.accept(new GeneratedContext(lazyFactory)); - return this; - } - - } - - interface Verifyer { - Verifyer verify(Consumer methodInvoker); - - default Verifyer verifyInTx(Consumer methodInvoker) { - - verify(ctx -> { - - PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class); - new TransactionTemplate(txMgr).execute(action -> { - verify(methodInvoker); - return "ok"; - }); - }); - - return this; - } - - default void doWithBean(Class type, Consumer runit) { - verify(ctx -> { - - boolean isEntityManager = type == EntityManager.class; - T bean = ctx.delegate.get().getBean(type); - - if (!isEntityManager) { - runit.accept(bean); - } else { - - PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class); - new TransactionTemplate(txMgr).execute(action -> { - runit.accept(bean); - return "ok"; - }); - - } - }); - } - } - - static class GeneratedContext { - - private Supplier delegate; - - public GeneratedContext(Supplier defaultListableBeanFactory) { - this.delegate = defaultListableBeanFactory; - } - - InvocationBuilder invoke(String method, Object... arguments) { - - return new InvocationBuilder() { - @Override - public T onBean(String beanName) { - DefaultListableBeanFactory defaultListableBeanFactory = delegate.get(); - - Object bean = defaultListableBeanFactory.getBean(beanName); - return ReflectionTestUtils.invokeMethod(bean, method, arguments); - } - }; - } - - interface InvocationBuilder { - T onBean(String beanName); - } - - } - -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java index ad1273b8c5..e90ce0aae2 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.core.CrudMethods; import org.springframework.data.repository.core.RepositoryInformation; @@ -27,7 +29,6 @@ import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepositoryContext.java similarity index 89% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepositoryContext.java index 433a6e602d..df4e62a873 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepositoryContext.java @@ -37,14 +37,20 @@ /** * @author Christoph Strobl */ -class TestJpaAotRepsitoryContext implements AotRepositoryContext { +class TestJpaAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; + private final Class repositoryInterface; - public TestJpaAotRepsitoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInterface = repositoryInterface; this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } + public Class getRepositoryInterface() { + return repositoryInterface; + } + @Override public ConfigurableListableBeanFactory getBeanFactory() { return null; diff --git a/spring-data-jpa/src/test/java/com/example/UserDtoProjection.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserDtoProjection.java similarity index 94% rename from spring-data-jpa/src/test/java/com/example/UserDtoProjection.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserDtoProjection.java index 2605f553f2..bc8d8f578a 100644 --- a/spring-data-jpa/src/test/java/com/example/UserDtoProjection.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserDtoProjection.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.example; +package org.springframework.data.jpa.repository.aot.generated; /** * @author Christoph Strobl diff --git a/spring-data-jpa/src/test/java/com/example/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java similarity index 92% rename from spring-data-jpa/src/test/java/com/example/UserRepository.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java index 8c3e9135e9..4e8088fa33 100644 --- a/spring-data-jpa/src/test/java/com/example/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.example; +package org.springframework.data.jpa.repository.aot.generated; import java.util.List; import java.util.Optional; @@ -27,7 +27,6 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.query.Param; /** * @author Christoph Strobl @@ -42,7 +41,7 @@ public interface UserRepository extends CrudRepository { Long countUsersByLastname(String lastname); - Boolean existsUserByLastname(String lastname); + boolean existsUserByLastname(String lastname); List findByLastnameStartingWith(String lastname); @@ -71,7 +70,7 @@ public interface UserRepository extends CrudRepository { List findAnnotatedQueryByLastname(String lastname); @Query("select u from User u where u.lastname like :lastname%") - List findAnnotatedQueryByLastnameParamter(String lastname); + List findAnnotatedQueryByLastnameParameter(String lastname); @Query(""" select u @@ -94,6 +93,9 @@ public interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like ?1%") Page findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable); + @Query("select u from User u where u.lastname like ?1% ORDER BY u.lastname") + Page findAnnotatedQueryPageWithStaticSort(String lastname, Pageable pageable); + @Query("select u from User u where u.lastname like ?1%") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); @@ -115,8 +117,6 @@ public interface UserRepository extends CrudRepository { // projections - - List findUserProjectionByLastnameStartingWith(String lastname); Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); @@ -137,4 +137,5 @@ public interface UserRepository extends CrudRepository { List findByLastnameOrderByFirstname(String lastname); User findByEmailAddress(String emailAddress); + } From 52e453cc05a69b842eafc09c9b51667090ce09ac Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 08:56:51 +0100 Subject: [PATCH 56/94] Add query hint support. See #3830 --- .../aot/generated/JpaCodeBlocks.java | 235 +++++++++++------- .../generated/JpaRepositoryContributor.java | 4 +- ...RepositoryContributorIntegrationTests.java | 9 +- .../aot/generated/UserRepository.java | 6 + 4 files changed, 159 insertions(+), 95 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java index 7bfdb07173..3f249fdf4f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java @@ -17,13 +17,16 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; +import jakarta.persistence.QueryHint; import java.util.List; import java.util.Optional; import java.util.function.LongSupplier; import java.util.regex.Pattern; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.domain.SliceImpl; +import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.ParameterBinding; import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; @@ -37,93 +40,21 @@ /** * @author Christoph Strobl - * @since 2025/01 + * @author Mark Paluch + * @since 4.0 */ -public class JpaCodeBlocks { +class JpaCodeBlocks { private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); - static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + public static QueryBlockBuilder queryBuilder(AotRepositoryMethodGenerationContext context) { return new QueryBlockBuilder(context); } - static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + static QueryExecutionBlockBuilder executionBuilder(AotRepositoryMethodGenerationContext context) { return new QueryExecutionBlockBuilder(context); } - static class QueryExecutionBlockBuilder { - - AotRepositoryMethodGenerationContext context; - private String queryVariableName = "query"; - - public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { - this.context = context; - } - - QueryExecutionBlockBuilder referencing(String queryVariableName) { - - this.queryVariableName = queryVariableName; - return this; - } - - CodeBlock build() { - - Builder builder = CodeBlock.builder(); - - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); - Object actualReturnType = isProjecting ? context.getActualReturnType() - : context.getRepositoryInformation().getDomainType(); - - builder.add("\n"); - - if (context.isDeleteMethod()) { - - builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); - builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class)); - if (context.returnsSingleValue()) { - if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { - builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType()); - } else { - builder.addStatement("return resultList.isEmpty() ? null : resultList.iterator().next()"); - } - } else { - builder.addStatement("return resultList"); - } - } else if (context.isExistsMethod()) { - builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); - } else { - - if (context.returnsSingleValue()) { - if (context.returnsOptionalValue()) { - builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, - actualReturnType, queryVariableName); - } else { - builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnType(), queryVariableName); - } - } else if (context.returnsPage()) { - builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", - PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, - context.getPageableParameterName()); - } else if (context.returnsSlice()) { - builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, - queryVariableName); - builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", - context.getPageableParameterName(), context.getPageableParameterName()); - builder.addStatement( - "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", - SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); - } else { - builder.addStatement("return ($T) query.getResultList()", context.getReturnType()); - } - } - - return builder.build(); - - } - } - /** * Builder for the actual query code block. */ @@ -132,23 +63,35 @@ static class QueryBlockBuilder { private final AotRepositoryMethodGenerationContext context; private String queryVariableName = "query"; private AotQueries queries; + private MergedAnnotation queryHints = MergedAnnotation.missing(); - public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { + private QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; } - QueryBlockBuilder usingQueryVariableName(String queryVariableName) { + public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { this.queryVariableName = queryVariableName; return this; } - QueryBlockBuilder filter(AotQueries query) { + public QueryBlockBuilder filter(AotQueries query) { this.queries = query; return this; } - CodeBlock build() { + public QueryBlockBuilder queryHints(MergedAnnotation queryHints) { + + this.queryHints = queryHints; + return this; + } + + /** + * Build the query block. + * + * @return + */ + public CodeBlock build() { boolean isProjecting = context.getActualReturnType() != null && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), @@ -172,8 +115,7 @@ CodeBlock build() { countQuyerVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName)); StringAotQuery countQuery = (StringAotQuery) queries.count(); - builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, - countQuery.getQueryString()); + builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, countQuery.getQueryString()); } // sorting @@ -185,17 +127,21 @@ CodeBlock build() { } if (StringUtils.hasText(sortParameterName)) { - applySorting(builder, sortParameterName, queryStringNameVariableName, actualReturnType); + builder.add(applySorting(sortParameterName, queryStringNameVariableName, actualReturnType)); } - addQueryBlock(builder, queryVariableName, queryStringNameVariableName, queries.result()); + builder.add(createQuery(queryVariableName, queryStringNameVariableName, queries.result(), queryHints)); - applyLimits(builder); + builder.add(applyLimits()); if (StringUtils.hasText(countQueryStringNameVariableName)) { builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); - addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, queries.count()); + + boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); + + builder.add(createQuery(countQuyerVariableName, countQueryStringNameVariableName, queries.count(), + queryHints ? this.queryHints : MergedAnnotation.missing())); builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName); // end control flow does not work well with lambdas @@ -206,8 +152,9 @@ CodeBlock build() { return builder.build(); } - private void applySorting(Builder builder, String sort, String queryString, Object actualReturnType) { + private CodeBlock applySorting(String sort, String queryString, Object actualReturnType) { + Builder builder = CodeBlock.builder(); builder.beginControlFlow("if ($L.isSorted())", sort); if (queries.isNative()) { @@ -221,14 +168,18 @@ private void applySorting(Builder builder, String sort, String queryString, Obje builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); builder.endControlFlow(); + + return builder.build(); } - private void applyLimits(Builder builder) { + private CodeBlock applyLimits() { + + Builder builder = CodeBlock.builder(); if (context.isExistsMethod()) { builder.addStatement("$L.setMaxResults(1)", queryVariableName); - return; + return builder.build(); } String limit = context.getLimitParameterName(); @@ -254,15 +205,24 @@ private void applyLimits(Builder builder) { } builder.endControlFlow(); } + + return builder.build(); } - private void addQueryBlock(Builder builder, String queryVariableName, String queryStringNameVariableName, - AotQuery query) { + private CodeBlock createQuery(String queryVariableName, String queryStringNameVariableName, AotQuery query, + MergedAnnotation queryHints) { + + Builder builder = CodeBlock.builder(); builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery", queryStringNameVariableName); + if (queryHints.isPresent()) { + builder.add(applyHints(queryVariableName, queryHints)); + builder.add("\n"); + } + for (ParameterBinding binding : query.getParameterBindings()) { Object prepare = binding.prepare("s"); @@ -287,6 +247,97 @@ private void addQueryBlock(Builder builder, String queryVariableName, String que } } } + + return builder.build(); } + + private CodeBlock applyHints(String queryVariableName, MergedAnnotation queryHints) { + + Builder hintsBuilder = CodeBlock.builder(); + MergedAnnotation[] values = queryHints.getAnnotationArray("value", QueryHint.class); + + for (MergedAnnotation hint : values) { + hintsBuilder.addStatement("$L.setHint($S, $S)", queryVariableName, hint.getString("name"), + hint.getString("value")); + } + + return hintsBuilder.build(); + } + } + + static class QueryExecutionBlockBuilder { + + private final AotRepositoryMethodGenerationContext context; + private String queryVariableName = "query"; + + private QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + public QueryExecutionBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + public CodeBlock build() { + + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + builder.add("\n"); + + if (context.isDeleteMethod()) { + + builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); + builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class)); + if (context.returnsSingleValue()) { + if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { + builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType()); + } else { + builder.addStatement("return resultList.isEmpty() ? null : resultList.iterator().next()"); + } + } else { + builder.addStatement("return resultList"); + } + } else if (context.isExistsMethod()) { + builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); + } else { + + if (context.returnsSingleValue()) { + if (context.returnsOptionalValue()) { + builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, + actualReturnType, queryVariableName); + } else { + builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnType(), queryVariableName); + } + } else if (context.returnsPage()) { + builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", + PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, + context.getPageableParameterName()); + } else if (context.returnsSlice()) { + builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, + queryVariableName); + builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", + context.getPageableParameterName(), context.getPageableParameterName()); + builder.addStatement( + "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", + SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + } else { + builder.addStatement("return ($T) query.getResultList()", context.getReturnType()); + } + } + + return builder.build(); + + } + + } + + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java index 2d4a92bacd..c16f8a6ed8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java @@ -125,8 +125,8 @@ protected AotRepositoryMethodBuilder contributeRepositoryMethod( aotQueries = buildPartTreeQuery(context, query); } - body.addCode(JpaCodeBlocks.queryBlockBuilder(context).filter(aotQueries).build()); - body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).build()); + body.addCode(JpaCodeBlocks.queryBuilder(context).filter(aotQueries).queryHints(queryHints).build()); + body.addCode(JpaCodeBlocks.executionBuilder(context).build()); }); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java index 582b476277..bfa8077082 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java @@ -297,6 +297,12 @@ void testDerivedFinderReturningListOfProjections() { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } + @Test + void shouldApplyQueryHints() { + assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker")) + .withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo"); + } + @Test void testDerivedFinderReturningPageOfProjections() { @@ -344,8 +350,9 @@ void todo() { // interface projections // named queries + // dynamic projections + // class type parameter - // query hints // entity graphs // native queries // delete diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java index 4e8088fa33..d783c9cdfc 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jpa.repository.aot.generated; +import jakarta.persistence.QueryHint; + import java.util.List; import java.util.Optional; @@ -26,6 +28,7 @@ import org.springframework.data.jpa.domain.sample.User; 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.repository.CrudRepository; /** @@ -128,6 +131,9 @@ public interface UserRepository extends CrudRepository { List findByLastname(String lastname); + @QueryHints(value = { @QueryHint(name = "jakarta.persistence.cache.storeMode", value = "foo") }, forCounting = false) + List findHintedByLastname(String lastname); + List findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit); List findByLastname(String lastname, Sort sort); From e890fd7e14e040be89e458f55de586306375e796 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 09:13:38 +0100 Subject: [PATCH 57/94] Polishing. Fix Like with starts/ends, use proper parameter origins instead of assuming binding name matches parameter names. Simplify binding block. See #3830 --- .../aot/generated/AotQueryCreator.java | 34 ------------- .../aot/generated/JpaCodeBlocks.java | 48 +++++++++++++------ .../generated/JpaRepositoryContributor.java | 10 +++- .../aot/generated/StringAotQuery.java | 23 ++++++++- .../repository/query/ParameterBinding.java | 6 +-- ...RepositoryContributorIntegrationTests.java | 21 +++++++- .../aot/generated/UserRepository.java | 3 ++ 7 files changed, 88 insertions(+), 57 deletions(-) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java deleted file mode 100644 index 98254aff9b..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java +++ /dev/null @@ -1,34 +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 - * - * http://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.aot.generated; - -import jakarta.persistence.metamodel.Metamodel; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -class AotQueryCreator { - - Metamodel metamodel; - - public AotQueryCreator(Metamodel metamodel) { - this.metamodel = metamodel; - } - - - -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java index 3f249fdf4f..edf9bfbd85 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java @@ -226,31 +226,49 @@ private CodeBlock createQuery(String queryVariableName, String queryStringNameVa for (ParameterBinding binding : query.getParameterBindings()) { Object prepare = binding.prepare("s"); + Object parameterIdentifier = getParameterName(binding.getIdentifier()); + String valueFormat = parameterIdentifier instanceof CharSequence ? "$S" : "$L"; if (prepare instanceof String prepared && !prepared.equals("s")) { + String format = prepared.replaceAll("%", "%%").replace("s", "%s"); - if (binding.getIdentifier().hasPosition()) { - builder.addStatement("$L.setParameter($L, $S.formatted($L))", queryVariableName, - binding.getIdentifier().getPosition(), format, - context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1)); - } else { - builder.addStatement("$L.setParameter($S, $S.formatted($L))", queryVariableName, - binding.getIdentifier().getName(), format, binding.getIdentifier().getName()); - } + builder.addStatement("$L.setParameter(%s, $S.formatted($L))".formatted(valueFormat), queryVariableName, + parameterIdentifier, format, getParameter(binding.getOrigin())); } else { - if (binding.getIdentifier().hasPosition()) { - builder.addStatement("$L.setParameter($L, $L)", queryVariableName, binding.getIdentifier().getPosition(), - context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1)); - } else { - builder.addStatement("$L.setParameter($S, $L)", queryVariableName, binding.getIdentifier().getName(), - binding.getIdentifier().getName()); - } + builder.addStatement("$L.setParameter(%s, $L)".formatted(valueFormat), queryVariableName, parameterIdentifier, + getParameter(binding.getOrigin())); } } return builder.build(); } + private Object getParameterName(ParameterBinding.BindingIdentifier identifier) { + + if (identifier.hasPosition()) { + return identifier.getPosition(); + } + + return identifier.getName(); + + } + + private Object getParameter(ParameterBinding.ParameterOrigin origin) { + + if (origin.isMethodArgument() && origin instanceof ParameterBinding.MethodInvocationArgument mia) { + + if (mia.identifier().hasPosition()) { + return context.getParameterNameOfPosition(mia.identifier().getPosition() - 1); + } + + if (mia.identifier().hasName()) { + return mia.identifier().getName(); + } + } + + throw new UnsupportedOperationException("Not supported yet"); + } + private CodeBlock applyHints(String queryVariableName, MergedAnnotation queryHints) { Builder hintsBuilder = CodeBlock.builder(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java index c16f8a6ed8..7f29a51f59 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java @@ -23,6 +23,8 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; @@ -59,14 +61,12 @@ public class JpaRepositoryContributor extends RepositoryContributor { private final CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory(); - private final AotQueryCreator queryCreator; private final AotMetaModel metaModel; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); this.metaModel = new AotMetaModel(repositoryContext.getResolvedTypes()); - this.queryCreator = new AotQueryCreator(metaModel); } @Override @@ -106,6 +106,12 @@ protected AotRepositoryMethodBuilder contributeRepositoryMethod( } } + // no KeysetScrolling for now. + if (generationContext.getParameterNameOf(ScrollPosition.class) != null + || generationContext.getParameterNameOf(KeysetScrollPosition.class) != null) { + return null; + } + // TODO: Named query via EntityManager, NamedQuery via properties, also for count queries. return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java index c9a0d318f2..fb41cabcef 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java @@ -34,6 +34,9 @@ private StringAotQuery(List parameterBindings) { super(parameterBindings); } + /** + * Creates a new {@code StringAotQuery} from a {@link DeclaredQuery}. Parses the query into {@link PreprocessedQuery}. + */ static StringAotQuery of(DeclaredQuery query) { if (query instanceof PreprocessedQuery pq) { @@ -43,21 +46,37 @@ static StringAotQuery of(DeclaredQuery query) { return new DeclaredAotQuery(PreprocessedQuery.parse(query)); } + /** + * Creates a new {@code StringAotQuery} from a JPQL {@code queryString}. Parses the query into + * {@link PreprocessedQuery}. + */ static StringAotQuery jpqlQuery(String queryString) { return of(DeclaredQuery.jpqlQuery(queryString)); } + /** + * Creates a JPQL {@code StringAotQuery} using the given bindings and limit. + */ public static StringAotQuery jpqlQuery(String queryString, List bindings, Limit resultLimit) { return new LimitedAotQuery(queryString, bindings, resultLimit); } + /** + * Creates a new {@code StringAotQuery} from a native (SQL) {@code queryString}. Parses the query into + * {@link PreprocessedQuery}. + */ static StringAotQuery nativeQuery(String queryString) { return of(DeclaredQuery.nativeQuery(queryString)); } + /** + * @return the underlying declared query. + */ public abstract DeclaredQuery getQuery(); - public abstract String getQueryString(); + public String getQueryString() { + return getQuery().getQueryString(); + } @Override public String toString() { @@ -94,6 +113,8 @@ public PreprocessedQuery getQuery() { } /** + * Query with a limit associated. + * * @author Mark Paluch */ static class LimitedAotQuery extends StringAotQuery { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index 8b40751cd6..b06b0f9711 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -25,9 +25,9 @@ import java.util.List; import java.util.stream.Collectors; -import org.springframework.data.expression.ValueExpression; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.expression.ValueExpression; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Parameter; @@ -608,7 +608,7 @@ public String toString() { * @author Mark Paluch * @since 3.1.2 */ - sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic { + public sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic { /** * Creates a {@link Expression} for the given {@code expression}. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java index bfa8077082..c61be724ad 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java @@ -218,6 +218,20 @@ void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } + @Test + void shouldApplyAnnotatedLikeStartsEnds() { + + // start with case + List users = fragment.findAnnotatedLikeStartsEnds("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + + // ends case + users = fragment.findAnnotatedLikeStartsEnds("a"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("leia@resistance.gov", + "chewie@smuggler.net", "yoda@jedi.org"); + } + @Test void testAnnotatedMultilineFinderWithQuery() { @@ -306,7 +320,6 @@ void shouldApplyQueryHints() { @Test void testDerivedFinderReturningPageOfProjections() { - // TODO: query.setParameter(1, "%s%%".formatted(lastname)); Page page = fragment.findUserProjectionByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("emailAddress"))); @@ -314,6 +327,9 @@ void testDerivedFinderReturningPageOfProjections() { assertThat(page.getSize()).isEqualTo(2); assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + + Page noResults = fragment.findUserProjectionByLastnameStartingWith("a", + PageRequest.of(0, 2, Sort.by("emailAddress"))); } // modifying @@ -345,9 +361,10 @@ void nativeQuery() { // old stuff below - // TODO: void todo() { + // expressions, templated query with #{#entityName} + // synthetic parameters (keyset scrolling! yuck!) // interface projections // named queries // dynamic projections diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java index d783c9cdfc..f766a3f164 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java @@ -75,6 +75,9 @@ public interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like :lastname%") List findAnnotatedQueryByLastnameParameter(String lastname); + @Query("select u from User u where u.lastname like :lastname% or u.lastname like %:lastname") + List findAnnotatedLikeStartsEnds(String lastname); + @Query(""" select u from User u From f76ff3e2143e6fa4a3ea021ed65521f46d6900f3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 10:12:06 +0100 Subject: [PATCH 58/94] Add support for Value Expressions, Stream, Named and Modifying Queries. See #3830 --- .../data/jpa/provider/HibernateUtils.java | 44 ++- .../data/jpa/provider/JpaClassUtils.java | 6 +- .../jpa/provider/PersistenceProvider.java | 86 +++++- .../data/jpa/provider/QueryExtractor.java | 19 +- .../{AotMetaModel.java => AotMetamodel.java} | 21 +- .../repository/aot/generated/AotQueries.java | 15 +- .../repository/aot/generated/AotQuery.java | 28 ++ .../AotRepositoryFragmentSupport.java | 3 + .../aot/generated/JpaCodeBlocks.java | 288 ++++++++++++++---- .../generated/JpaRepositoryContributor.java | 274 ++++++++++++----- .../aot/generated/NamedAotQuery.java | 63 ++++ .../aot/generated/StringAotQuery.java | 63 +++- .../jpa/repository/query/EntityQuery.java | 2 +- .../repository/query/JpaQueryExecution.java | 13 +- .../jpa/repository/query/JpaQueryMethod.java | 4 +- .../data/jpa/domain/sample/User.java | 2 + .../AotFragmentTestConfigurationSupport.java | 2 +- ...RepositoryContributorIntegrationTests.java | 117 ++++++- .../aot/generated/UserRepository.java | 41 ++- .../jpa/repository/sample/UserRepository.java | 8 +- 20 files changed, 905 insertions(+), 194 deletions(-) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/{AotMetaModel.java => AotMetamodel.java} (86%) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java index 414d8d5952..f185237d4b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateUtils.java @@ -15,8 +15,11 @@ */ package org.springframework.data.jpa.provider; +import org.hibernate.query.NativeQuery; import org.hibernate.query.Query; import org.hibernate.query.spi.SqmQuery; +import org.hibernate.query.sql.spi.NamedNativeQueryMemento; +import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.jspecify.annotations.Nullable; /** @@ -44,7 +47,6 @@ private HibernateUtils() {} public @Nullable static String getHibernateQuery(Object query) { try { - // Try the new Hibernate implementation first if (query instanceof SqmQuery sqmQuery) { @@ -57,6 +59,22 @@ private HibernateUtils() {} return sqmQuery.getSqmStatement().toHqlString(); } + // Try the new Hibernate implementation first + if (query instanceof NamedSqmQueryMemento sqmQuery) { + + String hql = sqmQuery.getHqlString(); + + if (!hql.equals("")) { + return hql; + } + + return sqmQuery.getSqmStatement().toHqlString(); + } + + if (query instanceof NamedNativeQueryMemento nativeQuery) { + return nativeQuery.getSqlString(); + } + // Couple of cases in which this still breaks, see HHH-15389 } catch (RuntimeException o_O) {} @@ -67,4 +85,28 @@ private HibernateUtils() {} throw new IllegalArgumentException("Don't know how to extract the query string from " + query); } } + + public static boolean isNativeQuery(Object query) { + + // Try the new Hibernate implementation first + if (query instanceof SqmQuery) { + return false; + } + + if (query instanceof NativeQuery) { + return true; + } + + // Try the new Hibernate implementation first + if (query instanceof NamedSqmQueryMemento) { + + return false; + } + + if (query instanceof NamedNativeQueryMemento) { + return true; + } + + return false; + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java index f6ea036c2b..71971df3bf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java @@ -18,9 +18,9 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.Metamodel; -import org.springframework.util.Assert; - import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -59,7 +59,7 @@ public static boolean isMetamodelOfType(Metamodel metamodel, String type) { return isOfType(metamodel, type, metamodel.getClass().getClassLoader()); } - private static boolean isOfType(Object source, String typeName, @Nullable ClassLoader classLoader) { + public static boolean isOfType(Object source, String typeName, @Nullable ClassLoader classLoader) { Assert.notNull(source, "Source instance must not be null"); Assert.hasText(typeName, "Target type name must not be null or empty"); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 14a8db9dcc..4d604b452c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -19,6 +19,7 @@ import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Query; import jakarta.persistence.metamodel.IdentifiableType; import jakarta.persistence.metamodel.Metamodel; @@ -65,14 +66,20 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer * @see DATAJPA-444 */ HIBERNATE(// + Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), // Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) { @Override - public @Nullable String extractQueryString(Query query) { + public @Nullable String extractQueryString(Object query) { return HibernateUtils.getHibernateQuery(query); } + @Override + public boolean isNativeQuery(Object query) { + return HibernateUtils.isNativeQuery(query); + } + /** * Return custom placeholder ({@code *}) as Hibernate does create invalid queries for count queries for objects with * compound keys. @@ -115,14 +122,20 @@ public String getCommentHintKey() { /** * EclipseLink persistence provider. */ - ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), + ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2), + Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) { @Override - public String extractQueryString(Query query) { + public String extractQueryString(Object query) { return ((JpaQuery) query).getDatabaseQuery().getJPQLString(); } + @Override + public boolean isNativeQuery(Object query) { + return false; + } + @Override public boolean shouldUseAccessorFor(Object entity) { return false; @@ -152,13 +165,19 @@ public String getCommentHintValue(String comment) { /** * Unknown special provider. Use standard JPA. */ - GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { + GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), + Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { @Override - public @Nullable String extractQueryString(Query query) { + public @Nullable String extractQueryString(Object query) { return null; } + @Override + public boolean isNativeQuery(Object query) { + return false; + } + @Override public boolean canExtractQuery() { return false; @@ -196,6 +215,7 @@ public boolean shouldUseAccessorFor(Object entity) { private static final Collection ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA); private static final ConcurrentReferenceHashMap, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>(); + private final Iterable entityManagerFactoryClassNames; private final Iterable entityManagerClassNames; private final Iterable metamodelClassNames; @@ -204,24 +224,38 @@ public boolean shouldUseAccessorFor(Object entity) { /** * Creates a new {@link PersistenceProvider}. * + * @param entityManagerFactoryClassNames the names of the provider specific + * {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty. * @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not * be {@literal null} or empty. * @param metamodelClassNames must not be {@literal null}. */ - PersistenceProvider(Iterable entityManagerClassNames, Iterable metamodelClassNames) { + PersistenceProvider(Iterable entityManagerFactoryClassNames, Iterable entityManagerClassNames, + Iterable metamodelClassNames) { + this.entityManagerFactoryClassNames = entityManagerFactoryClassNames; this.entityManagerClassNames = entityManagerClassNames; this.metamodelClassNames = metamodelClassNames; boolean present = false; - for (String entityManagerClassName : entityManagerClassNames) { + for (String emfClassName : entityManagerFactoryClassNames) { - if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) { + if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) { present = true; break; } } + if (!present) { + for (String entityManagerClassName : entityManagerClassNames) { + + if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) { + present = true; + break; + } + } + } + this.present = present; } @@ -266,6 +300,36 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { return cacheAndReturn(entityManagerType, GENERIC_JPA); } + /** + * Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be + * determined {@link #GENERIC_JPA} will be returned. + * + * @param emf must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) { + + Assert.notNull(emf, "EntityManager must not be null"); + + Class entityManagerType = emf.getPersistenceUnitUtil().getClass(); + PersistenceProvider cachedProvider = CACHE.get(entityManagerType); + + if (cachedProvider != null) { + return cachedProvider; + } + + for (PersistenceProvider provider : ALL) { + for (String emfClassName : provider.entityManagerFactoryClassNames) { + if (isOfType(emf.getPersistenceUnitUtil(), emfClassName, + emf.getPersistenceUnitUtil().getClass().getClassLoader())) { + return cacheAndReturn(entityManagerType, provider); + } + } + } + + return cacheAndReturn(entityManagerType, GENERIC_JPA); + } + /** * Determines the {@link PersistenceProvider} from the given {@link Metamodel}. If no special one can be determined * {@link #GENERIC_JPA} will be returned. @@ -350,9 +414,15 @@ public boolean isPresent() { */ interface Constants { + String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory"; String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager"; + + String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate"; + String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl"; String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager"; + // needed as Spring only exposes that interface via the EM proxy + String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl"; String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor"; String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel"; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java index b9be1da3bf..6d25429525 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/QueryExtractor.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.provider; import jakarta.persistence.Query; +import jakarta.persistence.TypedQueryReference; import org.jspecify.annotations.Nullable; @@ -28,14 +29,25 @@ public interface QueryExtractor { /** - * Reverse engineers the query string from the {@link Query} object. This requires provider specific API as JPA does - * not provide access to the underlying query string as soon as one has created a {@link Query} instance of it. + * Reverse engineers the query string from the {@link Query} or a {@link TypedQueryReference} object. This requires + * provider specific API as JPA does not provide access to the underlying query string as soon as one has created a + * {@link Query} instance of it. * * @param query * @return the query string representing the query or {@literal null} if resolving is not possible. */ @Nullable - String extractQueryString(Query query); + String extractQueryString(Object query); + + /** + * Reverse engineers the query native flag from a {@link Query} or native query as JPA does not provide access to the + * underlying query string once a (named) query is constructed. + * + * @param query + * @return {@literal true} if the query is a native one. + * @since 4.0 + */ + boolean isNativeQuery(Object query); /** * Returns whether the extractor is able to extract the original query string from a given {@link Query}. @@ -43,4 +55,5 @@ public interface QueryExtractor { * @return */ boolean canExtractQuery(); + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java similarity index 86% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java index 797e7a45a4..fcdd221cf9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java @@ -36,8 +36,9 @@ /** * @author Christoph Strobl + * @since 4.0 */ -class AotMetaModel implements Metamodel { +class AotMetamodel implements Metamodel { private final String persistenceUnit; private final Set> managedTypes; @@ -45,21 +46,21 @@ class AotMetaModel implements Metamodel { private final Lazy metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel()); private final Lazy entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); - public AotMetaModel(Set> managedTypes) { + public AotMetamodel(Set> managedTypes) { this("dynamic-tests", managedTypes); } - private AotMetaModel(String persistenceUnit, Set> managedTypes) { + private AotMetamodel(String persistenceUnit, Set> managedTypes) { this.persistenceUnit = persistenceUnit; this.managedTypes = managedTypes; } - public static AotMetaModel hibernateModel(Class... types) { - return new AotMetaModel(Set.of(types)); + public static AotMetamodel hibernateModel(Class... types) { + return new AotMetamodel(Set.of(types)); } - public static AotMetaModel hibernateModel(String persistenceUnit, Class... types) { - return new AotMetaModel(persistenceUnit, Set.of(types)); + public static AotMetamodel hibernateModel(String persistenceUnit, Class... types) { + return new AotMetamodel(persistenceUnit, Set.of(types)); } public EntityType entity(Class cls) { @@ -95,6 +96,11 @@ public EntityManager entityManager() { return entityManager.get(); } + // TODO: Capture an existing factory bean (e.g. EntityManagerFactoryInfo) to extract PersistenceInfo + public EntityManagerFactory getEntityManagerFactory() { + return entityManagerFactory.get(); + } + EntityManagerFactory init() { MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { @@ -121,4 +127,5 @@ public List getManagedClassNames() { } }, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build(); } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java index c7d8051bd1..14f94e625f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java @@ -17,6 +17,8 @@ import jakarta.validation.constraints.Null; +import java.util.function.Function; + import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.QueryEnhancer; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; @@ -34,13 +36,22 @@ record AotQueries(AotQuery result, AotQuery count) { * Derive a count query from the given query. */ public static AotQueries from(StringAotQuery query, @Null String countProjection, QueryEnhancerSelector selector) { + return from(query, StringAotQuery::getQuery, countProjection, selector); + } + + /** + * Derive a count query from the given query. + */ + public static AotQueries from(T query, Function queryMapper, + @Null String countProjection, QueryEnhancerSelector selector) { - QueryEnhancer queryEnhancer = selector.select(query.getQuery()).create(query.getQuery()); + DeclaredQuery underlyingQuery = queryMapper.apply(query); + QueryEnhancer queryEnhancer = selector.select(underlyingQuery).create(underlyingQuery); String derivedCountQuery = queryEnhancer .createCountQueryFor(StringUtils.hasText(countProjection) ? countProjection : null); - DeclaredQuery countQuery = query.getQuery().rewrite(derivedCountQuery); + DeclaredQuery countQuery = underlyingQuery.rewrite(derivedCountQuery); return new AotQueries(query, StringAotQuery.of(countQuery)); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java index 2f48b43887..926fe45c4b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java @@ -58,4 +58,32 @@ public boolean isLimited() { return getLimit().isLimited(); } + /** + * @return whether the query a delete query. + */ + public boolean isDelete() { + return false; + } + + /** + * @return whether the query is an exists query. + */ + public boolean isExists() { + return false; + } + + /** + * @return {@literal true} if the query uses value expressions. + */ + public boolean hasExpression() { + + for (ParameterBinding parameterBinding : parameterBindings) { + if (parameterBinding.getOrigin().isExpression()) { + return true; + } + } + + return false; + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java index dd1deeec2b..a20acf49f5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java @@ -35,7 +35,10 @@ import org.springframework.util.ConcurrentLruCache; /** + * Support class for JPA AOT repository fragments. + * * @author Mark Paluch + * @since 4.0 */ public class AotRepositoryFragmentSupport { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java index edf9bfbd85..e39e89327b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java @@ -19,17 +19,24 @@ import jakarta.persistence.Query; import jakarta.persistence.QueryHint; +import java.lang.reflect.Type; import java.util.List; import java.util.Optional; import java.util.function.LongSupplier; -import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.domain.SliceImpl; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.ParameterBinding; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; @@ -39,20 +46,29 @@ import org.springframework.util.StringUtils; /** + * Common code blocks for JPA AOT Fragment generation. + * * @author Christoph Strobl * @author Mark Paluch * @since 4.0 */ class JpaCodeBlocks { - private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); - - public static QueryBlockBuilder queryBuilder(AotRepositoryMethodGenerationContext context) { - return new QueryBlockBuilder(context); + /** + * @param context + * @return new {@link QueryBlockBuilder}. + */ + public static QueryBlockBuilder queryBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { + return new QueryBlockBuilder(context, queryMethod); } - static QueryExecutionBlockBuilder executionBuilder(AotRepositoryMethodGenerationContext context) { - return new QueryExecutionBlockBuilder(context); + /** + * @param context + * @return new {@link QueryExecutionBlockBuilder}. + */ + static QueryExecutionBlockBuilder executionBuilder(AotQueryMethodGenerationContext context, + JpaQueryMethod queryMethod) { + return new QueryExecutionBlockBuilder(context, queryMethod); } /** @@ -60,13 +76,18 @@ static QueryExecutionBlockBuilder executionBuilder(AotRepositoryMethodGeneration */ static class QueryBlockBuilder { - private final AotRepositoryMethodGenerationContext context; + private final AotQueryMethodGenerationContext context; + private final JpaQueryMethod queryMethod; private String queryVariableName = "query"; private AotQueries queries; private MergedAnnotation queryHints = MergedAnnotation.missing(); + private MergedAnnotation query = MergedAnnotation.missing(); + private @Nullable String sqlResultSetMapping; + private @Nullable Class queryReturnType; - private QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { + private QueryBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { this.context = context; + this.queryMethod = queryMethod; } public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { @@ -86,6 +107,25 @@ public QueryBlockBuilder queryHints(MergedAnnotation queryHints) { return this; } + public QueryBlockBuilder query(MergedAnnotation query) { + + this.query = query; + return this; + } + + public QueryBlockBuilder nativeQuery(MergedAnnotation nativeQuery) { + + if (nativeQuery.isPresent()) { + this.sqlResultSetMapping = nativeQuery.getString("sqlResultSetMapping"); + } + return this; + } + + public QueryBlockBuilder queryReturnType(@Nullable Class queryReturnType) { + this.queryReturnType = queryReturnType; + return this; + } + /** * Build the query block. * @@ -93,29 +133,28 @@ public QueryBlockBuilder queryHints(MergedAnnotation queryHints) { */ public CodeBlock build() { - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); - Object actualReturnType = isProjecting ? context.getActualReturnType() + boolean isProjecting = context.getReturnedType().isProjecting(); + Class actualReturnType = isProjecting ? context.getActualReturnType().toClass() : context.getRepositoryInformation().getDomainType(); CodeBlock.Builder builder = CodeBlock.builder(); builder.add("\n"); - String queryStringNameVariableName = "%sString".formatted(queryVariableName); - StringAotQuery query = (StringAotQuery) queries.result(); - builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, query.getQueryString()); + String queryStringNameVariableName = null; + + if (queries.result() instanceof StringAotQuery sq) { + + queryStringNameVariableName = "%sString".formatted(queryVariableName); + builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, sq.getQueryString()); + } String countQueryStringNameVariableName = null; - String countQuyerVariableName = null; + String countQueryVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName)); - if (context.returnsPage()) { + if (queryMethod.isPageQuery() && queries.count() instanceof StringAotQuery sq) { countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName)); - countQuyerVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName)); - - StringAotQuery countQuery = (StringAotQuery) queries.count(); - builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, countQuery.getQueryString()); + builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, sq.getQueryString()); } // sorting @@ -126,23 +165,28 @@ public CodeBlock build() { sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); } - if (StringUtils.hasText(sortParameterName)) { + if (StringUtils.hasText(sortParameterName) && queries.result() instanceof StringAotQuery) { builder.add(applySorting(sortParameterName, queryStringNameVariableName, actualReturnType)); } - builder.add(createQuery(queryVariableName, queryStringNameVariableName, queries.result(), queryHints)); + if (queries.result().hasExpression() || queries.count().hasExpression()) { + builder.addStatement("class ExpressionMarker{}"); + } + + builder.add(createQuery(queryVariableName, queryStringNameVariableName, queries.result(), + this.sqlResultSetMapping, this.queryHints, this.queryReturnType)); - builder.add(applyLimits()); + builder.add(applyLimits(queries.result().isExists())); - if (StringUtils.hasText(countQueryStringNameVariableName)) { + if (queryMethod.isPageQuery()) { builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); - builder.add(createQuery(countQuyerVariableName, countQueryStringNameVariableName, queries.count(), - queryHints ? this.queryHints : MergedAnnotation.missing())); - builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName); + builder.add(createQuery(countQueryVariableName, countQueryStringNameVariableName, queries.count(), null, + queryHints ? this.queryHints : MergedAnnotation.missing(), Long.class)); + builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQueryVariableName); // end control flow does not work well with lambdas builder.unindent(); @@ -152,31 +196,26 @@ public CodeBlock build() { return builder.build(); } - private CodeBlock applySorting(String sort, String queryString, Object actualReturnType) { + private CodeBlock applySorting(String sort, String queryString, Class actualReturnType) { Builder builder = CodeBlock.builder(); builder.beginControlFlow("if ($L.isSorted())", sort); - if (queries.isNative()) { - builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class, - queryString); - } else { - builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class, + builder.addStatement("$T declaredQuery = $T.$L($L)", DeclaredQuery.class, DeclaredQuery.class, + queries.isNative() ? "nativeQuery" : "jpqlQuery", queryString); - } builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); - builder.endControlFlow(); return builder.build(); } - private CodeBlock applyLimits() { + private CodeBlock applyLimits(boolean exists) { Builder builder = CodeBlock.builder(); - if (context.isExistsMethod()) { + if (exists) { builder.addStatement("$L.setMaxResults(1)", queryVariableName); return builder.build(); @@ -198,7 +237,7 @@ private CodeBlock applyLimits() { builder.beginControlFlow("if ($L.isPaged())", pageable); builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName, pageable); - if (context.returnsSlice() && !context.returnsPage()) { + if (queryMethod.isSliceQuery()) { builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageable); } else { builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageable); @@ -209,14 +248,14 @@ private CodeBlock applyLimits() { return builder.build(); } - private CodeBlock createQuery(String queryVariableName, String queryStringNameVariableName, AotQuery query, - MergedAnnotation queryHints) { + private CodeBlock createQuery(String queryVariableName, @Nullable String queryStringNameVariableName, + AotQuery query, @Nullable String sqlResultSetMapping, MergedAnnotation queryHints, + @Nullable Class queryReturnType) { Builder builder = CodeBlock.builder(); - builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery", - queryStringNameVariableName); + builder.add( + doCreateQuery(queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping, queryReturnType)); if (queryHints.isPresent()) { builder.add(applyHints(queryVariableName, queryHints)); @@ -243,6 +282,55 @@ private CodeBlock createQuery(String queryVariableName, String queryStringNameVa return builder.build(); } + private CodeBlock doCreateQuery(String queryVariableName, @Nullable String queryStringNameVariableName, + AotQuery query, @Nullable String sqlResultSetMapping, @Nullable Class queryReturnType) { + + Builder builder = CodeBlock.builder(); + + if (query instanceof StringAotQuery) { + + if (StringUtils.hasText(sqlResultSetMapping)) { + + builder.addStatement("$T $L = this.$L.createNativeQuery($L, $S)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), queryStringNameVariableName, sqlResultSetMapping); + + return builder.build(); + } + + if (query.isNative() && queryReturnType != null) { + + builder.addStatement("$T $L = this.$L.createNativeQuery($L, $T.class)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), queryStringNameVariableName, queryReturnType); + + return builder.build(); + } + + builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery", + queryStringNameVariableName); + + return builder.build(); + } + + if (query instanceof NamedAotQuery nq) { + + if (queryReturnType != null) { + + builder.addStatement("$T $L = this.$L.createNamedQuery($S, $T.class)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), nq.getName(), queryReturnType); + + return builder.build(); + } + + builder.addStatement("$T $L = this.$L.createNamedQuery($S)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), nq.getName()); + + return builder.build(); + } + + throw new UnsupportedOperationException("Unsupported query type: " + query); + } + private Object getParameterName(ParameterBinding.BindingIdentifier identifier) { if (identifier.hasPosition()) { @@ -258,12 +346,30 @@ private Object getParameter(ParameterBinding.ParameterOrigin origin) { if (origin.isMethodArgument() && origin instanceof ParameterBinding.MethodInvocationArgument mia) { if (mia.identifier().hasPosition()) { - return context.getParameterNameOfPosition(mia.identifier().getPosition() - 1); + return context.getRequiredBindableParameterName(mia.identifier().getPosition() - 1); } if (mia.identifier().hasName()) { - return mia.identifier().getName(); + return context.getRequiredBindableParameterName(mia.identifier().getName()); + } + } + + if (origin.isExpression() && origin instanceof ParameterBinding.Expression expr) { + + Builder builder = CodeBlock.builder(); + ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); + String[] parameterNames = discoverer.getParameterNames(context.getMethod()); + + String expressionString = expr.expression().getExpressionString(); + // re-wrap expression + if (!expressionString.startsWith("$")) { + expressionString = "#{" + expressionString + "}"; } + + builder.add("evaluateExpression(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", expressionString, + StringUtils.arrayToCommaDelimitedString(parameterNames)); + + return builder.build(); } throw new UnsupportedOperationException("Not supported yet"); @@ -286,11 +392,15 @@ private CodeBlock applyHints(String queryVariableName, MergedAnnotation modifying = MergedAnnotation.missing(); - private QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + private QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { this.context = context; + this.queryMethod = queryMethod; } public QueryExecutionBlockBuilder referencing(String queryVariableName) { @@ -299,6 +409,18 @@ public QueryExecutionBlockBuilder referencing(String queryVariableName) { return this; } + public QueryExecutionBlockBuilder query(AotQuery aotQuery) { + + this.aotQuery = aotQuery; + return this; + } + + public QueryExecutionBlockBuilder modifying(MergedAnnotation modifying) { + + this.modifying = modifying; + return this; + } + public CodeBlock build() { Builder builder = CodeBlock.builder(); @@ -306,15 +428,44 @@ public CodeBlock build() { boolean isProjecting = context.getActualReturnType() != null && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), context.getActualReturnType()); - Object actualReturnType = isProjecting ? context.getActualReturnType() + Type actualReturnType = isProjecting ? context.getActualReturnType().getType() : context.getRepositoryInformation().getDomainType(); builder.add("\n"); - if (context.isDeleteMethod()) { + if (modifying.isPresent()) { + + if (modifying.getBoolean("flushAutomatically")) { + builder.addStatement("this.$L.flush()", context.fieldNameOf(EntityManager.class)); + } + + Class returnType = context.getMethod().getReturnType(); + + if (returnsModifying(returnType)) { + builder.addStatement("int result = $L.executeUpdate()", queryVariableName); + } else { + builder.addStatement("$L.executeUpdate()", queryVariableName); + } + + if (modifying.getBoolean("clearAutomatically")) { + builder.addStatement("this.$L.clear()", context.fieldNameOf(EntityManager.class)); + } + + if (returnType == int.class || returnType == long.class || returnType == Integer.class) { + builder.addStatement("return result"); + } + + if (returnType == Long.class) { + builder.addStatement("return (long) result"); + } + + return builder.build(); + } + + if (aotQuery != null && aotQuery.isDelete()) { builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class)); - if (context.returnsSingleValue()) { + if (!context.getReturnType().isAssignableFrom(List.class)) { if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType()); } else { @@ -323,22 +474,19 @@ public CodeBlock build() { } else { builder.addStatement("return resultList"); } - } else if (context.isExistsMethod()) { + } else if (aotQuery != null && aotQuery.isExists()) { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); } else { - if (context.returnsSingleValue()) { - if (context.returnsOptionalValue()) { - builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, - actualReturnType, queryVariableName); - } else { - builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnType(), queryVariableName); - } - } else if (context.returnsPage()) { + if (queryMethod.isCollectionQuery()) { + builder.addStatement("return ($T) query.getResultList()", context.getReturnTypeName()); + } else if (queryMethod.isStreamQuery()) { + builder.addStatement("return ($T) query.getResultStream()", context.getReturnTypeName()); + } else if (queryMethod.isPageQuery()) { builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, context.getPageableParameterName()); - } else if (context.returnsSlice()) { + } else if (queryMethod.isSliceQuery()) { builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", @@ -347,12 +495,24 @@ public CodeBlock build() { "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); } else { - builder.addStatement("return ($T) query.getResultList()", context.getReturnType()); + + if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { + builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, + actualReturnType, queryVariableName); + } else { + builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnTypeName(), + queryVariableName); + } } } return builder.build(); + } + + public static boolean returnsModifying(Class returnType) { + return returnType == int.class || returnType == long.class || returnType == Integer.class + || returnType == Long.class; } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java index 7f29a51f59..0b2522ce27 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-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. @@ -16,61 +16,77 @@ package org.springframework.data.jpa.repository.aot.generated; import jakarta.persistence.EntityManager; - +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Tuple; +import jakarta.persistence.TypedQueryReference; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.function.Function; -import java.util.regex.Pattern; +import java.util.function.UnaryOperator; + +import org.jspecify.annotations.Nullable; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.data.domain.KeysetScrollPosition; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.provider.QueryExtractor; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.EntityQuery; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaCountQueryCreator; import org.springframework.data.jpa.repository.query.JpaParameters; import org.springframework.data.jpa.repository.query.JpaQueryCreator; +import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider; -import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryImplementationMetadata; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata; +import org.springframework.data.repository.aot.generate.MethodContributor; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; -import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** + * JPA-specific {@link RepositoryContributor} contributing an AOT repository fragment using the {@link EntityManager} + * directly to run queries. + *

        + * The underlying {@link jakarta.persistence.metamodel.Metamodel} requires Hibernate to build metamodel information. + * * @author Christoph Strobl * @author Mark Paluch + * @since 4.0 */ public class JpaRepositoryContributor extends RepositoryContributor { - private final CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory(); - private final AotMetaModel metaModel; + private final AotMetamodel metaModel; + private final PersistenceProvider persistenceProvider; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); - - this.metaModel = new AotMetaModel(repositoryContext.getResolvedTypes()); + this.metaModel = new AotMetamodel(repositoryContext.getResolvedTypes()); + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(metaModel.getEntityManagerFactory()); } @Override - protected void customizeFile(RepositoryInformation information, AotRepositoryImplementationMetadata metadata, + protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata, TypeSpec.Builder builder) { builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class)); } @@ -88,103 +104,188 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } @Override - protected AotRepositoryMethodBuilder contributeRepositoryMethod( - AotRepositoryMethodGenerationContext generationContext) { + protected @Nullable MethodContributor contributeQueryMethod(Method method, + RepositoryInformation repositoryInformation) { + + JpaQueryMethod queryMethod = new JpaQueryMethod(method, repositoryInformation, getProjectionFactory(), + persistenceProvider); + // meh! QueryEnhancerSelector selector = QueryEnhancerSelector.DEFAULT_SELECTOR; // no stored procedures for now. - if (AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Procedure.class) != null) { + if (queryMethod.isProcedureQuery()) { + return null; + } + + // no KeysetScrolling for now. + if (queryMethod.getParameters().hasScrollPositionParameter()) { return null; } - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); - if (queryAnnotation != null) { - if (StringUtils.hasText(queryAnnotation.value()) - && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { + if (queryMethod.isModifyingQuery()) { + + Class returnType = repositoryInformation.getReturnType(method).getType(); + if (!ClassUtils.isVoidType(returnType) + && !JpaCodeBlocks.QueryExecutionBlockBuilder.returnsModifying(returnType)) { return null; } } - // no KeysetScrolling for now. - if (generationContext.getParameterNameOf(ScrollPosition.class) != null - || generationContext.getParameterNameOf(KeysetScrollPosition.class) != null) { - return null; - } + return MethodContributor.forQueryMethod(queryMethod).contribute(context -> { - // TODO: Named query via EntityManager, NamedQuery via properties, also for count queries. + CodeBlock.Builder body = CodeBlock.builder(); - return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { + MergedAnnotation query = context.getAnnotation(Query.class); + MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class); + MergedAnnotation queryHints = context.getAnnotation(QueryHints.class); + MergedAnnotation modifying = context.getAnnotation(Modifying.class); + ReturnedType returnedType = context.getReturnedType(); - MergedAnnotations annotations = MergedAnnotations.from(context.getMethod()); + body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - MergedAnnotation query = annotations.get(Query.class); - MergedAnnotation nativeQuery = annotations.get(NativeQuery.class); - MergedAnnotation queryHints = annotations.get(QueryHints.class); + AotQueries aotQueries = getQueries(context, query, selector, queryMethod, returnedType); - body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) + .queryReturnType(getQueryReturnType(aotQueries.result(), returnedType, context)).query(query) + .nativeQuery(nativeQuery).queryHints(queryHints).build()); - AotQueries aotQueries; - if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { - aotQueries = buildStringQuery(selector, query); - } else { - aotQueries = buildPartTreeQuery(context, query); - } + body.add( + JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build()); - body.addCode(JpaCodeBlocks.queryBuilder(context).filter(aotQueries).queryHints(queryHints).build()); - body.addCode(JpaCodeBlocks.executionBuilder(context).build()); + return body.build(); }); } - private AotQueries buildStringQuery(QueryEnhancerSelector selector, MergedAnnotation query) { + private AotQueries getQueries(AotQueryMethodGenerationContext context, MergedAnnotation query, + QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { + + if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { + return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query, + queryMethod); + } + + TypedQueryReference namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName()); + if (namedQuery != null) { + return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod); + } - Function queryFunction = query.getBoolean("nativeQuery") ? StringAotQuery::nativeQuery + return buildPartTreeQuery(returnedType, context, query, queryMethod); + } + + private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, + MergedAnnotation query, JpaQueryMethod queryMethod) { + + UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName()); + boolean isNative = query.getBoolean("nativeQuery"); + Function queryFunction = isNative ? StringAotQuery::nativeQuery : StringAotQuery::jpqlQuery; + queryFunction = operator.andThen(queryFunction); - StringAotQuery aotStringQuery = queryFunction.apply(query.getString("value")); + String queryString = query.getString("value"); + + StringAotQuery aotStringQuery = queryFunction.apply(queryString); String countQuery = query.getString("countQuery"); + EntityQuery entityQuery = EntityQuery.create(aotStringQuery.getQuery(), selector); + if (entityQuery.hasConstructorExpression() || entityQuery.isDefaultProjection()) { + aotStringQuery = aotStringQuery.withReturnsDeclaredMethodType(); + } + if (StringUtils.hasText(countQuery)) { return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery)); } + String namedCountQueryName = queryMethod.getNamedCountQueryName(); + TypedQueryReference namedCountQuery = getNamedQuery(returnedType, namedCountQueryName); + if (namedCountQuery != null) { + return AotQueries.from(aotStringQuery, buildNamedAotQuery(namedCountQuery, queryMethod, isNative)); + } + String countProjection = query.getString("countProjection"); return AotQueries.from(aotStringQuery, countProjection, selector); } - private AotQueries buildPartTreeQuery(AotRepositoryMethodGenerationContext context, MergedAnnotation query) { + private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector, + TypedQueryReference namedQuery, MergedAnnotation query, JpaQueryMethod queryMethod) { - PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); - // TODO make configurable - JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; + NamedAotQuery aotQuery = buildNamedAotQuery(namedQuery, queryMethod, + query.isPresent() && query.getBoolean("nativeQuery")); + + String countQuery = query.isPresent() ? query.getString("countQuery") : null; + if (StringUtils.hasText(countQuery)) { + return AotQueries.from(aotQuery, + aotQuery.isNative() ? StringAotQuery.nativeQuery(countQuery) : StringAotQuery.jpqlQuery(countQuery)); + } - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); + TypedQueryReference namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName()); - Class actualReturnType; - try { - actualReturnType = isProjecting - ? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader()) - : context.getRepositoryInformation().getDomainType(); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + if (namedCountQuery != null) { + return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, aotQuery.isNative())); } - ReturnedType returnedType = ReturnedType.of(actualReturnType, context.getRepositoryInformation().getDomainType(), - projectionFactory); + String countProjection = query.isPresent() ? query.getString("countProjection") : null; + return AotQueries.from(aotQuery, it -> { + return StringAotQuery.of(aotQuery.getQueryString()).getQuery(); + }, countProjection, selector); + } + + private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQueryMethod queryMethod, + boolean isNative) { - ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod()); - JpaParameters parameters = new JpaParameters(parametersSource); + QueryExtractor queryExtractor = queryMethod.getQueryExtractor(); + String queryString = queryExtractor.extractQueryString(namedQuery); - AotQuery partTreeQuery = createQuery(partTree, returnedType, parameters, templates); + if (!isNative) { + isNative = queryExtractor.isNativeQuery(namedQuery); + } + + Assert.hasText(queryString, () -> "Cannot extract Query from named query [%s]".formatted(namedQuery.getName())); + + return NamedAotQuery.named(namedQuery.getName(), + isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString)); + } + + private @Nullable TypedQueryReference getNamedQuery(ReturnedType returnedType, String queryName) { + + List> candidates = Arrays.asList(Object.class, returnedType.getDomainType(), + returnedType.getReturnedType(), returnedType.getTypeToRead(), void.class, null, Long.class, Integer.class, + Long.TYPE, Integer.TYPE, Number.class); + + EntityManagerFactory emf = metaModel.getEntityManagerFactory(); + + for (Class candidate : candidates) { + + Map> namedQueries = emf.getNamedQueries(candidate); + + if (namedQueries.containsKey(queryName)) { + return namedQueries.get(queryName); + } + } + + return null; + } + + private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context, + MergedAnnotation query, JpaQueryMethod queryMethod) { + + PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); + // TODO make configurable + JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; + + AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates); if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { - return AotQueries.from(partTreeQuery, StringAotQuery.jpqlQuery(query.getString("countQuery"))); + return AotQueries.from(aotQuery, StringAotQuery.jpqlQuery(query.getString("countQuery"))); } - AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, parameters, templates); - return AotQueries.from(partTreeQuery, partTreeCountQuery); + TypedQueryReference namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName()); + if (namedCountQuery != null) { + return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, false)); + } + + AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates); + return AotQueries.from(aotQuery, partTreeCountQuery); } private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, @@ -195,7 +296,7 @@ private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaPa JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metaModel); return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), - partTree.getResultLimit()); + partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection()); } private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, @@ -206,7 +307,34 @@ private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, metaModel); - return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), null); + return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), null, false, false); + } + + private static @Nullable Class getQueryReturnType(AotQuery query, ReturnedType returnedType, + AotQueryMethodGenerationContext context) { + + Method method = context.getMethod(); + RepositoryInformation repositoryInformation = context.getRepositoryInformation(); + + Class methodReturnType = repositoryInformation.getReturnedDomainClass(method); + boolean queryForEntity = repositoryInformation.getDomainType().isAssignableFrom(methodReturnType); + + Class result = queryForEntity ? returnedType.getDomainType() : null; + + if (query instanceof StringAotQuery sq && sq.returnsDeclaredMethodType()) { + return result; + } + + if (returnedType.isProjecting()) { + + if (returnedType.getReturnedType().isInterface()) { + return Tuple.class; + } + + return returnedType.getReturnedType(); + } + + return result; } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java new file mode 100644 index 0000000000..4df1c509ce --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java @@ -0,0 +1,63 @@ +/* + * 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.aot.generated; + +import java.util.List; + +import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.ParameterBinding; +import org.springframework.data.jpa.repository.query.PreprocessedQuery; + +/** + * Value object to describe a named AOT query. + * + * @author Mark Paluch + * @since 4.0 + */ +class NamedAotQuery extends AotQuery { + + private final String name; + private final DeclaredQuery queryString; + + private NamedAotQuery(String name, DeclaredQuery queryString, List parameterBindings) { + super(parameterBindings); + this.name = name; + this.queryString = queryString; + } + + /** + * Creates a new {@code NamedAotQuery}. + */ + public static NamedAotQuery named(String namedQuery, DeclaredQuery queryString) { + + PreprocessedQuery parsed = PreprocessedQuery.parse(queryString); + return new NamedAotQuery(namedQuery, queryString, parsed.getBindings()); + } + + public String getName() { + return name; + } + + public DeclaredQuery getQueryString() { + return queryString; + } + + @Override + public boolean isNative() { + return queryString.isNative(); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java index fb41cabcef..d68daa32bf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java @@ -40,10 +40,10 @@ private StringAotQuery(List parameterBindings) { static StringAotQuery of(DeclaredQuery query) { if (query instanceof PreprocessedQuery pq) { - return new DeclaredAotQuery(pq); + return new DeclaredAotQuery(pq, false); } - return new DeclaredAotQuery(PreprocessedQuery.parse(query)); + return new DeclaredAotQuery(PreprocessedQuery.parse(query), false); } /** @@ -57,8 +57,9 @@ static StringAotQuery jpqlQuery(String queryString) { /** * Creates a JPQL {@code StringAotQuery} using the given bindings and limit. */ - public static StringAotQuery jpqlQuery(String queryString, List bindings, Limit resultLimit) { - return new LimitedAotQuery(queryString, bindings, resultLimit); + public static StringAotQuery jpqlQuery(String queryString, List bindings, Limit resultLimit, + boolean delete, boolean exists) { + return new LimitedAotQuery(queryString, bindings, resultLimit, delete, exists); } /** @@ -78,6 +79,14 @@ public String getQueryString() { return getQuery().getQueryString(); } + /** + * @return {@literal true} if query is expected to return the declared method type directly; {@literal false} if the + * result requires projection post-processing. See also {@code NativeJpaQuery#getTypeToQueryFor}. + */ + public abstract boolean returnsDeclaredMethodType(); + + public abstract StringAotQuery withReturnsDeclaredMethodType(); + @Override public String toString() { return getQueryString(); @@ -90,10 +99,17 @@ public String toString() { static class DeclaredAotQuery extends StringAotQuery { private final PreprocessedQuery query; + private final boolean returnsDeclaredMethodType; - DeclaredAotQuery(PreprocessedQuery query) { + DeclaredAotQuery(PreprocessedQuery query, boolean returnsDeclaredMethodType) { super(query.getBindings()); this.query = query; + this.returnsDeclaredMethodType = returnsDeclaredMethodType; + } + + @Override + public PreprocessedQuery getQuery() { + return query; } @Override @@ -106,8 +122,14 @@ public boolean isNative() { return query.isNative(); } - public PreprocessedQuery getQuery() { - return query; + @Override + public boolean returnsDeclaredMethodType() { + return returnsDeclaredMethodType; + } + + @Override + public StringAotQuery withReturnsDeclaredMethodType() { + return new DeclaredAotQuery(query, returnsDeclaredMethodType); } } @@ -121,11 +143,16 @@ static class LimitedAotQuery extends StringAotQuery { private final String queryString; private final Limit limit; + private final boolean delete; + private final boolean exists; - LimitedAotQuery(String queryString, List parameterBindings, Limit limit) { + LimitedAotQuery(String queryString, List parameterBindings, Limit limit, boolean delete, + boolean exists) { super(parameterBindings); this.queryString = queryString; this.limit = limit; + this.delete = delete; + this.exists = exists; } @Override @@ -148,5 +175,25 @@ public Limit getLimit() { return limit; } + @Override + public boolean isDelete() { + return delete; + } + + @Override + public boolean isExists() { + return exists; + } + + @Override + public boolean returnsDeclaredMethodType() { + return true; + } + + @Override + public StringAotQuery withReturnsDeclaredMethodType() { + return this; + } + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java index b28fa9f10d..f827e0b291 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -28,7 +28,7 @@ * @author Diego Krupitza * @since 4.0 */ -interface EntityQuery extends ParametrizedQuery { +public interface EntityQuery extends ParametrizedQuery { /** * Create a new {@link EntityQuery} given {@link DeclaredQuery} and {@link QueryEnhancerSelector}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 961123b94e..338a2204e8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java @@ -25,9 +25,9 @@ import java.util.Map; import java.util.Optional; -import org.springframework.core.convert.ConversionService; - import org.jspecify.annotations.Nullable; + +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -225,6 +225,7 @@ static class ModifyingExecution extends JpaQueryExecution { private final EntityManager em; private final boolean flush; private final boolean clear; + private final JpaQueryMethod method; /** * Creates an execution that automatically flushes the given {@link EntityManager} before execution and/or clears @@ -233,6 +234,7 @@ static class ModifyingExecution extends JpaQueryExecution { * @param em Must not be {@literal null}. */ public ModifyingExecution(JpaQueryMethod method, EntityManager em) { + this.method = method; Assert.notNull(em, "The EntityManager must not be null"); @@ -240,8 +242,9 @@ public ModifyingExecution(JpaQueryMethod method, EntityManager em) { boolean isVoid = ClassUtils.isAssignable(returnType, Void.class); boolean isInt = ClassUtils.isAssignable(returnType, Integer.class); + boolean isLong = ClassUtils.isAssignable(returnType, Long.class); - Assert.isTrue(isInt || isVoid, + Assert.isTrue(isInt || isLong || isVoid, "Modifying queries can only use void or int/Integer as return type; Offending method: " + method); this.em = em; @@ -262,6 +265,10 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso em.clear(); } + if (ClassUtils.isAssignable(method.getReturnType(), Long.class)) { + return (long) result; + } + return result; } } 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 9a702d6464..10b985449d 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 @@ -232,7 +232,7 @@ boolean applyHintsToCountQuery() { * * @return */ - QueryExtractor getQueryExtractor() { + public QueryExtractor getQueryExtractor() { return extractor; } @@ -430,7 +430,7 @@ public String getNamedQueryName() { * * @return */ - String getNamedCountQueryName() { + public String getNamedCountQueryName() { String annotatedName = getAnnotationValue("countName", String.class); return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName() + ".count"; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java index fafd6fca4a..d4027be5ba 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java @@ -60,6 +60,8 @@ @NamedQueries({ // @NamedQuery(name = "User.findByEmailAddress", // query = "SELECT u FROM User u WHERE u.emailAddress = ?1"), // + @NamedQuery(name = "User.findByEmailAddress.count-provided", // + query = "SELECT count(u) FROM User u WHERE u.emailAddress = ?1"), // @NamedQuery(name = "User.findByNamedQueryWithAliasInInvertedOrder", // query = "SELECT u.lastname AS lastname, u.firstname AS firstname FROM User u ORDER BY u.lastname ASC"), @NamedQuery(name = "User.findByNamedQueryWithConstructorExpression", diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java index 15e2606118..3cecfff000 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java @@ -72,7 +72,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) .addConstructorArgReference("jpaSharedEM_entityManagerFactory") .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); - TestCompiler.forSystem().with(generationContext).compile(compiled -> { + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { beanFactory.setBeanClassLoader(compiled.getClassLoader()); ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository); }); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java index c61be724ad..ed91fab36f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java @@ -21,6 +21,7 @@ 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; @@ -48,6 +49,7 @@ class JpaRepositoryContributorIntegrationTests { @Autowired UserRepository fragment; @Autowired EntityManager em; + User luke, leia, han, chewbacca, yoda, vader, kylo; @Configuration static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { @@ -61,35 +63,69 @@ void beforeEach() { em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate(); - User luke = new User("Luke", "Skywalker", "luke@jedi.org"); + luke = new User("Luke", "Skywalker", "luke@jedi.org"); em.persist(luke); - User leia = new User("Leia", "Organa", "leia@resistance.gov"); + leia = new User("Leia", "Organa", "leia@resistance.gov"); em.persist(leia); - User han = new User("Han", "Solo", "han@smuggler.net"); + han = new User("Han", "Solo", "han@smuggler.net"); em.persist(han); - User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net"); + chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net"); em.persist(chewbacca); - User yoda = new User("Yoda", "n/a", "yoda@jedi.org"); + yoda = new User("Yoda", "n/a", "yoda@jedi.org"); em.persist(yoda); - User vader = new User("Anakin", "Skywalker", "vader@empire.com"); + vader = new User("Anakin", "Skywalker", "vader@empire.com"); em.persist(vader); - User kylo = new User("Ben", "Solo", "kylo@new-empire.com"); + kylo = new User("Ben", "Solo", "kylo@new-empire.com"); em.persist(kylo); } @Test - void testFindDerivedFinderSingleEntity() { + void testFindDerivedQuerySingleEntity() { + + User user = fragment.findOneByEmailAddress("luke@jedi.org"); + assertThat(user.getLastname()).isEqualTo("Skywalker"); + } + + @Test + void shouldUseNamedQuery() { User user = fragment.findByEmailAddress("luke@jedi.org"); assertThat(user.getLastname()).isEqualTo("Skywalker"); } + @Test + void shouldUseNamedQueryAndDeriveCountQuery() { + + Page user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); + + assertThat(user).hasSize(1); + assertThat(user.getTotalElements()).isEqualTo(1); + } + + @Test + void shouldUseNamedQueryAndProvidedCountQuery() { + + Page user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); + + assertThat(user).hasSize(1); + assertThat(user.getTotalElements()).isEqualTo(1); + } + + @Test + void shouldUseNamedQueryAndNamedCountQuery() { + + Page user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); + + assertThat(user).hasSize(1); + assertThat(user.getTotalElements()).isEqualTo(1); + } + @Test void testFindDerivedFinderOptionalEntity() { @@ -127,6 +163,14 @@ void testDerivedFinderReturningList() { "kylo@new-empire.com", "han@smuggler.net"); } + @Test + void shouldReturnStream() { + + Stream users = fragment.streamByLastnameLike("S%"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com", + "kylo@new-empire.com", "han@smuggler.net"); + } + @Test void testLimitedDerivedFinder() { @@ -303,6 +347,33 @@ void testAnnotatedFinderReturningSlice() { "kylo@new-empire.com"); } + @Test + void shouldResolveTemplatedQuery() { + + User user = fragment.findTemplatedByEmailAddress("han@smuggler.net"); + + assertThat(user).isNotNull(); + assertThat(user.getFirstname()).isEqualTo("Han"); + } + + @Test + void shouldEvaluateExpressionByName() { + + User user = fragment.findValueExpressionNamedByEmailAddress("han@smuggler.net"); + + assertThat(user).isNotNull(); + assertThat(user.getFirstname()).isEqualTo("Han"); + } + + @Test + void shouldEvaluateExpressionByPosition() { + + User user = fragment.findValueExpressionPositionalByEmailAddress("han@smuggler.net"); + + assertThat(user).isNotNull(); + assertThat(user.getFirstname()).isEqualTo("Han"); + } + @Test void testDerivedFinderReturningListOfProjections() { @@ -328,6 +399,7 @@ void testDerivedFinderReturningPageOfProjections() { assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + // TODO Page noResults = fragment.findUserProjectionByLastnameStartingWith("a", PageRequest.of(0, 2, Sort.by("emailAddress"))); } @@ -347,6 +419,19 @@ void testDerivedDeleteSingle() { assertThat(yodaShouldBeGone).isNull(); } + @Test + void shouldApplyModifying() { + + int affected = fragment.renameAllUsersTo("Jones"); + + assertThat(affected).isEqualTo(7); + + Object yodaShouldBeGone = em + .createQuery("SELECT u FROM %s u WHERE u.lastname = 'n/a'".formatted(User.class.getName())) + .getSingleResultOrNull(); + assertThat(yodaShouldBeGone).isNull(); + } + // native queries @Test @@ -359,22 +444,24 @@ void nativeQuery() { assertThat(page.getContent()).containsExactly("Anakin", "Ben"); } + @Test + void shouldApplySqlResultSetMapping() { + + User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId()); + + assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress()); + } + // old stuff below void todo() { - // expressions, templated query with #{#entityName} - // synthetic parameters (keyset scrolling! yuck!) // interface projections - // named queries // dynamic projections // class type parameter // entity graphs - // native queries - // delete - // @Modifying - // flush / clear + // synthetic parameters (keyset scrolling! yuck!) } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java index f766a3f164..98c3ac7a30 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; @@ -27,14 +28,17 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.repository.CrudRepository; /** * @author Christoph Strobl + * @author Mark Paluch */ -public interface UserRepository extends CrudRepository { +// TODO: Querydsl, query by example +interface UserRepository extends CrudRepository { List findUserNoArgumentsBy(); @@ -64,6 +68,8 @@ public interface UserRepository extends CrudRepository { Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); + Stream streamByLastnameLike(String lastname); + /* Annotated Queries */ @Query("select u from User u where u.emailAddress = ?1") @@ -105,6 +111,22 @@ public interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like ?1%") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + + // Value Expressions + + @Query("select u from #{#entityName} u where u.emailAddress = ?1") + User findTemplatedByEmailAddress(String emailAddress); + + @Query("select u from User u where u.emailAddress = :#{#emailAddress}") + User findValueExpressionNamedByEmailAddress(String emailAddress); + + @Query("select u from User u where u.emailAddress = ?#{[0]} or u.firstname = ?${user.dir}") + User findValueExpressionPositionalByEmailAddress(String emailAddress); + + @NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", + sqlResultSetMapping = "emailDto") + User.EmailDto findEmailDtoByNativeQuery(Integer id); + // modifying User deleteByEmailAddress(String username); @@ -145,6 +167,23 @@ public interface UserRepository extends CrudRepository { List findByLastnameOrderByFirstname(String lastname); + /** + * Retrieve users by their email address. The finder {@literal User.findByEmailAddress} is declared as annotation at + * {@code User}. + */ User findByEmailAddress(String emailAddress); + @Query(name = "User.findByEmailAddress") + Page findPagedByEmailAddress(Pageable pageable, String emailAddress); + + @Query(name = "User.findByEmailAddress", countQuery = "SELECT CoUnT(u) FROM User u WHERE u.emailAddress = ?1") + Page findPagedWithCountByEmailAddress(Pageable pageable, String emailAddress); + + @Query(name = "User.findByEmailAddress", countName = "User.findByEmailAddress.count-provided") + Page findPagedWithNamedCountByEmailAddress(Pageable pageable, String emailAddress); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("update User u set u.lastname = ?1") + int renameAllUsersTo(String lastname); + } 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 9f86810dc9..19e253484c 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 @@ -28,9 +28,9 @@ import java.util.Set; import java.util.stream.Stream; -import org.springframework.data.domain.Limit; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Limit; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -299,6 +299,10 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 List deleteByLastname(String lastname); + @Modifying + @Query("delete from User u where u.emailAddress = ?1") + User deleteAnnotatedQueryByEmailAddress(String username); + /** * Explicitly mapped to a procedure with name "plus1inout" in database. */ From c12bdb77fba4ede2020a21d03fa374f59334a573 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Mar 2025 12:02:07 +0100 Subject: [PATCH 59/94] Simplify package structure. See #3830 --- .../aot/{generated => }/AotMetamodel.java | 4 +- .../aot/{generated => }/AotQueries.java | 10 ++-- .../aot/{generated => }/AotQuery.java | 2 +- .../AotRepositoryFragmentSupport.java | 23 ++++---- .../aot/{generated => }/JpaCodeBlocks.java | 36 +++++-------- .../JpaRepositoryContributor.java | 53 +++++++++++++------ .../aot/{generated => }/NamedAotQuery.java | 2 +- .../aot/{generated => }/StringAotQuery.java | 2 +- .../data/jpa/repository/aot/package-info.java | 5 ++ .../config/JpaRepositoryConfigExtension.java | 2 +- .../AotFragmentTestConfigurationSupport.java | 2 +- ...RepositoryContributorIntegrationTests.java | 16 ++++-- .../StubRepositoryInformation.java | 4 +- .../TestJpaAotRepositoryContext.java | 4 +- .../{generated => }/UserDtoProjection.java | 2 +- .../aot/{generated => }/UserRepository.java | 5 +- .../query/JpaQueryExecutionUnitTests.java | 3 +- .../jpa/repository/sample/UserRepository.java | 4 -- 18 files changed, 101 insertions(+), 78 deletions(-) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/AotMetamodel.java (97%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/AotQueries.java (87%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/AotQuery.java (97%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/AotRepositoryFragmentSupport.java (81%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/JpaCodeBlocks.java (96%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/JpaRepositoryContributor.java (89%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/NamedAotQuery.java (96%) rename spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/{generated => }/StringAotQuery.java (98%) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/package-info.java rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/{generated => }/AotFragmentTestConfigurationSupport.java (98%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/{generated => }/JpaRepositoryContributorIntegrationTests.java (97%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/{generated => }/StubRepositoryInformation.java (96%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/{generated => }/TestJpaAotRepositoryContext.java (96%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/{generated => }/UserDtoProjection.java (94%) rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/{generated => }/UserRepository.java (98%) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java similarity index 97% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java index fcdd221cf9..8b68214ab5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java similarity index 87% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java index 14f94e625f..0b900c72a5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueries.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; -import jakarta.validation.constraints.Null; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.QueryEnhancer; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; @@ -35,7 +36,8 @@ record AotQueries(AotQuery result, AotQuery count) { /** * Derive a count query from the given query. */ - public static AotQueries from(StringAotQuery query, @Null String countProjection, QueryEnhancerSelector selector) { + public static AotQueries from(StringAotQuery query, @Nullable String countProjection, + QueryEnhancerSelector selector) { return from(query, StringAotQuery::getQuery, countProjection, selector); } @@ -43,7 +45,7 @@ public static AotQueries from(StringAotQuery query, @Null String countProjection * Derive a count query from the given query. */ public static AotQueries from(T query, Function queryMapper, - @Null String countProjection, QueryEnhancerSelector selector) { + @Nullable String countProjection, QueryEnhancerSelector selector) { DeclaredQuery underlyingQuery = queryMapper.apply(query); QueryEnhancer queryEnhancer = selector.select(underlyingQuery).create(underlyingQuery); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java similarity index 97% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java index 926fe45c4b..b9b3eeb1a6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import java.util.List; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java similarity index 81% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java index a20acf49f5..8c0abf97a8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotRepositoryFragmentSupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import java.lang.reflect.Method; @@ -32,6 +32,7 @@ import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.util.Lazy; import org.springframework.util.ConcurrentLruCache; /** @@ -48,11 +49,11 @@ public class AotRepositoryFragmentSupport { private final ProjectionFactory projectionFactory; - private final ConcurrentLruCache enhancers; + private final Lazy> enhancers; - private final ConcurrentLruCache expressions; + private final Lazy> expressions; - private final ConcurrentLruCache contextProviders; + private final Lazy> contextProviders; protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector, RepositoryFactoryBeanSupport.FragmentCreationContext context) { @@ -66,10 +67,10 @@ protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector, Repositor this.repositoryMetadata = repositoryMetadata; this.valueExpressions = valueExpressions; this.projectionFactory = projectionFactory; - this.enhancers = new ConcurrentLruCache<>(32, query -> selector.select(query).create(query)); - this.expressions = new ConcurrentLruCache<>(32, valueExpressions::parse); - this.contextProviders = new ConcurrentLruCache<>(32, it -> valueExpressions - .createValueContextProvider(new JpaParameters(ParametersSource.of(repositoryMetadata, it)))); + this.enhancers = Lazy.of(() -> new ConcurrentLruCache<>(32, query -> selector.select(query).create(query))); + this.expressions = Lazy.of(() -> new ConcurrentLruCache<>(32, valueExpressions::parse)); + this.contextProviders = Lazy.of(() -> new ConcurrentLruCache<>(32, it -> valueExpressions + .createValueContextProvider(new JpaParameters(ParametersSource.of(repositoryMetadata, it))))); } /** @@ -82,7 +83,7 @@ protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector, Repositor */ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class returnedType) { - QueryEnhancer queryStringEnhancer = this.enhancers.get(query); + QueryEnhancer queryStringEnhancer = this.enhancers.get().get(query); return queryStringEnhancer.rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(returnedType, repositoryMetadata.getDomainType(), projectionFactory))); } @@ -97,8 +98,8 @@ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class returnedT */ protected @Nullable Object evaluateExpression(Method method, String expressionString, Object... args) { - ValueExpression expression = this.expressions.get(expressionString); - ValueEvaluationContextProvider contextProvider = this.contextProviders.get(method); + ValueExpression expression = this.expressions.get().get(expressionString); + ValueEvaluationContextProvider contextProvider = this.contextProviders.get().get(method); return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies())); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java similarity index 96% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index e39e89327b..75e74a78e1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; @@ -55,7 +55,6 @@ class JpaCodeBlocks { /** - * @param context * @return new {@link QueryBlockBuilder}. */ public static QueryBlockBuilder queryBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { @@ -63,7 +62,6 @@ public static QueryBlockBuilder queryBuilder(AotQueryMethodGenerationContext con } /** - * @param context * @return new {@link QueryExecutionBlockBuilder}. */ static QueryExecutionBlockBuilder executionBuilder(AotQueryMethodGenerationContext context, @@ -79,9 +77,8 @@ static class QueryBlockBuilder { private final AotQueryMethodGenerationContext context; private final JpaQueryMethod queryMethod; private String queryVariableName = "query"; - private AotQueries queries; + private @Nullable AotQueries queries; private MergedAnnotation queryHints = MergedAnnotation.missing(); - private MergedAnnotation query = MergedAnnotation.missing(); private @Nullable String sqlResultSetMapping; private @Nullable Class queryReturnType; @@ -101,18 +98,6 @@ public QueryBlockBuilder filter(AotQueries query) { return this; } - public QueryBlockBuilder queryHints(MergedAnnotation queryHints) { - - this.queryHints = queryHints; - return this; - } - - public QueryBlockBuilder query(MergedAnnotation query) { - - this.query = query; - return this; - } - public QueryBlockBuilder nativeQuery(MergedAnnotation nativeQuery) { if (nativeQuery.isPresent()) { @@ -121,6 +106,12 @@ public QueryBlockBuilder nativeQuery(MergedAnnotation nativeQuery) return this; } + public QueryBlockBuilder queryHints(MergedAnnotation queryHints) { + + this.queryHints = queryHints; + return this; + } + public QueryBlockBuilder queryReturnType(@Nullable Class queryReturnType) { this.queryReturnType = queryReturnType; return this; @@ -142,7 +133,7 @@ public CodeBlock build() { String queryStringNameVariableName = null; - if (queries.result() instanceof StringAotQuery sq) { + if (queries != null && queries.result() instanceof StringAotQuery sq) { queryStringNameVariableName = "%sString".formatted(queryVariableName); builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, sq.getQueryString()); @@ -157,9 +148,6 @@ public CodeBlock build() { builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, sq.getQueryString()); } - // sorting - // TODO: refactor into sort builder - String sortParameterName = context.getSortParameterName(); if (sortParameterName == null && context.getPageableParameterName() != null) { sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); @@ -202,7 +190,7 @@ private CodeBlock applySorting(String sort, String queryString, Class actualR builder.beginControlFlow("if ($L.isSorted())", sort); builder.addStatement("$T declaredQuery = $T.$L($L)", DeclaredQuery.class, DeclaredQuery.class, - queries.isNative() ? "nativeQuery" : "jpqlQuery", + queries != null && queries.isNative() ? "nativeQuery" : "jpqlQuery", queryString); builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); @@ -227,7 +215,7 @@ private CodeBlock applyLimits(boolean exists) { builder.beginControlFlow("if ($L.isLimited())", limit); builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limit); builder.endControlFlow(); - } else if (queries.result().isLimited()) { + } else if (queries != null && queries.result().isLimited()) { builder.addStatement("$L.setMaxResults($L)", queryVariableName, queries.result().getLimit().max()); } @@ -358,7 +346,7 @@ private Object getParameter(ParameterBinding.ParameterOrigin origin) { Builder builder = CodeBlock.builder(); ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); - String[] parameterNames = discoverer.getParameterNames(context.getMethod()); + var parameterNames = discoverer.getParameterNames(context.getMethod()); String expressionString = expr.expression().getExpressionString(); // re-wrap expression diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java similarity index 89% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 0b2522ce27..1cacad6536 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-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. @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Tuple; import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.metamodel.Metamodel; import java.lang.reflect.Method; import java.util.Arrays; @@ -57,6 +58,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; @@ -76,13 +78,23 @@ */ public class JpaRepositoryContributor extends RepositoryContributor { - private final AotMetamodel metaModel; + private final EntityManagerFactory emf; + private final Metamodel metaModel; private final PersistenceProvider persistenceProvider; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); - this.metaModel = new AotMetamodel(repositoryContext.getResolvedTypes()); - this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(metaModel.getEntityManagerFactory()); + AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes()); + this.metaModel = amm; + this.emf = amm.getEntityManagerFactory(); + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory()); + } + + public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { + super(repositoryContext); + this.emf = entityManagerFactory; + this.metaModel = entityManagerFactory.getMetamodel(); + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); } @Override @@ -118,6 +130,17 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB return null; } + ReturnedType returnedType = queryMethod.getResultProcessor().getReturnedType(); + + // no interface/dynamic projections for now. + if (returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) { + return null; + } + + if (queryMethod.getParameters().hasDynamicProjection()) { + return null; + } + // no KeysetScrolling for now. if (queryMethod.getParameters().hasScrollPositionParameter()) { return null; @@ -125,9 +148,13 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB if (queryMethod.isModifyingQuery()) { - Class returnType = repositoryInformation.getReturnType(method).getType(); - if (!ClassUtils.isVoidType(returnType) - && !JpaCodeBlocks.QueryExecutionBlockBuilder.returnsModifying(returnType)) { + TypeInformation returnType = repositoryInformation.getReturnType(method); + + boolean returnsCount = JpaCodeBlocks.QueryExecutionBlockBuilder.returnsModifying(returnType.getType()); + + boolean isVoid = ClassUtils.isVoidType(returnType.getType()); + + if (!returnsCount && !isVoid) { return null; } } @@ -140,15 +167,14 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class); MergedAnnotation queryHints = context.getAnnotation(QueryHints.class); MergedAnnotation modifying = context.getAnnotation(Modifying.class); - ReturnedType returnedType = context.getReturnedType(); body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); AotQueries aotQueries = getQueries(context, query, selector, queryMethod, returnedType); body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) - .queryReturnType(getQueryReturnType(aotQueries.result(), returnedType, context)).query(query) - .nativeQuery(nativeQuery).queryHints(queryHints).build()); + .queryReturnType(getQueryReturnType(aotQueries.result(), returnedType, context)).nativeQuery(nativeQuery) + .queryHints(queryHints).build()); body.add( JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build()); @@ -178,8 +204,7 @@ private AotQueries buildStringQuery(Class domainType, ReturnedType returnedTy UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName()); boolean isNative = query.getBoolean("nativeQuery"); - Function queryFunction = isNative ? StringAotQuery::nativeQuery - : StringAotQuery::jpqlQuery; + Function queryFunction = isNative ? StringAotQuery::nativeQuery : StringAotQuery::jpqlQuery; queryFunction = operator.andThen(queryFunction); String queryString = query.getString("value"); @@ -252,8 +277,6 @@ private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQ returnedType.getReturnedType(), returnedType.getTypeToRead(), void.class, null, Long.class, Integer.class, Long.TYPE, Integer.TYPE, Number.class); - EntityManagerFactory emf = metaModel.getEntityManagerFactory(); - for (Class candidate : candidates) { Map> namedQueries = emf.getNamedQueries(candidate); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java similarity index 96% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java index 4df1c509ce..3f7b9293bb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/NamedAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import java.util.List; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java similarity index 98% rename from spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java rename to spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java index d68daa32bf..499f1d6c6f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import java.util.List; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/package-info.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/package-info.java new file mode 100644 index 0000000000..a0fa7b10f2 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/package-info.java @@ -0,0 +1,5 @@ +/** + * Ahead-of-Time (AOT) generation for Spring Data JPA repositories. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.data.jpa.repository.aot; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 742387add6..7de820f3e9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -52,7 +52,7 @@ import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.data.aot.AotContext; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.aot.generated.JpaRepositoryContributor; +import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.DefaultJpaContext; import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor; import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java similarity index 98% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java index 3cecfff000..670c871caa 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/AotFragmentTestConfigurationSupport.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java similarity index 97% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java index ed91fab36f..d6e0edebb3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import static org.assertj.core.api.Assertions.*; @@ -399,9 +399,10 @@ void testDerivedFinderReturningPageOfProjections() { assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); - // TODO Page noResults = fragment.findUserProjectionByLastnameStartingWith("a", PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(noResults).isEmpty(); } // modifying @@ -419,6 +420,13 @@ void testDerivedDeleteSingle() { assertThat(yodaShouldBeGone).isNull(); } + @Test + void shouldOmitAnnotatedDeleteReturningDomainType() { + + assertThatException().isThrownBy(() -> fragment.deleteAnnotatedQueryByEmailAddress("foo")) + .withRootCauseInstanceOf(NoSuchMethodException.class); + } + @Test void shouldApplyModifying() { @@ -456,11 +464,11 @@ void shouldApplySqlResultSetMapping() { void todo() { + // entity graphs // interface projections // dynamic projections // class type parameter - // entity graphs // synthetic parameters (keyset scrolling! yuck!) } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java similarity index 96% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java index e90ce0aae2..6e9b1d900c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import java.lang.reflect.Method; import java.util.Set; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java similarity index 96% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepositoryContext.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index df4e62a873..0aeaba3644 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import jakarta.persistence.Entity; import jakarta.persistence.MappedSuperclass; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserDtoProjection.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserDtoProjection.java similarity index 94% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserDtoProjection.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserDtoProjection.java index bc8d8f578a..3e8e974500 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserDtoProjection.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserDtoProjection.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; /** * @author Christoph Strobl diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java similarity index 98% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index 98c3ac7a30..9664faaeab 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.aot.generated; +package org.springframework.data.jpa.repository.aot; import jakarta.persistence.QueryHint; @@ -131,8 +131,7 @@ interface UserRepository extends CrudRepository { User deleteByEmailAddress(String username); - Long deleteReturningDeleteCountByEmailAddress(String username); - + // cannot generate delete and return a domain object @Modifying @Query("delete from User u where u.emailAddress = ?1") User deleteAnnotatedQueryByEmailAddress(String username); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java index 6d93f6ae9f..e8907f16fc 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java @@ -24,6 +24,7 @@ import jakarta.persistence.TypedQuery; import java.lang.reflect.Method; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import java.util.Optional; @@ -169,7 +170,7 @@ void allowsMethodReturnTypesForModifyingQuery() { @Test void modifyingExecutionRejectsNonIntegerOrVoidReturnType() { - when(method.getReturnType()).thenReturn((Class) Long.class); + when(method.getReturnType()).thenReturn((Class) BigDecimal.class); assertThatIllegalArgumentException().isThrownBy(() -> new ModifyingExecution(method, em)); } 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 19e253484c..3c6d2ce4ea 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 @@ -299,10 +299,6 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 List deleteByLastname(String lastname); - @Modifying - @Query("delete from User u where u.emailAddress = ?1") - User deleteAnnotatedQueryByEmailAddress(String username); - /** * Explicitly mapped to a procedure with name "plus1inout" in database. */ From 1b242dd8584b651eb48fa0b0d28781f5b90d9536 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 2 Apr 2025 14:11:38 +0200 Subject: [PATCH 60/94] Add support for Entity Graphs. See #3830 --- .../jpa/repository/aot/AotEntityGraph.java | 31 ++++++++ .../jpa/repository/aot/JpaCodeBlocks.java | 57 +++++++++++++-- .../aot/JpaRepositoryContributor.java | 72 ++++++++++++++++++- ...RepositoryContributorIntegrationTests.java | 37 +++++++++- .../jpa/repository/aot/UserRepository.java | 10 ++- 5 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityGraph.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityGraph.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityGraph.java new file mode 100644 index 0000000000..388c041cb4 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotEntityGraph.java @@ -0,0 +1,31 @@ +/* + * 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.aot; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.data.jpa.repository.EntityGraph; + +/** + * AOT representation of an resolved entity graph. The graph can be either named or defined by attribute paths in case + * the named entity graph cannot be looked up. + * + * @author Mark Paluch + */ +record AotEntityGraph(@Nullable String name, EntityGraph.EntityGraphType type, List attributePaths) { +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index 75e74a78e1..7b906e937b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -79,6 +79,7 @@ static class QueryBlockBuilder { private String queryVariableName = "query"; private @Nullable AotQueries queries; private MergedAnnotation queryHints = MergedAnnotation.missing(); + private @Nullable AotEntityGraph entityGraph; private @Nullable String sqlResultSetMapping; private @Nullable Class queryReturnType; @@ -112,6 +113,11 @@ public QueryBlockBuilder queryHints(MergedAnnotation queryHints) { return this; } + public QueryBlockBuilder entityGraph(@Nullable AotEntityGraph entityGraph) { + this.entityGraph = entityGraph; + return this; + } + public QueryBlockBuilder queryReturnType(@Nullable Class queryReturnType) { this.queryReturnType = queryReturnType; return this; @@ -162,7 +168,7 @@ public CodeBlock build() { } builder.add(createQuery(queryVariableName, queryStringNameVariableName, queries.result(), - this.sqlResultSetMapping, this.queryHints, this.queryReturnType)); + this.sqlResultSetMapping, this.queryHints, this.entityGraph, this.queryReturnType)); builder.add(applyLimits(queries.result().isExists())); @@ -173,7 +179,7 @@ public CodeBlock build() { boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); builder.add(createQuery(countQueryVariableName, countQueryStringNameVariableName, queries.count(), null, - queryHints ? this.queryHints : MergedAnnotation.missing(), Long.class)); + queryHints ? this.queryHints : MergedAnnotation.missing(), null, Long.class)); builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQueryVariableName); // end control flow does not work well with lambdas @@ -190,8 +196,7 @@ private CodeBlock applySorting(String sort, String queryString, Class actualR builder.beginControlFlow("if ($L.isSorted())", sort); builder.addStatement("$T declaredQuery = $T.$L($L)", DeclaredQuery.class, DeclaredQuery.class, - queries != null && queries.isNative() ? "nativeQuery" : "jpqlQuery", - queryString); + queries != null && queries.isNative() ? "nativeQuery" : "jpqlQuery", queryString); builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); builder.endControlFlow(); @@ -238,13 +243,17 @@ private CodeBlock applyLimits(boolean exists) { private CodeBlock createQuery(String queryVariableName, @Nullable String queryStringNameVariableName, AotQuery query, @Nullable String sqlResultSetMapping, MergedAnnotation queryHints, - @Nullable Class queryReturnType) { + @Nullable AotEntityGraph entityGraph, @Nullable Class queryReturnType) { Builder builder = CodeBlock.builder(); builder.add( doCreateQuery(queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping, queryReturnType)); + if (entityGraph != null) { + builder.add(applyEntityGraph(entityGraph, queryVariableName)); + } + if (queryHints.isPresent()) { builder.add(applyHints(queryVariableName, queryHints)); builder.add("\n"); @@ -363,6 +372,43 @@ private Object getParameter(ParameterBinding.ParameterOrigin origin) { throw new UnsupportedOperationException("Not supported yet"); } + private CodeBlock applyEntityGraph(AotEntityGraph entityGraph, String queryVariableName) { + + CodeBlock.Builder builder = CodeBlock.builder(); + + if (StringUtils.hasText(entityGraph.name())) { + + builder.addStatement("$T entityGraph = $L.getEntityGraph($S)", jakarta.persistence.EntityGraph.class, + context.fieldNameOf(EntityManager.class), entityGraph.name()); + } else { + + builder.addStatement("$T<$T> entityGraph = $L.createEntityGraph($T.class)", + jakarta.persistence.EntityGraph.class, context.getActualReturnType().getType(), + context.fieldNameOf(EntityManager.class), context.getActualReturnType().getType()); + + for (String attributePath : entityGraph.attributePaths()) { + + String[] pathComponents = StringUtils.delimitedListToStringArray(attributePath, "."); + + StringBuilder chain = new StringBuilder("entityGraph"); + for (int i = 0; i < pathComponents.length; i++) { + + if (i < pathComponents.length - 1) { + chain.append(".addSubgraph($S)"); + } else { + chain.append(".addAttributeNodes($S)"); + } + } + + builder.addStatement(chain.toString(), (Object[]) pathComponents); + } + + builder.addStatement("$L.setHint($S, entityGraph)", queryVariableName, entityGraph.type().getKey()); + } + + return builder.build(); + } + private CodeBlock applyHints(String queryVariableName, MergedAnnotation queryHints) { Builder hintsBuilder = CodeBlock.builder(); @@ -505,5 +551,4 @@ public static boolean returnsModifying(Class returnType) { } - } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 1cacad6536..732441f3a9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jpa.repository.aot; +import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Tuple; @@ -23,16 +24,22 @@ import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; @@ -166,15 +173,17 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB MergedAnnotation query = context.getAnnotation(Query.class); MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class); MergedAnnotation queryHints = context.getAnnotation(QueryHints.class); + MergedAnnotation entityGraph = context.getAnnotation(EntityGraph.class); MergedAnnotation modifying = context.getAnnotation(Modifying.class); body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); AotQueries aotQueries = getQueries(context, query, selector, queryMethod, returnedType); + AotEntityGraph aotEntityGraph = getAotEntityGraph(entityGraph, repositoryInformation, returnedType, queryMethod); body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) .queryReturnType(getQueryReturnType(aotQueries.result(), returnedType, context)).nativeQuery(nativeQuery) - .queryHints(queryHints).build()); + .queryHints(queryHints).entityGraph(aotEntityGraph).build()); body.add( JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build()); @@ -360,4 +369,65 @@ private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, return result; } + @SuppressWarnings("unchecked") + private @Nullable AotEntityGraph getAotEntityGraph(MergedAnnotation entityGraph, + RepositoryInformation information, ReturnedType returnedType, JpaQueryMethod queryMethod) { + + if (!entityGraph.isPresent()) { + return null; + } + + EntityGraph.EntityGraphType type = entityGraph.getEnum("type", EntityGraph.EntityGraphType.class); + String[] attributePaths = entityGraph.getStringArray("attributePaths"); + Collection entityGraphNames = getEntityGraphNames(entityGraph, information, queryMethod); + List> candidates = Arrays.asList(returnedType.getDomainType(), returnedType.getReturnedType(), + returnedType.getTypeToRead()); + + for (Class candidate : candidates) { + + Map> namedEntityGraphs = emf + .getNamedEntityGraphs(Class.class.cast(candidate)); + + if (namedEntityGraphs.isEmpty()) { + continue; + } + + for (String entityGraphName : entityGraphNames) { + if (namedEntityGraphs.containsKey(entityGraphName)) { + return new AotEntityGraph(entityGraphName, type, Collections.emptyList()); + } + } + } + + if (attributePaths.length > 0) { + return new AotEntityGraph(null, type, Arrays.asList(attributePaths)); + } + + return null; + } + + private Set getEntityGraphNames(MergedAnnotation entityGraph, RepositoryInformation information, + JpaQueryMethod queryMethod) { + + Set entityGraphNames = new LinkedHashSet<>(); + String value = entityGraph.getString("value"); + + if (StringUtils.hasText(value)) { + entityGraphNames.add(value); + } + entityGraphNames.add(queryMethod.getNamedQueryName()); + entityGraphNames.add(getFallbackEntityGraphName(information, queryMethod)); + return entityGraphNames; + } + + private String getFallbackEntityGraphName(RepositoryInformation information, JpaQueryMethod queryMethod) { + + Class domainType = information.getDomainType(); + Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class); + String entityName = entity != null && StringUtils.hasText(entity.name()) ? entity.name() + : domainType.getSimpleName(); + + return entityName + "." + queryMethod.getName(); + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java index d6e0edebb3..5f609bad6d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java @@ -23,6 +23,7 @@ import java.util.Optional; import java.util.stream.Stream; +import org.hibernate.proxy.HibernateProxy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,6 +34,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.annotation.Transactional; @@ -50,6 +52,7 @@ class JpaRepositoryContributorIntegrationTests { @Autowired UserRepository fragment; @Autowired EntityManager em; User luke, leia, han, chewbacca, yoda, vader, kylo; + Role smuggler, jedi, imperium; @Configuration static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { @@ -62,17 +65,26 @@ public JpaRepositoryContributorConfiguration() { void beforeEach() { em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate(); + em.createQuery("DELETE FROM %s".formatted(Role.class.getName())).executeUpdate(); + + smuggler = em.merge(new Role("Smuggler")); + jedi = em.merge(new Role("Jedi")); + imperium = em.merge(new Role("Imperium")); luke = new User("Luke", "Skywalker", "luke@jedi.org"); + luke.addRole(jedi); em.persist(luke); leia = new User("Leia", "Organa", "leia@resistance.gov"); em.persist(leia); han = new User("Han", "Solo", "han@smuggler.net"); + han.setManager(luke); em.persist(han); chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net"); + chewbacca.setManager(han); + chewbacca.addRole(smuggler); em.persist(chewbacca); yoda = new User("Yoda", "n/a", "yoda@jedi.org"); @@ -83,6 +95,9 @@ void beforeEach() { kylo = new User("Ben", "Solo", "kylo@new-empire.com"); em.persist(kylo); + + em.flush(); + em.clear(); } @Test @@ -388,6 +403,27 @@ void shouldApplyQueryHints() { .withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo"); } + @Test + void shouldApplyNamedEntityGraph() { + + User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca"); + + assertThat(chewie.getManager()).isInstanceOf(HibernateProxy.class); + assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); + } + + @Test + void shouldApplyDeclaredEntityGraph() { + + User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca"); + + assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); + + User han = chewie.getManager(); + assertThat(han.getRoles()).isNotInstanceOf(HibernateProxy.class); + assertThat(han.getManager()).isInstanceOf(HibernateProxy.class); + } + @Test void testDerivedFinderReturningPageOfProjections() { @@ -464,7 +500,6 @@ void shouldApplySqlResultSetMapping() { void todo() { - // entity graphs // interface projections // dynamic projections // class type parameter diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index 9664faaeab..1326960dbe 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -27,6 +27,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; @@ -111,7 +112,6 @@ interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like ?1%") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); - // Value Expressions @Query("select u from #{#entityName} u where u.emailAddress = ?1") @@ -139,7 +139,7 @@ interface UserRepository extends CrudRepository { // native queries @Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User", - nativeQuery = true) + nativeQuery = true) Page findByNativeQueryWithPageable(Pageable pageable); // projections @@ -158,6 +158,12 @@ interface UserRepository extends CrudRepository { @QueryHints(value = { @QueryHint(name = "jakarta.persistence.cache.storeMode", value = "foo") }, forCounting = false) List findHintedByLastname(String lastname); + @EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "User.overview") + User findWithNamedEntityGraphByFirstname(String firstname); + + @EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = { "roles", "manager.roles" }) + User findWithDeclaredEntityGraphByFirstname(String firstname); + List findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit); List findByLastname(String lastname, Sort sort); From 8ec2b60fef1debf140511f80df014dc19b423fba Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 11:43:42 +0200 Subject: [PATCH 61/94] Add support for projections. See #3830 --- .../data/jpa/repository/aot/AotQuery.java | 3 + .../aot/AotRepositoryFragmentSupport.java | 53 +++ .../jpa/repository/aot/EntityGraphLookup.java | 114 +++++++ .../jpa/repository/aot/JpaCodeBlocks.java | 174 +++++++--- .../aot/JpaRepositoryContributor.java | 304 +----------------- .../jpa/repository/aot/QueriesFactory.java | 268 +++++++++++++++ .../jpa/repository/aot/StringAotQuery.java | 51 ++- .../repository/query/AbstractJpaQuery.java | 187 +---------- .../data/jpa/util/TupleBackedMap.java | 219 +++++++++++++ ...RepositoryContributorIntegrationTests.java | 270 +++++++++++----- .../jpa/repository/aot/UserRepository.java | 115 +++++-- 11 files changed, 1120 insertions(+), 638 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/util/TupleBackedMap.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java index b9b3eeb1a6..6bf3a5186d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQuery.java @@ -40,6 +40,9 @@ abstract class AotQuery { */ public abstract boolean isNative(); + /** + * @return the list of parameter bindings. + */ public List getParameterBindings() { return parameterBindings; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java index 8c0abf97a8..f5c9d16edb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotRepositoryFragmentSupport.java @@ -15,10 +15,16 @@ */ package org.springframework.data.jpa.repository.aot; +import jakarta.persistence.Tuple; + import java.lang.reflect.Method; +import java.util.Collection; +import java.util.stream.Stream; import org.jspecify.annotations.Nullable; +import org.springframework.core.CollectionFactory; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; @@ -26,6 +32,7 @@ import org.springframework.data.jpa.repository.query.JpaParameters; import org.springframework.data.jpa.repository.query.QueryEnhancer; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.data.jpa.util.TupleBackedMap; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -104,6 +111,52 @@ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class returnedT return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies())); } + protected @Nullable T convertOne(@Nullable Object result, boolean nativeQuery, Class projection) { + + if (result == null) { + return null; + } + + if (projection.isInstance(result)) { + return projection.cast(result); + } + + return projectionFactory.createProjection(projection, + result instanceof Tuple t ? new TupleBackedMap(nativeQuery ? TupleBackedMap.underscoreAware(t) : t) : result); + } + + protected @Nullable Object convertMany(@Nullable Object result, boolean nativeQuery, Class projection) { + + if (result == null) { + return null; + } + + if (projection.isInstance(result)) { + return result; + } + + if (result instanceof Stream stream) { + return stream.map(it -> convertOne(it, nativeQuery, projection)); + } + + if (result instanceof Slice slice) { + return slice.map(it -> convertOne(it, nativeQuery, projection)); + } + + if (result instanceof Collection collection) { + + Collection<@Nullable Object> target = CollectionFactory.createCollection(collection.getClass(), + collection.size()); + for (Object o : collection) { + target.add(convertOne(o, nativeQuery, projection)); + } + + return target; + } + + throw new UnsupportedOperationException("Cannot create projection for %s".formatted(result)); + } + private record DefaultQueryRewriteInformation(Sort sort, ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java new file mode 100644 index 0000000000..7e715f9e24 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/EntityGraphLookup.java @@ -0,0 +1,114 @@ +/* + * 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.aot; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.query.JpaQueryMethod; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.util.StringUtils; + +/** + * Factory for {@link AotEntityGraph}. + * + * @author Mark Paluch + * @since 4.0 + */ +class EntityGraphLookup { + + private final EntityManagerFactory entityManagerFactory; + + public EntityGraphLookup(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + @SuppressWarnings("unchecked") + public @Nullable AotEntityGraph findEntityGraph(MergedAnnotation entityGraph, + RepositoryInformation information, ReturnedType returnedType, JpaQueryMethod queryMethod) { + + if (!entityGraph.isPresent()) { + return null; + } + + EntityGraph.EntityGraphType type = entityGraph.getEnum("type", EntityGraph.EntityGraphType.class); + String[] attributePaths = entityGraph.getStringArray("attributePaths"); + Collection entityGraphNames = getEntityGraphNames(entityGraph, information, queryMethod); + List> candidates = Arrays.asList(returnedType.getDomainType(), returnedType.getReturnedType(), + returnedType.getTypeToRead()); + + for (Class candidate : candidates) { + + Map> namedEntityGraphs = entityManagerFactory + .getNamedEntityGraphs(Class.class.cast(candidate)); + + if (namedEntityGraphs.isEmpty()) { + continue; + } + + for (String entityGraphName : entityGraphNames) { + if (namedEntityGraphs.containsKey(entityGraphName)) { + return new AotEntityGraph(entityGraphName, type, Collections.emptyList()); + } + } + } + + if (attributePaths.length > 0) { + return new AotEntityGraph(null, type, Arrays.asList(attributePaths)); + } + + return null; + } + + private Set getEntityGraphNames(MergedAnnotation entityGraph, RepositoryInformation information, + JpaQueryMethod queryMethod) { + + Set entityGraphNames = new LinkedHashSet<>(); + String value = entityGraph.getString("value"); + + if (StringUtils.hasText(value)) { + entityGraphNames.add(value); + } + entityGraphNames.add(queryMethod.getNamedQueryName()); + entityGraphNames.add(getFallbackEntityGraphName(information, queryMethod)); + return entityGraphNames; + } + + private String getFallbackEntityGraphName(RepositoryInformation information, JpaQueryMethod queryMethod) { + + Class domainType = information.getDomainType(); + Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class); + String entityName = entity != null && StringUtils.hasText(entity.name()) ? entity.name() + : domainType.getSimpleName(); + + return entityName + "." + queryMethod.getName(); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index 7b906e937b..8dba827102 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -18,6 +18,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import jakarta.persistence.QueryHint; +import jakarta.persistence.Tuple; import java.lang.reflect.Type; import java.util.List; @@ -30,6 +31,7 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.QueryHints; @@ -37,6 +39,7 @@ import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.ParameterBinding; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; @@ -134,6 +137,11 @@ public CodeBlock build() { Class actualReturnType = isProjecting ? context.getActualReturnType().toClass() : context.getRepositoryInformation().getDomainType(); + String dynamicReturnType = null; + if (queryMethod.getParameters().hasDynamicProjection()) { + dynamicReturnType = context.getParameterName(queryMethod.getParameters().getDynamicProjectionIndex()); + } + CodeBlock.Builder builder = CodeBlock.builder(); builder.add("\n"); @@ -159,15 +167,16 @@ public CodeBlock build() { sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); } - if (StringUtils.hasText(sortParameterName) && queries.result() instanceof StringAotQuery) { - builder.add(applySorting(sortParameterName, queryStringNameVariableName, actualReturnType)); + if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType)) + && queries.result() instanceof StringAotQuery) { + builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringNameVariableName, actualReturnType)); } if (queries.result().hasExpression() || queries.count().hasExpression()) { builder.addStatement("class ExpressionMarker{}"); } - builder.add(createQuery(queryVariableName, queryStringNameVariableName, queries.result(), + builder.add(createQuery(false, queryVariableName, queryStringNameVariableName, queries.result(), this.sqlResultSetMapping, this.queryHints, this.entityGraph, this.queryReturnType)); builder.add(applyLimits(queries.result().isExists())); @@ -178,7 +187,7 @@ public CodeBlock build() { boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); - builder.add(createQuery(countQueryVariableName, countQueryStringNameVariableName, queries.count(), null, + builder.add(createQuery(true, countQueryVariableName, countQueryStringNameVariableName, queries.count(), null, queryHints ? this.queryHints : MergedAnnotation.missing(), null, Long.class)); builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQueryVariableName); @@ -190,16 +199,33 @@ public CodeBlock build() { return builder.build(); } - private CodeBlock applySorting(String sort, String queryString, Class actualReturnType) { + private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, String queryString, + Class actualReturnType) { Builder builder = CodeBlock.builder(); - builder.beginControlFlow("if ($L.isSorted())", sort); + + boolean hasSort = StringUtils.hasText(sort); + if (hasSort) { + builder.beginControlFlow("if ($L.isSorted())", sort); + } builder.addStatement("$T declaredQuery = $T.$L($L)", DeclaredQuery.class, DeclaredQuery.class, queries != null && queries.isNative() ? "nativeQuery" : "jpqlQuery", queryString); - builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); - builder.endControlFlow(); + boolean hasDynamicReturnType = StringUtils.hasText(dynamicReturnType); + + if (hasSort && hasDynamicReturnType) { + builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $L)", queryString, sort, dynamicReturnType); + } else if (hasSort) { + builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); + } else if (hasDynamicReturnType) { + builder.addStatement("$L = rewriteQuery(declaredQuery, $T.unsorted(), $L)", queryString, Sort.class, + dynamicReturnType); + } + + if (hasSort) { + builder.endControlFlow(); + } return builder.build(); } @@ -241,14 +267,14 @@ private CodeBlock applyLimits(boolean exists) { return builder.build(); } - private CodeBlock createQuery(String queryVariableName, @Nullable String queryStringNameVariableName, + private CodeBlock createQuery(boolean count, String queryVariableName, @Nullable String queryStringNameVariableName, AotQuery query, @Nullable String sqlResultSetMapping, MergedAnnotation queryHints, @Nullable AotEntityGraph entityGraph, @Nullable Class queryReturnType) { Builder builder = CodeBlock.builder(); - builder.add( - doCreateQuery(queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping, queryReturnType)); + builder.add(doCreateQuery(count, queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping, + queryReturnType)); if (entityGraph != null) { builder.add(applyEntityGraph(entityGraph, queryVariableName)); @@ -279,12 +305,14 @@ private CodeBlock createQuery(String queryVariableName, @Nullable String querySt return builder.build(); } - private CodeBlock doCreateQuery(String queryVariableName, @Nullable String queryStringNameVariableName, - AotQuery query, @Nullable String sqlResultSetMapping, @Nullable Class queryReturnType) { + private CodeBlock doCreateQuery(boolean count, String queryVariableName, + @Nullable String queryStringNameVariableName, AotQuery query, @Nullable String sqlResultSetMapping, + @Nullable Class queryReturnType) { + ReturnedType returnedType = context.getReturnedType(); Builder builder = CodeBlock.builder(); - if (query instanceof StringAotQuery) { + if (query instanceof StringAotQuery sq) { if (StringUtils.hasText(sqlResultSetMapping)) { @@ -294,24 +322,48 @@ private CodeBlock doCreateQuery(String queryVariableName, @Nullable String query return builder.build(); } - if (query.isNative() && queryReturnType != null) { + if (query.isNative()) { + + if (queryReturnType != null) { - builder.addStatement("$T $L = this.$L.createNativeQuery($L, $T.class)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), queryStringNameVariableName, queryReturnType); + builder.addStatement("$T $L = this.$L.createNativeQuery($L, $T.class)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), queryStringNameVariableName, queryReturnType); + } else { + builder.addStatement("$T $L = this.$L.createNativeQuery($L)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), queryStringNameVariableName); + } return builder.build(); } - builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery", - queryStringNameVariableName); + if (sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting() + && returnedType.getReturnedType().isInterface()) { + builder.addStatement("$T $L = this.$L.createQuery($L)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), queryStringNameVariableName); + } else { + + String createQueryMethod = query.isNative() ? "createNativeQuery" : "createQuery"; + + if (!sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting() + && returnedType.getReturnedType().isInterface()) { + builder.addStatement("$T $L = this.$L.$L($L, $T.class)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameVariableName, Tuple.class); + } else { + builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameVariableName); + } + } return builder.build(); } if (query instanceof NamedAotQuery nq) { - if (queryReturnType != null) { + if (!count && returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) { + builder.addStatement("$T $L = this.$L.createNamedQuery($S)", Query.class, queryVariableName, + context.fieldNameOf(EntityManager.class), nq.getName()); + return builder.build(); + } else if (queryReturnType != null) { builder.addStatement("$T $L = this.$L.createNamedQuery($S, $T.class)", Query.class, queryVariableName, context.fieldNameOf(EntityManager.class), nq.getName(), queryReturnType); @@ -512,30 +564,68 @@ public CodeBlock build() { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); } else { - if (queryMethod.isCollectionQuery()) { - builder.addStatement("return ($T) query.getResultList()", context.getReturnTypeName()); - } else if (queryMethod.isStreamQuery()) { - builder.addStatement("return ($T) query.getResultStream()", context.getReturnTypeName()); - } else if (queryMethod.isPageQuery()) { - builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", - PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, - context.getPageableParameterName()); - } else if (queryMethod.isSliceQuery()) { - builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, - queryVariableName); - builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", - context.getPageableParameterName(), context.getPageableParameterName()); - builder.addStatement( - "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", - SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + if (context.getReturnedType().isProjecting()) { + + TypeName queryResultType = TypeName.get(context.getActualReturnType().toClass()); + + if (queryMethod.isCollectionQuery()) { + builder.addStatement("return ($T) convertMany(query.getResultList(), $L, $T.class)", + context.getReturnTypeName(), aotQuery.isNative(), queryResultType); + } else if (queryMethod.isStreamQuery()) { + builder.addStatement("return ($T) convertMany(query.getResultStream(), $L, $T.class)", + context.getReturnTypeName(), aotQuery.isNative(), queryResultType); + } else if (queryMethod.isPageQuery()) { + builder.addStatement( + "return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, countAll)", + PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, aotQuery.isNative(), + queryResultType, context.getPageableParameterName()); + } else if (queryMethod.isSliceQuery()) { + builder.addStatement("$T<$T> resultList = ($T<$T>) convertMany($L.getResultList(), $L, $T.class)", + List.class, actualReturnType, List.class, actualReturnType, queryVariableName, aotQuery.isNative(), + queryResultType); + builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", + context.getPageableParameterName(), context.getPageableParameterName()); + builder.addStatement( + "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", + SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + } else { + + if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { + builder.addStatement("return $T.ofNullable(($T) convertOne($L.getSingleResultOrNull(), $L, $T.class))", + Optional.class, actualReturnType, queryVariableName, aotQuery.isNative(), queryResultType); + } else { + builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)", + context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType); + } + } + } else { - if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { - builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, - actualReturnType, queryVariableName); - } else { - builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnTypeName(), + if (queryMethod.isCollectionQuery()) { + builder.addStatement("return ($T) query.getResultList()", context.getReturnTypeName()); + } else if (queryMethod.isStreamQuery()) { + builder.addStatement("return ($T) query.getResultStream()", context.getReturnTypeName()); + } else if (queryMethod.isPageQuery()) { + builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", + PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, + context.getPageableParameterName()); + } else if (queryMethod.isSliceQuery()) { + builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); + builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", + context.getPageableParameterName(), context.getPageableParameterName()); + builder.addStatement( + "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", + SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + } else { + + if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { + builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, + actualReturnType, queryVariableName); + } else { + builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnTypeName(), + queryVariableName); + } } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 732441f3a9..157e77c5e1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -15,46 +15,22 @@ */ package org.springframework.data.jpa.repository.aot; -import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.Tuple; -import jakarta.persistence.TypedQueryReference; -import jakarta.persistence.metamodel.Metamodel; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; -import org.springframework.data.jpa.repository.query.DeclaredQuery; -import org.springframework.data.jpa.repository.query.EntityQuery; -import org.springframework.data.jpa.repository.query.EscapeCharacter; -import org.springframework.data.jpa.repository.query.JpaCountQueryCreator; import org.springframework.data.jpa.repository.query.JpaParameters; -import org.springframework.data.jpa.repository.query.JpaQueryCreator; import org.springframework.data.jpa.repository.query.JpaQueryMethod; -import org.springframework.data.jpa.repository.query.ParameterMetadataProvider; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; -import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; -import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata; import org.springframework.data.repository.aot.generate.MethodContributor; @@ -64,14 +40,11 @@ import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * JPA-specific {@link RepositoryContributor} contributing an AOT repository fragment using the {@link EntityManager} @@ -85,23 +58,18 @@ */ public class JpaRepositoryContributor extends RepositoryContributor { - private final EntityManagerFactory emf; - private final Metamodel metaModel; private final PersistenceProvider persistenceProvider; + private final QueriesFactory queriesFactory; + private final EntityGraphLookup entityGraphLookup; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); + AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes()); - this.metaModel = amm; - this.emf = amm.getEntityManagerFactory(); - this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory()); - } - public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { - super(repositoryContext); - this.emf = entityManagerFactory; - this.metaModel = entityManagerFactory.getMetamodel(); - this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory()); + this.queriesFactory = new QueriesFactory(amm, amm.getEntityManagerFactory()); + this.entityGraphLookup = new EntityGraphLookup(amm.getEntityManagerFactory()); } @Override @@ -138,18 +106,15 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } ReturnedType returnedType = queryMethod.getResultProcessor().getReturnedType(); + JpaParameters parameters = queryMethod.getParameters(); - // no interface/dynamic projections for now. - if (returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) { - return null; - } - - if (queryMethod.getParameters().hasDynamicProjection()) { + // no KeysetScrolling for now. + if (parameters.hasScrollPositionParameter()) { return null; } - // no KeysetScrolling for now. - if (queryMethod.getParameters().hasScrollPositionParameter()) { + // no dynamic projections. + if (parameters.hasDynamicProjection()) { return null; } @@ -178,12 +143,13 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - AotQueries aotQueries = getQueries(context, query, selector, queryMethod, returnedType); - AotEntityGraph aotEntityGraph = getAotEntityGraph(entityGraph, repositoryInformation, returnedType, queryMethod); + AotQueries aotQueries = queriesFactory.createQueries(context, query, selector, queryMethod, returnedType); + AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation, + returnedType, queryMethod); body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) - .queryReturnType(getQueryReturnType(aotQueries.result(), returnedType, context)).nativeQuery(nativeQuery) - .queryHints(queryHints).entityGraph(aotEntityGraph).build()); + .queryReturnType(QueriesFactory.getQueryReturnType(aotQueries.result(), returnedType, context)) + .nativeQuery(nativeQuery).queryHints(queryHints).entityGraph(aotEntityGraph).build()); body.add( JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build()); @@ -192,242 +158,4 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB }); } - private AotQueries getQueries(AotQueryMethodGenerationContext context, MergedAnnotation query, - QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { - - if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { - return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query, - queryMethod); - } - - TypedQueryReference namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName()); - if (namedQuery != null) { - return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod); - } - - return buildPartTreeQuery(returnedType, context, query, queryMethod); - } - - private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, - MergedAnnotation query, JpaQueryMethod queryMethod) { - - UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName()); - boolean isNative = query.getBoolean("nativeQuery"); - Function queryFunction = isNative ? StringAotQuery::nativeQuery : StringAotQuery::jpqlQuery; - queryFunction = operator.andThen(queryFunction); - - String queryString = query.getString("value"); - - StringAotQuery aotStringQuery = queryFunction.apply(queryString); - String countQuery = query.getString("countQuery"); - - EntityQuery entityQuery = EntityQuery.create(aotStringQuery.getQuery(), selector); - if (entityQuery.hasConstructorExpression() || entityQuery.isDefaultProjection()) { - aotStringQuery = aotStringQuery.withReturnsDeclaredMethodType(); - } - - if (StringUtils.hasText(countQuery)) { - return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery)); - } - - String namedCountQueryName = queryMethod.getNamedCountQueryName(); - TypedQueryReference namedCountQuery = getNamedQuery(returnedType, namedCountQueryName); - if (namedCountQuery != null) { - return AotQueries.from(aotStringQuery, buildNamedAotQuery(namedCountQuery, queryMethod, isNative)); - } - - String countProjection = query.getString("countProjection"); - return AotQueries.from(aotStringQuery, countProjection, selector); - } - - private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector, - TypedQueryReference namedQuery, MergedAnnotation query, JpaQueryMethod queryMethod) { - - NamedAotQuery aotQuery = buildNamedAotQuery(namedQuery, queryMethod, - query.isPresent() && query.getBoolean("nativeQuery")); - - String countQuery = query.isPresent() ? query.getString("countQuery") : null; - if (StringUtils.hasText(countQuery)) { - return AotQueries.from(aotQuery, - aotQuery.isNative() ? StringAotQuery.nativeQuery(countQuery) : StringAotQuery.jpqlQuery(countQuery)); - } - - TypedQueryReference namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName()); - - if (namedCountQuery != null) { - return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, aotQuery.isNative())); - } - - String countProjection = query.isPresent() ? query.getString("countProjection") : null; - return AotQueries.from(aotQuery, it -> { - return StringAotQuery.of(aotQuery.getQueryString()).getQuery(); - }, countProjection, selector); - } - - private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQueryMethod queryMethod, - boolean isNative) { - - QueryExtractor queryExtractor = queryMethod.getQueryExtractor(); - String queryString = queryExtractor.extractQueryString(namedQuery); - - if (!isNative) { - isNative = queryExtractor.isNativeQuery(namedQuery); - } - - Assert.hasText(queryString, () -> "Cannot extract Query from named query [%s]".formatted(namedQuery.getName())); - - return NamedAotQuery.named(namedQuery.getName(), - isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString)); - } - - private @Nullable TypedQueryReference getNamedQuery(ReturnedType returnedType, String queryName) { - - List> candidates = Arrays.asList(Object.class, returnedType.getDomainType(), - returnedType.getReturnedType(), returnedType.getTypeToRead(), void.class, null, Long.class, Integer.class, - Long.TYPE, Integer.TYPE, Number.class); - - for (Class candidate : candidates) { - - Map> namedQueries = emf.getNamedQueries(candidate); - - if (namedQueries.containsKey(queryName)) { - return namedQueries.get(queryName); - } - } - - return null; - } - - private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context, - MergedAnnotation query, JpaQueryMethod queryMethod) { - - PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); - // TODO make configurable - JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; - - AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates); - - if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { - return AotQueries.from(aotQuery, StringAotQuery.jpqlQuery(query.getString("countQuery"))); - } - - TypedQueryReference namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName()); - if (namedCountQuery != null) { - return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, false)); - } - - AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates); - return AotQueries.from(aotQuery, partTreeCountQuery); - } - - private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, - JpqlQueryTemplates templates) { - - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - templates); - JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metaModel); - - return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), - partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection()); - } - - private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, - JpqlQueryTemplates templates) { - - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - templates); - JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, - metaModel); - - return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), null, false, false); - } - - private static @Nullable Class getQueryReturnType(AotQuery query, ReturnedType returnedType, - AotQueryMethodGenerationContext context) { - - Method method = context.getMethod(); - RepositoryInformation repositoryInformation = context.getRepositoryInformation(); - - Class methodReturnType = repositoryInformation.getReturnedDomainClass(method); - boolean queryForEntity = repositoryInformation.getDomainType().isAssignableFrom(methodReturnType); - - Class result = queryForEntity ? returnedType.getDomainType() : null; - - if (query instanceof StringAotQuery sq && sq.returnsDeclaredMethodType()) { - return result; - } - - if (returnedType.isProjecting()) { - - if (returnedType.getReturnedType().isInterface()) { - return Tuple.class; - } - - return returnedType.getReturnedType(); - } - - return result; - } - - @SuppressWarnings("unchecked") - private @Nullable AotEntityGraph getAotEntityGraph(MergedAnnotation entityGraph, - RepositoryInformation information, ReturnedType returnedType, JpaQueryMethod queryMethod) { - - if (!entityGraph.isPresent()) { - return null; - } - - EntityGraph.EntityGraphType type = entityGraph.getEnum("type", EntityGraph.EntityGraphType.class); - String[] attributePaths = entityGraph.getStringArray("attributePaths"); - Collection entityGraphNames = getEntityGraphNames(entityGraph, information, queryMethod); - List> candidates = Arrays.asList(returnedType.getDomainType(), returnedType.getReturnedType(), - returnedType.getTypeToRead()); - - for (Class candidate : candidates) { - - Map> namedEntityGraphs = emf - .getNamedEntityGraphs(Class.class.cast(candidate)); - - if (namedEntityGraphs.isEmpty()) { - continue; - } - - for (String entityGraphName : entityGraphNames) { - if (namedEntityGraphs.containsKey(entityGraphName)) { - return new AotEntityGraph(entityGraphName, type, Collections.emptyList()); - } - } - } - - if (attributePaths.length > 0) { - return new AotEntityGraph(null, type, Arrays.asList(attributePaths)); - } - - return null; - } - - private Set getEntityGraphNames(MergedAnnotation entityGraph, RepositoryInformation information, - JpaQueryMethod queryMethod) { - - Set entityGraphNames = new LinkedHashSet<>(); - String value = entityGraph.getString("value"); - - if (StringUtils.hasText(value)) { - entityGraphNames.add(value); - } - entityGraphNames.add(queryMethod.getNamedQueryName()); - entityGraphNames.add(getFallbackEntityGraphName(information, queryMethod)); - return entityGraphNames; - } - - private String getFallbackEntityGraphName(RepositoryInformation information, JpaQueryMethod queryMethod) { - - Class domainType = information.getDomainType(); - Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class); - String entityName = entity != null && StringUtils.hasText(entity.name()) ? entity.name() - : domainType.getSimpleName(); - - return entityName + "." + queryMethod.getName(); - } - } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java new file mode 100644 index 0000000000..1188ec7d23 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java @@ -0,0 +1,268 @@ +/* + * 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.aot; + +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Tuple; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.metamodel.Metamodel; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.provider.QueryExtractor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.*; +import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Factory for {@link AotQueries}. + * + * @author Mark Paluch + * @since 4.0 + */ +class QueriesFactory { + + private final Metamodel metamodel; + private final EntityManagerFactory emf; + + public QueriesFactory(AotMetamodel metamodel, EntityManagerFactory emf) { + this.metamodel = metamodel; + this.emf = emf; + } + + /** + * Creates the {@link AotQueries} used within a specific {@link JpaQueryMethod}. + * + * @param context + * @param query + * @param selector + * @param queryMethod + * @param returnedType + * @return + */ + public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedAnnotation query, + QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { + + if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { + return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query, + queryMethod); + } + + TypedQueryReference namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName()); + if (namedQuery != null) { + return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod); + } + + return buildPartTreeQuery(returnedType, context, query, queryMethod); + } + + private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, + MergedAnnotation query, JpaQueryMethod queryMethod) { + + UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName()); + boolean isNative = query.getBoolean("nativeQuery"); + Function queryFunction = isNative ? StringAotQuery::nativeQuery : StringAotQuery::jpqlQuery; + queryFunction = operator.andThen(queryFunction); + + String queryString = query.getString("value"); + + StringAotQuery aotStringQuery = queryFunction.apply(queryString); + String countQuery = query.getString("countQuery"); + + EntityQuery entityQuery = EntityQuery.create(aotStringQuery.getQuery(), selector); + if (entityQuery.hasConstructorExpression() || entityQuery.isDefaultProjection()) { + aotStringQuery = aotStringQuery.withConstructorExpressionOrDefaultProjection(); + } + + if (returnedType.isProjecting() && returnedType.hasInputProperties() + && !returnedType.getReturnedType().isInterface()) { + + QueryProvider rewritten = entityQuery.rewrite(new QueryEnhancer.QueryRewriteInformation() { + @Override + public Sort getSort() { + return Sort.unsorted(); + } + + @Override + public ReturnedType getReturnedType() { + return returnedType; + } + }); + + aotStringQuery = aotStringQuery.rewrite(rewritten); + } + + if (StringUtils.hasText(countQuery)) { + return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery)); + } + + String namedCountQueryName = queryMethod.getNamedCountQueryName(); + TypedQueryReference namedCountQuery = getNamedQuery(returnedType, namedCountQueryName); + if (namedCountQuery != null) { + return AotQueries.from(aotStringQuery, buildNamedAotQuery(namedCountQuery, queryMethod, isNative)); + } + + String countProjection = query.getString("countProjection"); + return AotQueries.from(aotStringQuery, countProjection, selector); + } + + private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector, + TypedQueryReference namedQuery, MergedAnnotation query, JpaQueryMethod queryMethod) { + + NamedAotQuery aotQuery = buildNamedAotQuery(namedQuery, queryMethod, + query.isPresent() && query.getBoolean("nativeQuery")); + + String countQuery = query.isPresent() ? query.getString("countQuery") : null; + if (StringUtils.hasText(countQuery)) { + return AotQueries.from(aotQuery, + aotQuery.isNative() ? StringAotQuery.nativeQuery(countQuery) : StringAotQuery.jpqlQuery(countQuery)); + } + + TypedQueryReference namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName()); + + if (namedCountQuery != null) { + return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, aotQuery.isNative())); + } + + String countProjection = query.isPresent() ? query.getString("countProjection") : null; + return AotQueries.from(aotQuery, it -> { + return StringAotQuery.of(aotQuery.getQueryString()).getQuery(); + }, countProjection, selector); + } + + private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQueryMethod queryMethod, + boolean isNative) { + + QueryExtractor queryExtractor = queryMethod.getQueryExtractor(); + String queryString = queryExtractor.extractQueryString(namedQuery); + + if (!isNative) { + isNative = queryExtractor.isNativeQuery(namedQuery); + } + + Assert.hasText(queryString, () -> "Cannot extract Query from named query [%s]".formatted(namedQuery.getName())); + + return NamedAotQuery.named(namedQuery.getName(), + isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString)); + } + + private @Nullable TypedQueryReference getNamedQuery(ReturnedType returnedType, String queryName) { + + List> candidates = Arrays.asList(Object.class, returnedType.getDomainType(), + returnedType.getReturnedType(), returnedType.getTypeToRead(), void.class, null, Long.class, Integer.class, + Long.TYPE, Integer.TYPE, Number.class); + + for (Class candidate : candidates) { + + Map> namedQueries = emf.getNamedQueries(candidate); + + if (namedQueries.containsKey(queryName)) { + return namedQueries.get(queryName); + } + } + + return null; + } + + private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context, + MergedAnnotation query, JpaQueryMethod queryMethod) { + + PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); + // TODO make configurable + JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; + + AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates); + + if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { + return AotQueries.from(aotQuery, StringAotQuery.jpqlQuery(query.getString("countQuery"))); + } + + TypedQueryReference namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName()); + if (namedCountQuery != null) { + return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, false)); + } + + AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates); + return AotQueries.from(aotQuery, partTreeCountQuery); + } + + private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, + JpqlQueryTemplates templates) { + + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, + templates); + JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metamodel); + + return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), + partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection()); + } + + private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, + JpqlQueryTemplates templates) { + + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, + templates); + JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, + metamodel); + + return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), Limit.unlimited(), + false, false); + } + + public static @Nullable Class getQueryReturnType(AotQuery query, ReturnedType returnedType, + AotQueryMethodGenerationContext context) { + + Method method = context.getMethod(); + RepositoryInformation repositoryInformation = context.getRepositoryInformation(); + + Class methodReturnType = repositoryInformation.getReturnedDomainClass(method); + boolean queryForEntity = repositoryInformation.getDomainType().isAssignableFrom(methodReturnType); + + Class result = queryForEntity ? returnedType.getDomainType() : null; + + if (query instanceof StringAotQuery sq && sq.hasConstructorExpressionOrDefaultProjection()) { + return result; + } + + if (returnedType.isProjecting()) { + + if (returnedType.getReturnedType().isInterface()) { + return Tuple.class; + } + + return returnedType.getReturnedType(); + } + + return result; + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java index 499f1d6c6f..b30f0118c7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/StringAotQuery.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.ParameterBinding; import org.springframework.data.jpa.repository.query.PreprocessedQuery; +import org.springframework.data.jpa.repository.query.QueryProvider; /** * An AOT query represented by a string. @@ -59,7 +60,7 @@ static StringAotQuery jpqlQuery(String queryString) { */ public static StringAotQuery jpqlQuery(String queryString, List bindings, Limit resultLimit, boolean delete, boolean exists) { - return new LimitedAotQuery(queryString, bindings, resultLimit, delete, exists); + return new DerivedAotQuery(queryString, bindings, resultLimit, delete, exists); } /** @@ -83,28 +84,34 @@ public String getQueryString() { * @return {@literal true} if query is expected to return the declared method type directly; {@literal false} if the * result requires projection post-processing. See also {@code NativeJpaQuery#getTypeToQueryFor}. */ - public abstract boolean returnsDeclaredMethodType(); + public abstract boolean hasConstructorExpressionOrDefaultProjection(); - public abstract StringAotQuery withReturnsDeclaredMethodType(); + /** + * @return a new {@link StringAotQuery} using constructor expressions or containing the default (primary alias) + * projection. + */ + public abstract StringAotQuery withConstructorExpressionOrDefaultProjection(); @Override public String toString() { return getQueryString(); } + public abstract StringAotQuery rewrite(QueryProvider rewritten); + /** * @author Christoph Strobl * @author Mark Paluch */ - static class DeclaredAotQuery extends StringAotQuery { + private static class DeclaredAotQuery extends StringAotQuery { private final PreprocessedQuery query; - private final boolean returnsDeclaredMethodType; + private final boolean constructorExpressionOrDefaultProjection; - DeclaredAotQuery(PreprocessedQuery query, boolean returnsDeclaredMethodType) { + DeclaredAotQuery(PreprocessedQuery query, boolean constructorExpressionOrDefaultProjection) { super(query.getBindings()); this.query = query; - this.returnsDeclaredMethodType = returnsDeclaredMethodType; + this.constructorExpressionOrDefaultProjection = constructorExpressionOrDefaultProjection; } @Override @@ -123,30 +130,35 @@ public boolean isNative() { } @Override - public boolean returnsDeclaredMethodType() { - return returnsDeclaredMethodType; + public boolean hasConstructorExpressionOrDefaultProjection() { + return constructorExpressionOrDefaultProjection; + } + + @Override + public StringAotQuery withConstructorExpressionOrDefaultProjection() { + return new DeclaredAotQuery(query, true); } @Override - public StringAotQuery withReturnsDeclaredMethodType() { - return new DeclaredAotQuery(query, returnsDeclaredMethodType); + public StringAotQuery rewrite(QueryProvider rewritten) { + return new DeclaredAotQuery(query.rewrite(rewritten.getQueryString()), constructorExpressionOrDefaultProjection); } } /** - * Query with a limit associated. + * PartTree (derived) Query with a limit associated. * * @author Mark Paluch */ - static class LimitedAotQuery extends StringAotQuery { + private static class DerivedAotQuery extends StringAotQuery { private final String queryString; private final Limit limit; private final boolean delete; private final boolean exists; - LimitedAotQuery(String queryString, List parameterBindings, Limit limit, boolean delete, + DerivedAotQuery(String queryString, List parameterBindings, Limit limit, boolean delete, boolean exists) { super(parameterBindings); this.queryString = queryString; @@ -186,14 +198,19 @@ public boolean isExists() { } @Override - public boolean returnsDeclaredMethodType() { - return true; + public boolean hasConstructorExpressionOrDefaultProjection() { + return false; } @Override - public StringAotQuery withReturnsDeclaredMethodType() { + public StringAotQuery withConstructorExpressionOrDefaultProjection() { return this; } + @Override + public StringAotQuery rewrite(QueryProvider rewritten) { + return new DerivedAotQuery(rewritten.getQueryString(), this.getParameterBindings(), getLimit(), delete, exists); + } + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 2d75b3970c..4e672ccc80 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -25,18 +25,13 @@ import java.lang.reflect.Constructor; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Collectors; -import org.springframework.beans.BeanUtils; - import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeanUtils; import org.springframework.core.MethodParameter; import org.springframework.core.convert.converter.Converter; import org.springframework.data.jpa.provider.PersistenceProvider; @@ -50,13 +45,13 @@ import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution; import org.springframework.data.jpa.repository.support.QueryHints; import org.springframework.data.jpa.util.JpaMetamodel; +import org.springframework.data.jpa.util.TupleBackedMap; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.Lazy; -import org.springframework.jdbc.support.JdbcUtils; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -344,7 +339,7 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) { Assert.notNull(type, "Returned type must not be null"); this.type = type; - this.tupleWrapper = nativeQuery ? FallbackTupleWrapper::new : UnaryOperator.identity(); + this.tupleWrapper = nativeQuery ? TupleBackedMap::underscoreAware : UnaryOperator.identity(); this.dtoProjection = type.isProjecting() && !type.getReturnedType().isInterface() && !type.getInputProperties().isEmpty(); @@ -468,180 +463,6 @@ private static boolean areAssignmentCompatible(Class to, Class from) { return ClassUtils.isAssignable(to, from); } - /** - * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided - * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the - * key/entry set. - * - * @author Jens Schauder - */ - private static class TupleBackedMap implements Map { - - private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified"; - - private final Tuple tuple; - - TupleBackedMap(Tuple tuple) { - this.tuple = tuple; - } - - @Override - public int size() { - return tuple.getElements().size(); - } - - @Override - public boolean isEmpty() { - return tuple.getElements().isEmpty(); - } - - /** - * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. - * Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}. - * - * @param key the key for which to get the value from the map. - * @return whether the key is an element of the backing tuple. - */ - @Override - public boolean containsKey(Object key) { - - try { - tuple.get((String) key); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - @Override - public boolean containsValue(Object value) { - return Arrays.asList(tuple.toArray()).contains(value); - } - - /** - * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. - * Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}. - * - * @param key the key for which to get the value from the map. - * @return the value of the backing {@link Tuple} for that key or {@code null}. - */ - @Override - public @Nullable Object get(Object key) { - - if (!(key instanceof String)) { - return null; - } - - try { - return tuple.get((String) key); - } catch (IllegalArgumentException e) { - return null; - } - } - - @Override - public Object put(String key, Object value) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public Object remove(Object key) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public void putAll(Map m) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public Set keySet() { - - return tuple.getElements().stream() // - .map(TupleElement::getAlias) // - .collect(Collectors.toSet()); - } - - @Override - public Collection values() { - return Arrays.asList(tuple.toArray()); - } - - @Override - public Set> entrySet() { - - return tuple.getElements().stream() // - .map(e -> new HashMap.SimpleEntry(e.getAlias(), tuple.get(e))) // - .collect(Collectors.toSet()); - } - } } - private static class FallbackTupleWrapper implements Tuple { - - private final Tuple delegate; - private final UnaryOperator fallbackNameTransformer = JdbcUtils::convertPropertyNameToUnderscoreName; - - FallbackTupleWrapper(Tuple delegate) { - this.delegate = delegate; - } - - @Override - public X get(TupleElement tupleElement) { - return get(tupleElement.getAlias(), tupleElement.getJavaType()); - } - - @Override - public X get(String s, Class type) { - try { - return delegate.get(s, type); - } catch (IllegalArgumentException original) { - try { - return delegate.get(fallbackNameTransformer.apply(s), type); - } catch (IllegalArgumentException next) { - original.addSuppressed(next); - throw original; - } - } - } - - @Override - public Object get(String s) { - try { - return delegate.get(s); - } catch (IllegalArgumentException original) { - try { - return delegate.get(fallbackNameTransformer.apply(s)); - } catch (IllegalArgumentException next) { - original.addSuppressed(next); - throw original; - } - } - } - - @Override - public X get(int i, Class aClass) { - return delegate.get(i, aClass); - } - - @Override - public Object get(int i) { - return delegate.get(i); - } - - @Override - public Object[] toArray() { - return delegate.toArray(); - } - - @Override - public List> getElements() { - return delegate.getElements(); - } - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/TupleBackedMap.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/TupleBackedMap.java new file mode 100644 index 0000000000..1c6c6927f7 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/TupleBackedMap.java @@ -0,0 +1,219 @@ +/* + * 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.util; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TupleElement; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import org.jspecify.annotations.Nullable; + +import org.springframework.jdbc.support.JdbcUtils; + +/** + * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided {@link Tuple} + * implementation it might return the same value for various keys of which only one will appear in the key/entry set. + * + * @author Jens Schauder + * @since 4.0 + */ +public class TupleBackedMap implements Map { + + private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified"; + + private final Tuple tuple; + + public TupleBackedMap(Tuple tuple) { + this.tuple = tuple; + } + + /** + * Creates a underscore-aware {@link Tuple} wrapper applying {@link JdbcUtils#convertPropertyNameToUnderscoreName} + * conversion to leniently look up properties from query results whose columns follow snake-case syntax. + * + * @param delegate the tuple to wrap. + * @return + */ + public static Tuple underscoreAware(Tuple delegate) { + return new FallbackTupleWrapper(delegate); + } + + @Override + public int size() { + return tuple.getElements().size(); + } + + @Override + public boolean isEmpty() { + return tuple.getElements().isEmpty(); + } + + /** + * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. Otherwise + * this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}. + * + * @param key the key for which to get the value from the map. + * @return whether the key is an element of the backing tuple. + */ + @Override + public boolean containsKey(Object key) { + + try { + tuple.get((String) key); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public boolean containsValue(Object value) { + return Arrays.asList(tuple.toArray()).contains(value); + } + + /** + * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. Otherwise + * the value from the backing {@code Tuple} is returned, which also might be {@code null}. + * + * @param key the key for which to get the value from the map. + * @return the value of the backing {@link Tuple} for that key or {@code null}. + */ + @Override + public @Nullable Object get(Object key) { + + if (!(key instanceof String)) { + return null; + } + + try { + return tuple.get((String) key); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public Set keySet() { + + return tuple.getElements().stream() // + .map(TupleElement::getAlias) // + .collect(Collectors.toSet()); + } + + @Override + public Collection values() { + return Arrays.asList(tuple.toArray()); + } + + @Override + public Set> entrySet() { + + return tuple.getElements().stream() // + .map(e -> new HashMap.SimpleEntry(e.getAlias(), tuple.get(e))) // + .collect(Collectors.toSet()); + } + + static class FallbackTupleWrapper implements Tuple { + + private final Tuple delegate; + private final UnaryOperator fallbackNameTransformer = JdbcUtils::convertPropertyNameToUnderscoreName; + + FallbackTupleWrapper(Tuple delegate) { + this.delegate = delegate; + } + + @Override + public X get(TupleElement tupleElement) { + return get(tupleElement.getAlias(), tupleElement.getJavaType()); + } + + @Override + public X get(String s, Class type) { + try { + return delegate.get(s, type); + } catch (IllegalArgumentException original) { + try { + return delegate.get(fallbackNameTransformer.apply(s), type); + } catch (IllegalArgumentException next) { + original.addSuppressed(next); + throw original; + } + } + } + + @Override + public Object get(String s) { + try { + return delegate.get(s); + } catch (IllegalArgumentException original) { + try { + return delegate.get(fallbackNameTransformer.apply(s)); + } catch (IllegalArgumentException next) { + original.addSuppressed(next); + throw original; + } + } + } + + @Override + public X get(int i, Class aClass) { + return delegate.get(i, aClass); + } + + @Override + public Object get(int i) { + return delegate.get(i); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @Override + public List> getElements() { + return delegate.getElements(); + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java index 5f609bad6d..aa2f2f0c58 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.hibernate.proxy.HibernateProxy; +import org.hibernate.query.QueryTypeMismatchException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,6 +36,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.SpecialUser; import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.annotation.Transactional; @@ -101,46 +103,19 @@ void beforeEach() { } @Test - void testFindDerivedQuerySingleEntity() { + void testDerivedFinderWithoutArguments() { - User user = fragment.findOneByEmailAddress("luke@jedi.org"); - assertThat(user.getLastname()).isEqualTo("Skywalker"); + List users = fragment.findUserNoArgumentsBy(); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); } @Test - void shouldUseNamedQuery() { + void testFindDerivedQuerySingleEntity() { - User user = fragment.findByEmailAddress("luke@jedi.org"); + User user = fragment.findOneByEmailAddress("luke@jedi.org"); assertThat(user.getLastname()).isEqualTo("Skywalker"); } - @Test - void shouldUseNamedQueryAndDeriveCountQuery() { - - Page user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); - - assertThat(user).hasSize(1); - assertThat(user.getTotalElements()).isEqualTo(1); - } - - @Test - void shouldUseNamedQueryAndProvidedCountQuery() { - - Page user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); - - assertThat(user).hasSize(1); - assertThat(user.getTotalElements()).isEqualTo(1); - } - - @Test - void shouldUseNamedQueryAndNamedCountQuery() { - - Page user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); - - assertThat(user).hasSize(1); - assertThat(user.getTotalElements()).isEqualTo(1); - } - @Test void testFindDerivedFinderOptionalEntity() { @@ -163,13 +138,6 @@ void testDerivedExists() { assertThat(exists).isTrue(); } - @Test - void testDerivedFinderWithoutArguments() { - - List users = fragment.findUserNoArgumentsBy(); - assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); - } - @Test void testDerivedFinderReturningList() { @@ -398,50 +366,144 @@ void testDerivedFinderReturningListOfProjections() { } @Test - void shouldApplyQueryHints() { - assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker")) - .withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo"); + void testDerivedFinderReturningPageOfProjections() { + + Page page = fragment.findUserProjectionByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", + "kylo@new-empire.com"); + + Page noResults = fragment.findUserProjectionByLastnameStartingWith("a", + PageRequest.of(0, 2, Sort.by("emailAddress"))); + + assertThat(noResults).isEmpty(); } @Test - void shouldApplyNamedEntityGraph() { + void shouldApplySqlResultSetMapping() { - User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca"); + User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId()); - assertThat(chewie.getManager()).isInstanceOf(HibernateProxy.class); - assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); + assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress()); } @Test - void shouldApplyDeclaredEntityGraph() { + void shouldApplyNamedDto() { - User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca"); + // named queries cannot be rewritten + assertThatExceptionOfType(QueryTypeMismatchException.class) + .isThrownBy(() -> fragment.findNamedDtoEmailAddress(kylo.getEmailAddress())); + } - assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); + @Test + void shouldApplyDerivedDto() { - User han = chewie.getManager(); - assertThat(han.getRoles()).isNotInstanceOf(HibernateProxy.class); - assertThat(han.getManager()).isInstanceOf(HibernateProxy.class); + UserRepository.Names names = fragment.findDtoByEmailAddress(kylo.getEmailAddress()); + + assertThat(names.lastname()).isEqualTo(kylo.getLastname()); + assertThat(names.firstname()).isEqualTo(kylo.getFirstname()); } @Test - void testDerivedFinderReturningPageOfProjections() { + void shouldApplyDerivedDtoPage() { - Page page = fragment.findUserProjectionByLastnameStartingWith("S", - PageRequest.of(0, 2, Sort.by("emailAddress"))); + Page names = fragment.findDtoPageByEmailAddress(kylo.getEmailAddress(), PageRequest.of(0, 1)); - assertThat(page.getTotalElements()).isEqualTo(4); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", - "kylo@new-empire.com"); + assertThat(names).hasSize(1); + assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname()); + } - Page noResults = fragment.findUserProjectionByLastnameStartingWith("a", - PageRequest.of(0, 2, Sort.by("emailAddress"))); + @Test + void shouldApplyAnnotatedDto() { - assertThat(noResults).isEmpty(); + UserRepository.Names names = fragment.findAnnotatedDtoEmailAddress(kylo.getEmailAddress()); + + assertThat(names.lastname()).isEqualTo(kylo.getLastname()); + assertThat(names.firstname()).isEqualTo(kylo.getFirstname()); + } + + @Test + void shouldApplyAnnotatedDtoPage() { + + Page names = fragment.findAnnotatedDtoPageByEmailAddress(kylo.getEmailAddress(), + PageRequest.of(0, 1)); + + assertThat(names).hasSize(1); + assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname()); } - // modifying + @Test + void shouldApplyDerivedQueryInterfaceProjection() { + + UserRepository.EmailOnly result = fragment.findEmailProjectionById(kylo.getId()); + + assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } + + @Test + void shouldApplyInterfaceProjectionPage() { + + Page result = fragment.findProjectedPageByEmailAddress(kylo.getEmailAddress(), + PageRequest.of(0, 1)); + + assertThat(result).hasSize(1); + assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } + + @Test + void shouldApplyInterfaceProjectionSlice() { + + Slice result = fragment.findProjectedSliceByEmailAddress(kylo.getEmailAddress(), + PageRequest.of(0, 1)); + + assertThat(result).hasSize(1); + assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } + + @Test + void shouldApplyInterfaceProjectionToDerivedQueryStream() { + + Stream result = fragment.streamProjectedByEmailAddress(kylo.getEmailAddress()); + + assertThat(result).hasSize(1).map(UserRepository.EmailOnly::getEmailAddress).contains(kylo.getEmailAddress()); + } + + @Test + void shouldApplyAnnotatedQueryInterfaceProjection() { + + UserRepository.EmailOnly result = fragment.findAnnotatedEmailProjectionByEmailAddress(kylo.getEmailAddress()); + + assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } + + @Test + void shouldApplyAnnotatedInterfaceProjectionQueryPage() { + + Page result = fragment.findAnnotatedProjectedPageByEmailAddress(kylo.getEmailAddress(), + PageRequest.of(0, 1)); + + assertThat(result).hasSize(1); + assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } + + @Test + void shouldApplyNativeInterfaceProjection() { + + UserRepository.EmailOnly result = fragment.findEmailProjectionByNativeQuery(kylo.getId()); + + assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } + + @Test + void shouldApplyNamedQueryInterfaceProjection() { + + UserRepository.EmailOnly result = fragment.findNamedProjectionEmailAddress(kylo.getEmailAddress()); + + assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); + } @Test void testDerivedDeleteSingle() { @@ -476,8 +538,6 @@ void shouldApplyModifying() { assertThat(yodaShouldBeGone).isNull(); } - // native queries - @Test void nativeQuery() { @@ -489,22 +549,86 @@ void nativeQuery() { } @Test - void shouldApplySqlResultSetMapping() { + void shouldUseNamedQuery() { - User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId()); + User user = fragment.findByEmailAddress("luke@jedi.org"); + assertThat(user.getLastname()).isEqualTo("Skywalker"); + } - assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress()); + @Test + void shouldUseNamedQueryAndDeriveCountQuery() { + + Page user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); + + assertThat(user).hasSize(1); + assertThat(user.getTotalElements()).isEqualTo(1); } - // old stuff below + @Test + void shouldUseNamedQueryAndProvidedCountQuery() { + + Page user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); + + assertThat(user).hasSize(1); + assertThat(user.getTotalElements()).isEqualTo(1); + } + + @Test + void shouldUseNamedQueryAndNamedCountQuery() { + + Page user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); + + assertThat(user).hasSize(1); + assertThat(user.getTotalElements()).isEqualTo(1); + } + + @Test + void shouldApplyQueryHints() { + assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker")) + .withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo"); + } + + @Test + void shouldApplyNamedEntityGraph() { + + User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca"); + + assertThat(chewie.getManager()).isInstanceOf(HibernateProxy.class); + assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); + } + + @Test + void shouldApplyDeclaredEntityGraph() { + + User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca"); + + assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); + + User han = chewie.getManager(); + assertThat(han.getRoles()).isNotInstanceOf(HibernateProxy.class); + assertThat(han.getManager()).isInstanceOf(HibernateProxy.class); + } + + @Test + void shouldQuerySubtype() { + + SpecialUser snoopy = new SpecialUser(); + snoopy.setFirstname("Snoopy"); + snoopy.setLastname("n/a"); + snoopy.setEmailAddress("dog@home.com"); + em.persist(snoopy); + + SpecialUser result = fragment.findByEmailAddress("dog@home.com", SpecialUser.class); + + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(SpecialUser.class); + } void todo() { - // interface projections - // dynamic projections - // class type parameter + // dynamic projections: Not implemented + // keyset scrolling - // synthetic parameters (keyset scrolling! yuck!) } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index 1326960dbe..de4ae656df 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -38,7 +38,6 @@ * @author Christoph Strobl * @author Mark Paluch */ -// TODO: Querydsl, query by example interface UserRepository extends CrudRepository { List findUserNoArgumentsBy(); @@ -71,7 +70,9 @@ interface UserRepository extends CrudRepository { Stream streamByLastnameLike(String lastname); - /* Annotated Queries */ + // ------------------------------------------------------------------------- + // Declared Queries + // ------------------------------------------------------------------------- @Query("select u from User u where u.emailAddress = ?1") User findAnnotatedQueryByEmailAddress(String username); @@ -112,7 +113,9 @@ interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like ?1%") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + // ------------------------------------------------------------------------- // Value Expressions + // ------------------------------------------------------------------------- @Query("select u from #{#entityName} u where u.emailAddress = ?1") User findTemplatedByEmailAddress(String emailAddress); @@ -123,11 +126,58 @@ interface UserRepository extends CrudRepository { @Query("select u from User u where u.emailAddress = ?#{[0]} or u.firstname = ?${user.dir}") User findValueExpressionPositionalByEmailAddress(String emailAddress); + // ------------------------------------------------------------------------- + // Projections: DTO + // ------------------------------------------------------------------------- + + List findUserProjectionByLastnameStartingWith(String lastname); + + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + + Names findDtoByEmailAddress(String emailAddress); + + Page findDtoPageByEmailAddress(String emailAddress, Pageable pageable); + + @Query("select u from User u where u.emailAddress = ?1") + Names findAnnotatedDtoEmailAddress(String emailAddress); + + @Query("select u from User u where u.emailAddress = ?1") + Page findAnnotatedDtoPageByEmailAddress(String emailAddress, Pageable pageable); + @NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", sqlResultSetMapping = "emailDto") User.EmailDto findEmailDtoByNativeQuery(Integer id); - // modifying + @Query(name = "User.findByEmailAddress") + Names findNamedDtoEmailAddress(String emailAddress); + + // ------------------------------------------------------------------------- + // Projections: Interface + // ------------------------------------------------------------------------- + + EmailOnly findEmailProjectionById(Integer id); + + Page findProjectedPageByEmailAddress(String emailAddress, Pageable page); + + Slice findProjectedSliceByEmailAddress(String lastname, Pageable page); + + Stream streamProjectedByEmailAddress(String lastname); + + @Query("select u from User u where u.emailAddress = ?1") + EmailOnly findAnnotatedEmailProjectionByEmailAddress(String emailAddress); + + @Query("select u from User u where u.emailAddress = ?1") + Page findAnnotatedProjectedPageByEmailAddress(String emailAddress, Pageable page); + + @NativeQuery(value = "SELECT emailaddress as emailAddress FROM SD_User WHERE id = ?1") + EmailOnly findEmailProjectionByNativeQuery(Integer id); + + @Query(name = "User.findByEmailAddress") + EmailOnly findNamedProjectionEmailAddress(String emailAddress); + + // ------------------------------------------------------------------------- + // Modifying + // ------------------------------------------------------------------------- User deleteByEmailAddress(String username); @@ -136,24 +186,36 @@ interface UserRepository extends CrudRepository { @Query("delete from User u where u.emailAddress = ?1") User deleteAnnotatedQueryByEmailAddress(String username); - // native queries + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("update User u set u.lastname = ?1") + int renameAllUsersTo(String lastname); + + // ------------------------------------------------------------------------- + // Native Queries + // ------------------------------------------------------------------------- @Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User", nativeQuery = true) Page findByNativeQueryWithPageable(Pageable pageable); - // projections + // ------------------------------------------------------------------------- + // Named Queries + // ------------------------------------------------------------------------- - List findUserProjectionByLastnameStartingWith(String lastname); + User findByEmailAddress(String emailAddress); - Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + @Query(name = "User.findByEmailAddress") + Page findPagedByEmailAddress(Pageable pageable, String emailAddress); - // old ones + @Query(name = "User.findByEmailAddress", countQuery = "SELECT CoUnT(u) FROM User u WHERE u.emailAddress = ?1") + Page findPagedWithCountByEmailAddress(Pageable pageable, String emailAddress); - @Query("select u from User u where u.firstname = ?1") - List findAllUsingAnnotatedJpqlQuery(String firstname); + @Query(name = "User.findByEmailAddress", countName = "User.findByEmailAddress.count-provided") + Page findPagedWithNamedCountByEmailAddress(Pageable pageable, String emailAddress); - List findByLastname(String lastname); + // ------------------------------------------------------------------------- + // Query Hints + // ------------------------------------------------------------------------- @QueryHints(value = { @QueryHint(name = "jakarta.persistence.cache.storeMode", value = "foo") }, forCounting = false) List findHintedByLastname(String lastname); @@ -164,31 +226,14 @@ interface UserRepository extends CrudRepository { @EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = { "roles", "manager.roles" }) User findWithDeclaredEntityGraphByFirstname(String firstname); - List findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit); + @Query("select u from User u where u.emailAddress = ?1 AND TYPE(u) = ?2") + T findByEmailAddress(String emailAddress, Class type); - List findByLastname(String lastname, Sort sort); + interface EmailOnly { + String getEmailAddress(); + } - List findByLastname(String lastname, Pageable page); - - List findByLastnameOrderByFirstname(String lastname); - - /** - * Retrieve users by their email address. The finder {@literal User.findByEmailAddress} is declared as annotation at - * {@code User}. - */ - User findByEmailAddress(String emailAddress); - - @Query(name = "User.findByEmailAddress") - Page findPagedByEmailAddress(Pageable pageable, String emailAddress); - - @Query(name = "User.findByEmailAddress", countQuery = "SELECT CoUnT(u) FROM User u WHERE u.emailAddress = ?1") - Page findPagedWithCountByEmailAddress(Pageable pageable, String emailAddress); - - @Query(name = "User.findByEmailAddress", countName = "User.findByEmailAddress.count-provided") - Page findPagedWithNamedCountByEmailAddress(Pageable pageable, String emailAddress); - - @Modifying(flushAutomatically = true, clearAutomatically = true) - @Query("update User u set u.lastname = ?1") - int renameAllUsersTo(String lastname); + record Names(String firstname, String lastname) { + } } From 36552ddb8b43591dad9e71adabeed8d251c2dcf5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Mar 2025 16:57:39 +0100 Subject: [PATCH 62/94] Document AOT repositories. See #3830 --- src/main/antora/modules/ROOT/nav.adoc | 1 + .../antora/modules/ROOT/pages/jpa/aot.adoc | 200 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/main/antora/modules/ROOT/pages/jpa/aot.adoc diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 1e44d61f58..351c162366 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -25,6 +25,7 @@ ** xref:repositories/core-extensions.adoc[] ** xref:repositories/query-keywords-reference.adoc[] ** xref:repositories/query-return-types-reference.adoc[] +** xref:jpa/aot.adoc[] ** xref:jpa/faq.adoc[] ** xref:jpa/glossary.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc new file mode 100644 index 0000000000..145c19c950 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -0,0 +1,200 @@ += Ahead of Time Optimizations + +This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations]. + +[[aot.bestpractices]] +== Best Practices + +=== Annotate your Domain Types + +During application startup, Spring scans the classpath for domain classes for early processing of entities. +By annotating your domain types with Spring Data-specific `@Table`, `@Document` or `@Entity` annotations you can aid initial entity scanning and ensure that those types are registered with `ManagedTypes` for Runtime Hints. +Classpath scanning is not possible in native image arrangements and so Spring has to use `ManagedTypes` for the initial entity set. + +[[aot.hints]] +== Runtime Hints + +Running an application as a native image requires additional information compared to a regular JVM runtime. +Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage. +These are in particular hints for: + +* Auditing +* `ManagedTypes` to capture the outcome of class-path scans +* Repositories +** Reflection hints for entities, return types, and Spring Data annotations +** Repository fragments +** Querydsl `Q` classes +** Kotlin Coroutine support +* Web support (Jackson Hints for `PagedModel`) + +[[aot.repositories]] +== Ahead of Time Repositories + +AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations. +Query methods are opaque to developers regarding their underlying queries being executed in a query method call. +AOT repositories contribute query method implementations based on derived, annotated, and named queries that are known at build-time. +This optimization moves query method processing from runtime to build-time, which can lead to a significant performance improvement as query methods do not need to be analyzed reflectively upon each application start. + +The resulting AOT repository fragment follows the naming scheme of `Impl__Aot` and is placed in the same package as the repository interface. +You can find all queries in their String form for generated repository query methods. + +=== Running with AOT Repositories + +AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode. +It is also possible to use those optimizations on the JVM by setting the `spring.aot.enabled` and `spring.aot.repositories.enabled` properties to `true`. + +AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment. + +NOTE: When AOT optimizations are included, some decisions that have been taken at build-time are hard-coded in the application setup. +For instance, profiles that have been enabled at build-time are automatically enabled at runtime as well. +Also, the Spring Data module implementing a repository is fixed. +Changing the implementation requires AOT re-processing. + +=== Eligible Methods + +AOT repositories filter methods that are eligible for AOT processing. +These are typically all query methods that are not backed by an xref:repositories/custom-implementations.adoc[implementation fragment]. + +**Supported Features** + +* Derived query methods, `@Query`/`@NativeQuery` and named query methods +* `@Modifying` methods returning `void` or `int` +* `@QueryHints` support +* Pagination, `Slice`, `Stream`, and `Optional` return types +* Sort query rewriting +* DTO Projections +* Value Expressions (Those require a bit of reflective information. +Mind that using Value Expressions requires expression parsing and contextual information to evaluate the expression) + + +**Limitations** + +* Requires Hibernate for AOT processing. +* Configuration of `escapeCharacter` and `queryEnhancerSelector` are not yet considered +* `QueryRewriter` must be a no-args class. `QueryRewriter` beans are not yet supported. +* Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) are not yet supported + +**Excluded methods** + +* `CrudRepository` and other base interface methods +* Querydsl and Query by Example methods +* Methods whose implementation would be overly complex +** Methods accepting `ScrollPosition (e.g. `Keyset` pagination) +** Stored procedure query methods annotated with `@Procedure` +** For now: Dynamic and interface projections + +[[aot.repositories.json]] +== Repository Metadata + +AOT processing introspects query methods and collects metadata about repository queries. +Spring Data JPA stores this metadata in JSON files that are named like the repository interface and stored next to it (i.e. within the same package). +Repository JSON Metadata contains details about queries and fragments. +An example for the following repository is shown below: + +==== +[source,java] +---- +interface UserRepository extends CrudRepository { + + List findUserNoArgumentsBy(); <1> + + Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); <2> + + @Query("select u from User u where u.emailAddress = ?1") + User findAnnotatedQueryByEmailAddress(String username); <3> + + User findByEmailAddress(String emailAddress); <4> + + @Procedure(value = "sp_add") + Integer providedProcedure(@Param("arg") Integer arg); <5> +} +---- + +<1> Derived query without arguments. +<2> Derived query using pagination. +<3> Annotated query. +<4> Named query. +<5> Stored procedure with a provided procedure name. +While stored procedure methods are included in JSON metadata, their method code blocks are not generated in AOT repositories. +==== + +[source,json] +---- +{ + "name": "com.acme.UserRepository", + "module": "", + "type": "IMPERATIVE", + "methods": [ + { + "name": "findUserNoArgumentsBy", + "signature": "public abstract java.util.List com.acme.UserRepository.findUserNoArgumentsBy()", + "query": { + "query": "SELECT u FROM com.acme.User u" + } + }, + { + "name": "findPageOfUsersByLastnameStartingWith", + "signature": "public abstract org.springframework.data.domain.Page com.acme.UserRepository.findPageOfUsersByLastnameStartingWith(java.lang.String,org.springframework.data.domain.Pageable)", + "query": { + "query": "SELECT u FROM com.acme.User u WHERE u.lastname LIKE ?1 ESCAPE '\\'", + "count-query": "SELECT COUNT(u) FROM com.acme.User u WHERE u.lastname LIKE ?1 ESCAPE '\\'" + } + }, + { + "name": "findAnnotatedQueryByEmailAddress", + "signature": "public abstract com.acme.User com.acme.UserRepository.findAnnotatedQueryByEmailAddress(java.lang.String)", + "query": { + "query": "select u from User u where u.emailAddress = ?1" + } + }, + { + "name": "findByEmailAddress", + "signature": "public abstract com.acme.User com.acme.UserRepository.findByEmailAddress(java.lang.String)", + "query": { + "name": "User.findByEmailAddress", + "query": "SELECT u FROM User u WHERE u.emailAddress = ?1" + } + }, + { + "name": "providedProcedure", + "signature": "public abstract java.lang.Integer com.acme.UserRepository.providedProcedure(java.lang.Integer)", + "query": { + "procedure": "sp_add" + } + }, + { + "name": "count", + "signature": "public abstract long org.springframework.data.repository.CrudRepository.count()", + "fragment": { + "fragment": "org.springframework.data.jpa.repository.support.SimpleJpaRepository" + } + } + ] +} +---- + +Queries may contain the following fields: + +* `query`: Query descriptor if the method is a query method. +** `name`: Name of the named query if the query is a named one. +** `query` the query used to obtain the query method result from `EntityManager` +** `count-name`: Name of the named count query if the count query is a named one. +** `count-query`: The count query used to obtain the count for query methods using pagination. +** `procedure-name`: Name of the named stored procedure if the stored procedure is a named one. +** `procedure`: Stored procedure name if the query method uses stored procedures. +* `fragment`: Target fragment if the method call is delegated to a store (repository base class, functional fragment such as Querydsl) or user fragment. +Fragments are either described with just `fragment` if there is no further interface or as `interface` and `fragment` tuple in case there is an interface (such as Querydsl or user-declared fragment interface). + +[NOTE] +.Normalized Query Form +==== +Static analysis of queries allows only a limited representation of runtime query behavior. +Queries are represented in their normalized (pre-parsed and rewritten) form: + +* Value Expressions are replaced with bind markers. +* Queries follow the specified query language (JPQL or native) and do not represent the final SQL query. +Spring Data cannot derive the final SQL queries as this is database-specific and depends on the actual runtime environment and parameters (e.g. Entity Graphs, Lazy Loading). +* Query Metadata does not reflect bind-value processing. +`StartingWith`/`EndingWith` queries prepend/append the wildcard character `%` to the actual bind value. +* Runtime Sort information cannot be incorporated in the query string itself as that detail is not known at build-time. +==== From 027a797dd2cfa1059a555aacd474fc7d9ccbbc56 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 16:06:10 +0200 Subject: [PATCH 63/94] Add support for QueryRewriter. See #3830 --- .../jpa/repository/aot/JpaCodeBlocks.java | 75 +++++++++++++++---- .../aot/JpaRepositoryContributor.java | 5 +- ...RepositoryContributorIntegrationTests.java | 13 ++++ .../jpa/repository/aot/UserRepository.java | 19 +++++ .../antora/modules/ROOT/pages/jpa/aot.adoc | 7 +- 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index 8dba827102..5dacdd7cb9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -35,6 +35,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.ParameterBinding; @@ -85,6 +86,7 @@ static class QueryBlockBuilder { private @Nullable AotEntityGraph entityGraph; private @Nullable String sqlResultSetMapping; private @Nullable Class queryReturnType; + private @Nullable Class queryRewriter = QueryRewriter.IdentityQueryRewriter.class; private QueryBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { this.context = context; @@ -126,6 +128,11 @@ public QueryBlockBuilder queryReturnType(@Nullable Class queryReturnType) { return this; } + public QueryBlockBuilder queryRewriter(@Nullable Class queryRewriter) { + this.queryRewriter = queryRewriter == null ? QueryRewriter.IdentityQueryRewriter.class : queryRewriter; + return this; + } + /** * Build the query block. * @@ -145,12 +152,20 @@ public CodeBlock build() { CodeBlock.Builder builder = CodeBlock.builder(); builder.add("\n"); - String queryStringNameVariableName = null; + String queryStringVariableName = null; + + String queryRewriterName = null; + + if (queries.result() instanceof StringAotQuery && queryRewriter != QueryRewriter.IdentityQueryRewriter.class) { + + queryRewriterName = "queryRewriter"; + builder.addStatement("$T $L = new $T()", queryRewriter, queryRewriterName, queryRewriter); + } if (queries != null && queries.result() instanceof StringAotQuery sq) { - queryStringNameVariableName = "%sString".formatted(queryVariableName); - builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, sq.getQueryString()); + queryStringVariableName = "%sString".formatted(queryVariableName); + builder.add(buildQueryString(sq, queryStringVariableName)); } String countQueryStringNameVariableName = null; @@ -159,7 +174,7 @@ public CodeBlock build() { if (queryMethod.isPageQuery() && queries.count() instanceof StringAotQuery sq) { countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName)); - builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, sq.getQueryString()); + builder.add(buildQueryString(sq, countQueryStringNameVariableName)); } String sortParameterName = context.getSortParameterName(); @@ -169,14 +184,14 @@ public CodeBlock build() { if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType)) && queries.result() instanceof StringAotQuery) { - builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringNameVariableName, actualReturnType)); + builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringVariableName, actualReturnType)); } if (queries.result().hasExpression() || queries.count().hasExpression()) { builder.addStatement("class ExpressionMarker{}"); } - builder.add(createQuery(false, queryVariableName, queryStringNameVariableName, queries.result(), + builder.add(createQuery(false, queryVariableName, queryStringVariableName, queryRewriterName, queries.result(), this.sqlResultSetMapping, this.queryHints, this.entityGraph, this.queryReturnType)); builder.add(applyLimits(queries.result().isExists())); @@ -187,7 +202,8 @@ public CodeBlock build() { boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); - builder.add(createQuery(true, countQueryVariableName, countQueryStringNameVariableName, queries.count(), null, + builder.add(createQuery(true, countQueryVariableName, countQueryStringNameVariableName, queryRewriterName, + queries.count(), null, queryHints ? this.queryHints : MergedAnnotation.missing(), null, Long.class)); builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQueryVariableName); @@ -199,6 +215,13 @@ public CodeBlock build() { return builder.build(); } + private CodeBlock buildQueryString(StringAotQuery sq, String queryStringVariableName) { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.addStatement("$T $L = $S", String.class, queryStringVariableName, sq.getQueryString()); + return builder.build(); + } + private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, String queryString, Class actualReturnType) { @@ -268,12 +291,14 @@ private CodeBlock applyLimits(boolean exists) { } private CodeBlock createQuery(boolean count, String queryVariableName, @Nullable String queryStringNameVariableName, - AotQuery query, @Nullable String sqlResultSetMapping, MergedAnnotation queryHints, + @Nullable String queryRewriterName, AotQuery query, @Nullable String sqlResultSetMapping, + MergedAnnotation queryHints, @Nullable AotEntityGraph entityGraph, @Nullable Class queryReturnType) { Builder builder = CodeBlock.builder(); - builder.add(doCreateQuery(count, queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping, + builder.add(doCreateQuery(count, queryVariableName, queryStringNameVariableName, queryRewriterName, query, + sqlResultSetMapping, queryReturnType)); if (entityGraph != null) { @@ -306,18 +331,36 @@ private CodeBlock createQuery(boolean count, String queryVariableName, @Nullable } private CodeBlock doCreateQuery(boolean count, String queryVariableName, - @Nullable String queryStringNameVariableName, AotQuery query, @Nullable String sqlResultSetMapping, + @Nullable String queryStringName, @Nullable String queryRewriterName, AotQuery query, + @Nullable String sqlResultSetMapping, @Nullable Class queryReturnType) { ReturnedType returnedType = context.getReturnedType(); Builder builder = CodeBlock.builder(); + String queryStringNameToUse = queryStringName; if (query instanceof StringAotQuery sq) { + if (StringUtils.hasText(queryRewriterName)) { + + queryStringNameToUse = queryStringName + "Rewritten"; + + if (StringUtils.hasText(context.getPageableParameterName())) { + builder.addStatement("$T $L = $L.rewrite($L, $L)", String.class, queryStringNameToUse, queryRewriterName, + queryStringName, context.getPageableParameterName()); + } else if (StringUtils.hasText(context.getSortParameterName())) { + builder.addStatement("$T $L = $L.rewrite($L, $L)", String.class, queryStringNameToUse, queryRewriterName, + queryStringName, context.getSortParameterName()); + } else { + builder.addStatement("$T $L = $L.rewrite($L, $T.unsorted())", String.class, queryStringNameToUse, + queryRewriterName, queryStringName, Sort.class); + } + } + if (StringUtils.hasText(sqlResultSetMapping)) { builder.addStatement("$T $L = this.$L.createNativeQuery($L, $S)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), queryStringNameVariableName, sqlResultSetMapping); + context.fieldNameOf(EntityManager.class), queryStringNameToUse, sqlResultSetMapping); return builder.build(); } @@ -327,10 +370,10 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, if (queryReturnType != null) { builder.addStatement("$T $L = this.$L.createNativeQuery($L, $T.class)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), queryStringNameVariableName, queryReturnType); + context.fieldNameOf(EntityManager.class), queryStringNameToUse, queryReturnType); } else { builder.addStatement("$T $L = this.$L.createNativeQuery($L)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), queryStringNameVariableName); + context.fieldNameOf(EntityManager.class), queryStringNameToUse); } return builder.build(); @@ -339,7 +382,7 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, if (sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) { builder.addStatement("$T $L = this.$L.createQuery($L)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), queryStringNameVariableName); + context.fieldNameOf(EntityManager.class), queryStringNameToUse); } else { String createQueryMethod = query.isNative() ? "createNativeQuery" : "createQuery"; @@ -347,10 +390,10 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, if (!sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) { builder.addStatement("$T $L = this.$L.$L($L, $T.class)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameVariableName, Tuple.class); + context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameToUse, Tuple.class); } else { builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, - context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameVariableName); + context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameToUse); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 157e77c5e1..53ce0f8cc1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -81,6 +81,8 @@ protected void customizeClass(RepositoryInformation information, AotRepositoryFr @Override protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + // TODO: BeanFactoryQueryRewriterProvider if there is a method using QueryRewriters. + constructorBuilder.addParameter("entityManager", EntityManager.class); constructorBuilder.addParameter("context", RepositoryFactoryBeanSupport.FragmentCreationContext.class); @@ -149,7 +151,8 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) .queryReturnType(QueriesFactory.getQueryReturnType(aotQueries.result(), returnedType, context)) - .nativeQuery(nativeQuery).queryHints(queryHints).entityGraph(aotEntityGraph).build()); + .nativeQuery(nativeQuery).queryHints(queryHints).entityGraph(aotEntityGraph) + .queryRewriter(query.isPresent() ? query.getClass("queryRewriter") : null).build()); body.add( JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java index aa2f2f0c58..9cbd86109e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java @@ -33,6 +33,7 @@ import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; 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.jpa.domain.sample.Role; @@ -624,6 +625,18 @@ void shouldQuerySubtype() { assertThat(result).isInstanceOf(SpecialUser.class); } + @Test + void shouldApplyQueryRewriter() { + + User result = fragment.findAndApplyQueryRewriter(kylo.getEmailAddress()); + + assertThat(result).isNotNull(); + + Page page = fragment.findAndApplyQueryRewriter(kylo.getEmailAddress(), Pageable.unpaged()); + + assertThat(page).isNotEmpty(); + } + void todo() { // dynamic projections: Not implemented diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index de4ae656df..3d5eeb930c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -32,6 +32,7 @@ import org.springframework.data.jpa.repository.NativeQuery; 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.repository.CrudRepository; /** @@ -229,6 +230,12 @@ interface UserRepository extends CrudRepository { @Query("select u from User u where u.emailAddress = ?1 AND TYPE(u) = ?2") T findByEmailAddress(String emailAddress, Class type); + @Query(value = "select u from PLACEHOLDER u where u.emailAddress = ?1", queryRewriter = MyQueryRewriter.class) + User findAndApplyQueryRewriter(String emailAddress); + + @Query(value = "select u from OTHER u where u.emailAddress = ?1", queryRewriter = MyQueryRewriter.class) + Page findAndApplyQueryRewriter(String emailAddress, Pageable pageable); + interface EmailOnly { String getEmailAddress(); } @@ -236,4 +243,16 @@ interface EmailOnly { record Names(String firstname, String lastname) { } + static class MyQueryRewriter implements QueryRewriter { + + @Override + public String rewrite(String query, Sort sort) { + return query.replaceAll("PLACEHOLDER", "User"); + } + + @Override + public String rewrite(String query, Pageable pageRequest) { + return query.replaceAll("OTHER", "User"); + } + } } diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc index 145c19c950..031a75f527 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -38,6 +38,9 @@ This optimization moves query method processing from runtime to build-time, whic The resulting AOT repository fragment follows the naming scheme of `Impl__Aot` and is placed in the same package as the repository interface. You can find all queries in their String form for generated repository query methods. +NOTE: Consider AOT repository classes an internal optimization. +Do not use them directly in your code as generation and implementation details may change in future releases. + === Running with AOT Repositories AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode. @@ -79,9 +82,9 @@ Mind that using Value Expressions requires expression parsing and contextual inf * `CrudRepository` and other base interface methods * Querydsl and Query by Example methods * Methods whose implementation would be overly complex -** Methods accepting `ScrollPosition (e.g. `Keyset` pagination) +** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) ** Stored procedure query methods annotated with `@Procedure` -** For now: Dynamic and interface projections +** Dynamic projections [[aot.repositories.json]] == Repository Metadata From cfe681a200be21349f2b81c2910d3d6f16391bb4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 13:00:19 +0200 Subject: [PATCH 64/94] Add AOT repository benchmarks. See #3830 --- pom.xml | 17 ++ .../AotRepositoryQueryMethodBenchmarks.java | 260 ++++++++++++++++++ ...a => RepositoryQueryMethodBenchmarks.java} | 23 +- .../aot/JpaRepositoryContributor.java | 11 +- .../jpa/repository/aot/QueriesFactory.java | 12 +- .../aot/StubRepositoryInformation.java | 4 +- .../aot/TestJpaAotRepositoryContext.java | 2 +- .../src/test/resources/logback.xml | 2 +- 8 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java rename spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/{RepositoryFinderBenchmarks.java => RepositoryQueryMethodBenchmarks.java} (93%) diff --git a/pom.xml b/pom.xml index f6b6179301..2b0f8c1740 100755 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,23 @@ + + jmh + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + 0.4.0.RELEASE + test + + + + + jitpack.io + https://jitpack.io + + + hibernate-70-snapshots diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java new file mode 100644 index 0000000000..a8682d32c3 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/AotRepositoryQueryMethodBenchmarks.java @@ -0,0 +1,260 @@ +/* + * Copyright 2024-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.benchmark; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Query; + +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.benchmark.model.Person; +import org.springframework.data.jpa.benchmark.model.Profile; +import org.springframework.data.jpa.benchmark.repository.PersonRepository; +import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; +import org.springframework.data.jpa.repository.aot.TestJpaAotRepositoryContext; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @author Mark Paluch + */ +@Testable +@Fork(1) +@Warmup(time = 1, iterations = 3) +@Measurement(time = 1, iterations = 3) +@Timeout(time = 2) +public class AotRepositoryQueryMethodBenchmarks { + + private static final String PERSON_FIRSTNAME = "first"; + private static final String COLUMN_PERSON_FIRSTNAME = "firstname"; + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + public static Class aot; + public static TestJpaAotRepositoryContext repositoryContext = new TestJpaAotRepositoryContext<>( + PersonRepository.class, null); + + EntityManager entityManager; + RepositoryComposition.RepositoryFragments fragments; + PersonRepository repositoryProxy; + + @Setup(Level.Iteration) + public void doSetup() { + + createEntityManager(); + + if (!entityManager.getTransaction().isActive()) { + + if (ObjectUtils.nullSafeEquals( + entityManager.createNativeQuery("SELECT COUNT(*) FROM person", Integer.class).getSingleResult(), + Integer.valueOf(0))) { + + entityManager.getTransaction().begin(); + + Profile generalProfile = new Profile("general"); + Profile sdUserProfile = new Profile("sd-user"); + + entityManager.persist(generalProfile); + entityManager.persist(sdUserProfile); + + Person person = new Person(PERSON_FIRSTNAME, "last"); + person.setProfiles(Set.of(generalProfile, sdUserProfile)); + entityManager.persist(person); + entityManager.getTransaction().commit(); + } + } + + if (this.aot == null) { + + TestGenerationContext generationContext = new TestGenerationContext(PersonRepository.class); + + new JpaRepositoryContributor(repositoryContext, entityManager.getEntityManagerFactory()) + .contribute(generationContext); + + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { + + try { + this.aot = compiled.getClassLoader().loadClass(PersonRepository.class.getName() + "Impl__Aot"); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + try { + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = getCreationContext(repositoryContext); + fragments = RepositoryComposition.RepositoryFragments + .just(aot.getConstructor(EntityManager.class, RepositoryFactoryBeanSupport.FragmentCreationContext.class) + .newInstance(entityManager, creationContext)); + + this.repositoryProxy = createRepository(fragments); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( + TestJpaAotRepositoryContext repositoryContext) { + + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() { + @Override + public RepositoryMetadata getRepositoryMetadata() { + return repositoryContext.getRepositoryInformation(); + } + + @Override + public ValueExpressionDelegate getValueExpressionDelegate() { + return ValueExpressionDelegate.create(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return new SpelAwareProxyProjectionFactory(); + } + }; + + return creationContext; + } + + @TearDown(Level.Iteration) + public void doTearDown() { + entityManager.close(); + } + + private void createEntityManager() { + + LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); + factoryBean.setPersistenceUnitName("benchmark"); + factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + factoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); + factoryBean.setPersistenceXmlLocation("classpath*:META-INF/persistence-jmh.xml"); + factoryBean.setMappingResources("classpath*:META-INF/orm-jmh.xml"); + + Properties properties = new Properties(); + properties.put("jakarta.persistence.jdbc.url", "jdbc:h2:mem:test"); + properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + properties.put("hibernate.hbm2ddl.auto", "update"); + properties.put("hibernate.xml_mapping_enabled", "false"); + + factoryBean.setJpaProperties(properties); + factoryBean.afterPropertiesSet(); + + EntityManagerFactory entityManagerFactory = factoryBean.getObject(); + entityManager = entityManagerFactory.createEntityManager(); + } + + public PersonRepository createRepository(RepositoryComposition.RepositoryFragments fragments) { + JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager); + return repositoryFactory.getRepository(PersonRepository.class, fragments); + } + + } + + protected PersonRepository doCreateRepository(EntityManager entityManager) { + JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager); + return repositoryFactory.getRepository(PersonRepository.class); + } + + @Benchmark + public PersonRepository repositoryBootstrap(BenchmarkParameters parameters) { + return parameters.createRepository(parameters.fragments); + } + + @Benchmark + public List baselineEntityManagerHQLQuery(BenchmarkParameters parameters) { + + Query query = parameters.entityManager + .createQuery("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1"); + query.setParameter(1, PERSON_FIRSTNAME); + + return query.getResultList(); + } + + @Benchmark + public Long baselineEntityManagerCount(BenchmarkParameters parameters) { + + Query query = parameters.entityManager.createQuery( + "SELECT COUNT(*) FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1"); + query.setParameter(1, PERSON_FIRSTNAME); + + return (Long) query.getSingleResult(); + } + + @Benchmark + public List derivedFinderMethod(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME); + } + + /*@Benchmark + public List derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME); + } */ + + @Benchmark + public List stringBasedQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public List stringBasedQueryDynamicSort(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, + Sort.by(COLUMN_PERSON_FIRSTNAME)); + } + + @Benchmark + public List stringBasedNativeQuery(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithNativeQueryByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public Long derivedCount(BenchmarkParameters parameters) { + return parameters.repositoryProxy.countByFirstname(PERSON_FIRSTNAME); + } + + @Benchmark + public Long stringBasedCount(BenchmarkParameters parameters) { + return parameters.repositoryProxy.countWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME); + } +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java similarity index 93% rename from spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java rename to spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java index 209dc55318..f49d658a00 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryFinderBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java @@ -41,7 +41,6 @@ import org.openjdk.jmh.annotations.Warmup; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.benchmark.model.IPersonProjection; import org.springframework.data.jpa.benchmark.model.Person; import org.springframework.data.jpa.benchmark.model.Profile; import org.springframework.data.jpa.benchmark.repository.PersonRepository; @@ -52,13 +51,14 @@ /** * @author Christoph Strobl + * @author Mark Paluch */ @Testable @Fork(1) -@Warmup(time = 2, iterations = 3) -@Measurement(time = 2) +@Warmup(time = 1, iterations = 3) +@Measurement(time = 1, iterations = 3) @Timeout(time = 2) -public class RepositoryFinderBenchmarks { +public class RepositoryQueryMethodBenchmarks { private static final String PERSON_FIRSTNAME = "first"; private static final String COLUMN_PERSON_FIRSTNAME = "firstname"; @@ -125,10 +125,16 @@ private void createEntityManager() { entityManager = entityManagerFactory.createEntityManager(); } - PersonRepository createRepository() { + public PersonRepository createRepository() { JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager); return repositoryFactory.getRepository(PersonRepository.class); } + + } + + protected PersonRepository doCreateRepository(EntityManager entityManager) { + JpaRepositoryFactory repositoryFactory = new JpaRepositoryFactory(entityManager); + return repositoryFactory.getRepository(PersonRepository.class); } @Benchmark @@ -173,10 +179,10 @@ public List derivedFinderMethod(BenchmarkParameters parameters) { return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME); } - @Benchmark + /*@Benchmark public List derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) { return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME); - } + } */ @Benchmark public List stringBasedQuery(BenchmarkParameters parameters) { @@ -185,7 +191,8 @@ public List stringBasedQuery(BenchmarkParameters parameters) { @Benchmark public List stringBasedQueryDynamicSort(BenchmarkParameters parameters) { - return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, Sort.by(COLUMN_PERSON_FIRSTNAME)); + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, + Sort.by(COLUMN_PERSON_FIRSTNAME)); } @Benchmark diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 53ce0f8cc1..b5ab480311 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.aot; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import java.lang.reflect.Method; @@ -68,10 +69,18 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes()); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory()); - this.queriesFactory = new QueriesFactory(amm, amm.getEntityManagerFactory()); + this.queriesFactory = new QueriesFactory(amm.getEntityManagerFactory(), amm); this.entityGraphLookup = new EntityGraphLookup(amm.getEntityManagerFactory()); } + public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { + super(repositoryContext); + + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); + this.queriesFactory = new QueriesFactory(entityManagerFactory); + this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory); + } + @Override protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata, TypeSpec.Builder builder) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java index 1188ec7d23..f31d437fcf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java @@ -51,12 +51,16 @@ */ class QueriesFactory { + private final EntityManagerFactory entityManagerFactory; private final Metamodel metamodel; - private final EntityManagerFactory emf; - public QueriesFactory(AotMetamodel metamodel, EntityManagerFactory emf) { + public QueriesFactory(EntityManagerFactory entityManagerFactory) { + this(entityManagerFactory, entityManagerFactory.getMetamodel()); + } + + public QueriesFactory(EntityManagerFactory entityManagerFactory, Metamodel metamodel) { this.metamodel = metamodel; - this.emf = emf; + this.entityManagerFactory = entityManagerFactory; } /** @@ -183,7 +187,7 @@ private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQ for (Class candidate : candidates) { - Map> namedQueries = emf.getNamedQueries(candidate); + Map> namedQueries = entityManagerFactory.getNamedQueries(candidate); if (namedQueries.containsKey(queryName)) { return namedQueries.get(queryName); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java index 6e9b1d900c..f9c4042d36 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.aot; import java.lang.reflect.Method; +import java.util.List; import java.util.Set; import org.jspecify.annotations.Nullable; @@ -27,7 +28,6 @@ import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; /** @@ -111,7 +111,7 @@ public boolean isQueryMethod(Method method) { } @Override - public Streamable getQueryMethods() { + public List getQueryMethods() { return null; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index 0aeaba3644..aaf2e5218f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -37,7 +37,7 @@ /** * @author Christoph Strobl */ -class TestJpaAotRepositoryContext implements AotRepositoryContext { +public class TestJpaAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; private final Class repositoryInterface; diff --git a/spring-data-jpa/src/test/resources/logback.xml b/spring-data-jpa/src/test/resources/logback.xml index 780ba5e8fd..2df750b92a 100644 --- a/spring-data-jpa/src/test/resources/logback.xml +++ b/spring-data-jpa/src/test/resources/logback.xml @@ -19,7 +19,7 @@ - + From f9cdca1f6e6aaccac129ae5b45068cd340e4c973 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 7 Apr 2025 12:21:14 +0200 Subject: [PATCH 65/94] Add support for JSON repository metadata. See #3830 --- spring-data-jpa/pom.xml | 25 ++- .../data/jpa/repository/aot/AotQueries.java | 51 +++++ .../aot/JpaRepositoryContributor.java | 64 ++++++- .../jpa/repository/aot/NamedAotQuery.java | 14 +- .../jpa/repository/aot/QueriesFactory.java | 12 +- .../support/JpaRepositoryFactory.java | 4 +- .../support/JpaRepositoryFactoryBean.java | 6 +- ...RepositoryContributorIntegrationTests.java | 118 ++++++------ ...JpaRepositoryMetadataIntegrationTests.java | 178 ++++++++++++++++++ .../aot/StubRepositoryInformation.java | 13 +- .../jpa/repository/aot/UserRepository.java | 11 ++ 11 files changed, 410 insertions(+), 86 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 12a089e3e4..1cc6674063 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -88,16 +88,31 @@ true - - org.junit.platform - junit-platform-launcher - test - + + org.junit.platform + junit-platform-launcher + test + + org.springframework spring-core-test test + + + net.javacrumbs.json-unit + json-unit-assertj + 4.1.0 + test + + + + com.fasterxml.jackson.core + jackson-databind + test + + org.hsqldb hsqldb diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java index 0b900c72a5..51d639ea78 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotQueries.java @@ -16,6 +16,8 @@ package org.springframework.data.jpa.repository.aot; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -23,6 +25,7 @@ import org.springframework.data.jpa.repository.query.DeclaredQuery; import org.springframework.data.jpa.repository.query.QueryEnhancer; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.data.repository.aot.generate.QueryMetadata; import org.springframework.util.StringUtils; /** @@ -68,4 +71,52 @@ public boolean isNative() { return result().isNative(); } + public QueryMetadata toMetadata(boolean paging) { + return new AotQueryMetadata(paging); + } + + /** + * String and Named Query-based {@link QueryMetadata}. + */ + private class AotQueryMetadata implements QueryMetadata { + + private final boolean paging; + + AotQueryMetadata(boolean paging) { + this.paging = paging; + } + + @Override + public Map serialize() { + + Map serialized = new LinkedHashMap<>(); + + if (result() instanceof NamedAotQuery nq) { + + serialized.put("name", nq.getName()); + serialized.put("query", nq.getQueryString()); + } + + if (result() instanceof StringAotQuery sq) { + serialized.put("query", sq.getQueryString()); + } + + if (paging) { + + if (count() instanceof NamedAotQuery nq) { + + serialized.put("count-name", nq.getName()); + serialized.put("count-query", nq.getQueryString()); + } + + if (count() instanceof StringAotQuery sq) { + serialized.put("count-query", sq.getQueryString()); + } + } + + return serialized; + } + + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index b5ab480311..785c9133c6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -19,10 +19,13 @@ import jakarta.persistence.EntityManagerFactory; import java.lang.reflect.Method; +import java.util.Map; import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; @@ -31,10 +34,12 @@ import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.query.JpaParameters; import org.springframework.data.jpa.repository.query.JpaQueryMethod; +import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata; import org.springframework.data.repository.aot.generate.MethodContributor; +import org.springframework.data.repository.aot.generate.QueryMetadata; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; @@ -46,6 +51,7 @@ import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * JPA-specific {@link RepositoryContributor} contributing an AOT repository fragment using the {@link EntityManager} @@ -113,20 +119,50 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB // no stored procedures for now. if (queryMethod.isProcedureQuery()) { + + Procedure procedure = AnnotatedElementUtils.findMergedAnnotation(method, Procedure.class); + + MethodContributor.QueryMethodMetadataContributorBuilder builder = MethodContributor + .forQueryMethod(queryMethod); + + + if (procedure != null) { + + if (StringUtils.hasText(procedure.name())) { + return builder.metadataOnly(new NamedStoredProcedureMetadata(procedure.name())); + } + + if (StringUtils.hasText(procedure.procedureName())) { + return builder.metadataOnly(new StoredProcedureMetadata(procedure.procedureName())); + } + + if (StringUtils.hasText(procedure.value())) { + return builder.metadataOnly(new StoredProcedureMetadata(procedure.value())); + } + } + + // TODO: Better fallback. return null; } ReturnedType returnedType = queryMethod.getResultProcessor().getReturnedType(); JpaParameters parameters = queryMethod.getParameters(); + MergedAnnotation query = MergedAnnotations.from(method).get(Query.class); + + AotQueries aotQueries = queriesFactory.createQueries(repositoryInformation, query, selector, queryMethod, + returnedType); + // no KeysetScrolling for now. if (parameters.hasScrollPositionParameter()) { - return null; + return MethodContributor.forQueryMethod(queryMethod) + .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery())); } // no dynamic projections. if (parameters.hasDynamicProjection()) { - return null; + return MethodContributor.forQueryMethod(queryMethod) + .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery())); } if (queryMethod.isModifyingQuery()) { @@ -138,15 +174,16 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB boolean isVoid = ClassUtils.isVoidType(returnType.getType()); if (!returnsCount && !isVoid) { - return null; + return MethodContributor.forQueryMethod(queryMethod) + .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery())); } } - return MethodContributor.forQueryMethod(queryMethod).contribute(context -> { + return MethodContributor.forQueryMethod(queryMethod).withMetadata(aotQueries.toMetadata(queryMethod.isPageQuery())) + .contribute(context -> { CodeBlock.Builder body = CodeBlock.builder(); - MergedAnnotation query = context.getAnnotation(Query.class); MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class); MergedAnnotation queryHints = context.getAnnotation(QueryHints.class); MergedAnnotation entityGraph = context.getAnnotation(EntityGraph.class); @@ -154,7 +191,6 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - AotQueries aotQueries = queriesFactory.createQueries(context, query, selector, queryMethod, returnedType); AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation, returnedType, queryMethod); @@ -170,4 +206,20 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB }); } + record StoredProcedureMetadata(String procedure) implements QueryMetadata { + + @Override + public Map serialize() { + return Map.of("procedure", procedure()); + } + } + + record NamedStoredProcedureMetadata(String procedureName) implements QueryMetadata { + + @Override + public Map serialize() { + return Map.of("procedure-name", procedureName()); + } + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java index 3f7b9293bb..e3813ce137 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/NamedAotQuery.java @@ -30,12 +30,12 @@ class NamedAotQuery extends AotQuery { private final String name; - private final DeclaredQuery queryString; + private final DeclaredQuery query; private NamedAotQuery(String name, DeclaredQuery queryString, List parameterBindings) { super(parameterBindings); this.name = name; - this.queryString = queryString; + this.query = queryString; } /** @@ -51,13 +51,17 @@ public String getName() { return name; } - public DeclaredQuery getQueryString() { - return queryString; + public DeclaredQuery getQuery() { + return query; + } + + public String getQueryString() { + return getQuery().getQueryString(); } @Override public boolean isNative() { - return queryString.isNative(); + return query.isNative(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java index f31d437fcf..05c49f1144 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java @@ -73,11 +73,11 @@ public QueriesFactory(EntityManagerFactory entityManagerFactory, Metamodel metam * @param returnedType * @return */ - public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedAnnotation query, + public AotQueries createQueries(RepositoryInformation repositoryInformation, MergedAnnotation query, QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { - return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query, + return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, queryMethod); } @@ -86,7 +86,7 @@ public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedA return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod); } - return buildPartTreeQuery(returnedType, context, query, queryMethod); + return buildPartTreeQuery(returnedType, repositoryInformation, query, queryMethod); } private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, @@ -159,7 +159,7 @@ private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelec String countProjection = query.isPresent() ? query.getString("countProjection") : null; return AotQueries.from(aotQuery, it -> { - return StringAotQuery.of(aotQuery.getQueryString()).getQuery(); + return StringAotQuery.of(aotQuery.getQuery()).getQuery(); }, countProjection, selector); } @@ -197,10 +197,10 @@ private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQ return null; } - private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context, + private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInformation repositoryInformation, MergedAnnotation query, JpaQueryMethod queryMethod) { - PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); + PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); // TODO make configurable JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 2e24577f8f..91314ed115 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -47,6 +47,7 @@ import org.springframework.data.repository.core.support.QueryCreationListener; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor; import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -298,7 +299,8 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata getEntityInformation(metadata.getDomainType()), entityManager, resolver, crudMethodMetadata); invokeAwareMethods(querydslJpaPredicateExecutor); - return RepositoryFragments.just(querydslJpaPredicateExecutor); + return RepositoryFragments + .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, querydslJpaPredicateExecutor)); } return RepositoryFragments.empty(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index 8e8200a371..a9d8622a4b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -20,11 +20,11 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; - -import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.data.jpa.repository.query.EscapeCharacter; @@ -54,7 +54,7 @@ public class JpaRepositoryFactoryBean, S, ID> private @Nullable BeanFactory beanFactory; private @Nullable EntityManager entityManager; - private EntityPathResolver entityPathResolver; + private EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; private @Nullable JpaQueryMethodFactory queryMethodFactory; private @Nullable Function queryEnhancerSelectorSource; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java index 9cbd86109e..566a08cb76 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java @@ -103,21 +103,21 @@ void beforeEach() { em.clear(); } - @Test + @Test // GH-3830 void testDerivedFinderWithoutArguments() { List users = fragment.findUserNoArgumentsBy(); assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); } - @Test + @Test // GH-3830 void testFindDerivedQuerySingleEntity() { User user = fragment.findOneByEmailAddress("luke@jedi.org"); assertThat(user.getLastname()).isEqualTo("Skywalker"); } - @Test + @Test // GH-3830 void testFindDerivedFinderOptionalEntity() { Optional user = fragment.findOptionalOneByEmailAddress("yoda@jedi.org"); @@ -125,21 +125,21 @@ void testFindDerivedFinderOptionalEntity() { .hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda")); } - @Test + @Test // GH-3830 void testDerivedCount() { Long value = fragment.countUsersByLastname("Skywalker"); assertThat(value).isEqualTo(2L); } - @Test + @Test // GH-3830 void testDerivedExists() { Boolean exists = fragment.existsUserByLastname("Skywalker"); assertThat(exists).isTrue(); } - @Test + @Test // GH-3830 void testDerivedFinderReturningList() { List users = fragment.findByLastnameStartingWith("S"); @@ -147,7 +147,7 @@ void testDerivedFinderReturningList() { "kylo@new-empire.com", "han@smuggler.net"); } - @Test + @Test // GH-3830 void shouldReturnStream() { Stream users = fragment.streamByLastnameLike("S%"); @@ -155,14 +155,14 @@ void shouldReturnStream() { "kylo@new-empire.com", "han@smuggler.net"); } - @Test + @Test // GH-3830 void testLimitedDerivedFinder() { List users = fragment.findTop2ByLastnameStartingWith("S"); assertThat(users).hasSize(2); } - @Test + @Test // GH-3830 void testSortedDerivedFinder() { List users = fragment.findByLastnameStartingWithOrderByEmailAddress("S"); @@ -170,14 +170,14 @@ void testSortedDerivedFinder() { "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void testDerivedFinderWithLimitArgument() { List users = fragment.findByLastnameStartingWith("S", Limit.of(2)); assertThat(users).hasSize(2); } - @Test + @Test // GH-3830 void testDerivedFinderWithSort() { List users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress")); @@ -185,21 +185,21 @@ void testDerivedFinderWithSort() { "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void testDerivedFinderWithSortAndLimit() { List users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress"), Limit.of(2)); assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testDerivedFinderReturningListWithPageable() { List users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("emailAddress"))); assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testDerivedFinderReturningPage() { Page page = fragment.findPageOfUsersByLastnameStartingWith("S", @@ -211,7 +211,7 @@ void testDerivedFinderReturningPage() { "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testDerivedFinderReturningSlice() { Slice slice = fragment.findSliceOfUserByLastnameStartingWith("S", @@ -223,14 +223,14 @@ void testDerivedFinderReturningSlice() { "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderReturningSingleValueWithQuery() { User user = fragment.findAnnotatedQueryByEmailAddress("yoda@jedi.org"); assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda"); } - @Test + @Test // GH-3830 void testAnnotatedFinderReturningListWithQuery() { List users = fragment.findAnnotatedQueryByLastname("S"); @@ -238,7 +238,7 @@ void testAnnotatedFinderReturningListWithQuery() { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() { List users = fragment.findAnnotatedQueryByLastnameParameter("S"); @@ -246,7 +246,7 @@ void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void shouldApplyAnnotatedLikeStartsEnds() { // start with case @@ -260,7 +260,7 @@ void shouldApplyAnnotatedLikeStartsEnds() { "chewie@smuggler.net", "yoda@jedi.org"); } - @Test + @Test // GH-3830 void testAnnotatedMultilineFinderWithQuery() { List users = fragment.findAnnotatedMultilineQueryByLastname("S"); @@ -268,14 +268,14 @@ void testAnnotatedMultilineFinderWithQuery() { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderWithQueryAndLimit() { List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2)); assertThat(users).hasSize(2); } - @Test + @Test // GH-3830 void testAnnotatedFinderWithQueryAndSort() { List users = fragment.findAnnotatedQueryByLastname("S", Sort.by("emailAddress")); @@ -283,21 +283,21 @@ void testAnnotatedFinderWithQueryAndSort() { "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderWithQueryLimitAndSort() { List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("emailAddress")); assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderReturningListWithPageable() { List users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("emailAddress"))); assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderReturningPage() { Page page = fragment.findAnnotatedQueryPageOfUsersByLastname("S", @@ -309,7 +309,7 @@ void testAnnotatedFinderReturningPage() { "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void testPagingAnnotatedQueryWithSort() { Page page = fragment.findAnnotatedQueryPageWithStaticSort("S", PageRequest.of(0, 2, Sort.unsorted())); @@ -320,7 +320,7 @@ void testPagingAnnotatedQueryWithSort() { "vader@empire.com"); } - @Test + @Test // GH-3830 void testAnnotatedFinderReturningSlice() { Slice slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S", @@ -331,7 +331,7 @@ void testAnnotatedFinderReturningSlice() { "kylo@new-empire.com"); } - @Test + @Test // GH-3830 void shouldResolveTemplatedQuery() { User user = fragment.findTemplatedByEmailAddress("han@smuggler.net"); @@ -340,7 +340,7 @@ void shouldResolveTemplatedQuery() { assertThat(user.getFirstname()).isEqualTo("Han"); } - @Test + @Test // GH-3830 void shouldEvaluateExpressionByName() { User user = fragment.findValueExpressionNamedByEmailAddress("han@smuggler.net"); @@ -349,7 +349,7 @@ void shouldEvaluateExpressionByName() { assertThat(user.getFirstname()).isEqualTo("Han"); } - @Test + @Test // GH-3830 void shouldEvaluateExpressionByPosition() { User user = fragment.findValueExpressionPositionalByEmailAddress("han@smuggler.net"); @@ -358,7 +358,7 @@ void shouldEvaluateExpressionByPosition() { assertThat(user.getFirstname()).isEqualTo("Han"); } - @Test + @Test // GH-3830 void testDerivedFinderReturningListOfProjections() { List users = fragment.findUserProjectionByLastnameStartingWith("S"); @@ -366,7 +366,7 @@ void testDerivedFinderReturningListOfProjections() { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } - @Test + @Test // GH-3830 void testDerivedFinderReturningPageOfProjections() { Page page = fragment.findUserProjectionByLastnameStartingWith("S", @@ -383,7 +383,7 @@ void testDerivedFinderReturningPageOfProjections() { assertThat(noResults).isEmpty(); } - @Test + @Test // GH-3830 void shouldApplySqlResultSetMapping() { User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId()); @@ -391,7 +391,7 @@ void shouldApplySqlResultSetMapping() { assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyNamedDto() { // named queries cannot be rewritten @@ -399,7 +399,7 @@ void shouldApplyNamedDto() { .isThrownBy(() -> fragment.findNamedDtoEmailAddress(kylo.getEmailAddress())); } - @Test + @Test // GH-3830 void shouldApplyDerivedDto() { UserRepository.Names names = fragment.findDtoByEmailAddress(kylo.getEmailAddress()); @@ -408,7 +408,7 @@ void shouldApplyDerivedDto() { assertThat(names.firstname()).isEqualTo(kylo.getFirstname()); } - @Test + @Test // GH-3830 void shouldApplyDerivedDtoPage() { Page names = fragment.findDtoPageByEmailAddress(kylo.getEmailAddress(), PageRequest.of(0, 1)); @@ -417,7 +417,7 @@ void shouldApplyDerivedDtoPage() { assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname()); } - @Test + @Test // GH-3830 void shouldApplyAnnotatedDto() { UserRepository.Names names = fragment.findAnnotatedDtoEmailAddress(kylo.getEmailAddress()); @@ -426,7 +426,7 @@ void shouldApplyAnnotatedDto() { assertThat(names.firstname()).isEqualTo(kylo.getFirstname()); } - @Test + @Test // GH-3830 void shouldApplyAnnotatedDtoPage() { Page names = fragment.findAnnotatedDtoPageByEmailAddress(kylo.getEmailAddress(), @@ -436,7 +436,7 @@ void shouldApplyAnnotatedDtoPage() { assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname()); } - @Test + @Test // GH-3830 void shouldApplyDerivedQueryInterfaceProjection() { UserRepository.EmailOnly result = fragment.findEmailProjectionById(kylo.getId()); @@ -444,7 +444,7 @@ void shouldApplyDerivedQueryInterfaceProjection() { assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyInterfaceProjectionPage() { Page result = fragment.findProjectedPageByEmailAddress(kylo.getEmailAddress(), @@ -454,7 +454,7 @@ void shouldApplyInterfaceProjectionPage() { assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyInterfaceProjectionSlice() { Slice result = fragment.findProjectedSliceByEmailAddress(kylo.getEmailAddress(), @@ -464,7 +464,7 @@ void shouldApplyInterfaceProjectionSlice() { assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyInterfaceProjectionToDerivedQueryStream() { Stream result = fragment.streamProjectedByEmailAddress(kylo.getEmailAddress()); @@ -472,7 +472,7 @@ void shouldApplyInterfaceProjectionToDerivedQueryStream() { assertThat(result).hasSize(1).map(UserRepository.EmailOnly::getEmailAddress).contains(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyAnnotatedQueryInterfaceProjection() { UserRepository.EmailOnly result = fragment.findAnnotatedEmailProjectionByEmailAddress(kylo.getEmailAddress()); @@ -480,7 +480,7 @@ void shouldApplyAnnotatedQueryInterfaceProjection() { assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyAnnotatedInterfaceProjectionQueryPage() { Page result = fragment.findAnnotatedProjectedPageByEmailAddress(kylo.getEmailAddress(), @@ -490,7 +490,7 @@ void shouldApplyAnnotatedInterfaceProjectionQueryPage() { assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyNativeInterfaceProjection() { UserRepository.EmailOnly result = fragment.findEmailProjectionByNativeQuery(kylo.getId()); @@ -498,7 +498,7 @@ void shouldApplyNativeInterfaceProjection() { assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void shouldApplyNamedQueryInterfaceProjection() { UserRepository.EmailOnly result = fragment.findNamedProjectionEmailAddress(kylo.getEmailAddress()); @@ -506,7 +506,7 @@ void shouldApplyNamedQueryInterfaceProjection() { assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress()); } - @Test + @Test // GH-3830 void testDerivedDeleteSingle() { User result = fragment.deleteByEmailAddress("yoda@jedi.org"); @@ -519,14 +519,14 @@ void testDerivedDeleteSingle() { assertThat(yodaShouldBeGone).isNull(); } - @Test + @Test // GH-3830 void shouldOmitAnnotatedDeleteReturningDomainType() { assertThatException().isThrownBy(() -> fragment.deleteAnnotatedQueryByEmailAddress("foo")) .withRootCauseInstanceOf(NoSuchMethodException.class); } - @Test + @Test // GH-3830 void shouldApplyModifying() { int affected = fragment.renameAllUsersTo("Jones"); @@ -539,7 +539,7 @@ void shouldApplyModifying() { assertThat(yodaShouldBeGone).isNull(); } - @Test + @Test // GH-3830 void nativeQuery() { Page page = fragment.findByNativeQueryWithPageable(PageRequest.of(0, 2)); @@ -549,14 +549,14 @@ void nativeQuery() { assertThat(page.getContent()).containsExactly("Anakin", "Ben"); } - @Test + @Test // GH-3830 void shouldUseNamedQuery() { User user = fragment.findByEmailAddress("luke@jedi.org"); assertThat(user.getLastname()).isEqualTo("Skywalker"); } - @Test + @Test // GH-3830 void shouldUseNamedQueryAndDeriveCountQuery() { Page user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); @@ -565,7 +565,7 @@ void shouldUseNamedQueryAndDeriveCountQuery() { assertThat(user.getTotalElements()).isEqualTo(1); } - @Test + @Test // GH-3830 void shouldUseNamedQueryAndProvidedCountQuery() { Page user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); @@ -574,7 +574,7 @@ void shouldUseNamedQueryAndProvidedCountQuery() { assertThat(user.getTotalElements()).isEqualTo(1); } - @Test + @Test // GH-3830 void shouldUseNamedQueryAndNamedCountQuery() { Page user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org"); @@ -583,13 +583,13 @@ void shouldUseNamedQueryAndNamedCountQuery() { assertThat(user.getTotalElements()).isEqualTo(1); } - @Test + @Test // GH-3830 void shouldApplyQueryHints() { assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker")) .withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo"); } - @Test + @Test // GH-3830 void shouldApplyNamedEntityGraph() { User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca"); @@ -598,7 +598,7 @@ void shouldApplyNamedEntityGraph() { assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class); } - @Test + @Test // GH-3830 void shouldApplyDeclaredEntityGraph() { User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca"); @@ -610,7 +610,7 @@ void shouldApplyDeclaredEntityGraph() { assertThat(han.getManager()).isInstanceOf(HibernateProxy.class); } - @Test + @Test // GH-3830 void shouldQuerySubtype() { SpecialUser snoopy = new SpecialUser(); @@ -625,7 +625,7 @@ void shouldQuerySubtype() { assertThat(result).isInstanceOf(SpecialUser.class); } - @Test + @Test // GH-3830 void shouldApplyQueryRewriter() { User result = fragment.findAndApplyQueryRewriter(kylo.getEmailAddress()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java new file mode 100644 index 0000000000..0cdde1ef04 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java @@ -0,0 +1,178 @@ +/* + * 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.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link UserRepository} JSON metadata. + * + * @author Mark Paluch + */ +@SpringJUnitConfig(classes = JpaRepositoryMetadataIntegrationTests.JpaRepositoryContributorConfiguration.class) +@Transactional +class JpaRepositoryMetadataIntegrationTests { + + @Autowired AbstractApplicationContext context; + + @Configuration + static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + public JpaRepositoryContributorConfiguration() { + super(UserRepository.class); + } + } + + @Test // GH-3830 + void shouldDocumentBase() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", UserRepository.class.getName()) // + .containsEntry("module", "") // TODO: JPA should be here + .containsEntry("type", "IMPERATIVE"); + } + + @Test // GH-3830 + void shouldDocumentDerivedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[0]").isObject().containsEntry("name", "countUsersByLastname"); + assertThatJson(json).inPath("$.methods[0].query").isObject().containsEntry("query", + "SELECT COUNT(u) FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname = ?1"); + } + + @Test // GH-3830 + void shouldDocumentPagedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findAndApplyQueryRewriter')].query").isArray().element(1) + .isObject().containsEntry("query", "select u from OTHER u where u.emailAddress = ?1") + .containsEntry("count-query", "select count(u) from OTHER u where u.emailAddress = ?1"); + } + + @Test // GH-3830 + void shouldDocumentQueryWithExpression() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findValueExpressionNamedByEmailAddress')].query").isArray() + .first().isObject().containsEntry("query", "select u from User u where u.emailAddress = :__$synthetic$__1"); + } + + @Test // GH-3830 + void shouldDocumentNamedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findPagedWithNamedCountByEmailAddress')].query").isArray() + .first().isObject().containsEntry("name", "User.findByEmailAddress") + .containsEntry("query", "SELECT u FROM User u WHERE u.emailAddress = ?1") + .containsEntry("count-name", "User.findByEmailAddress.count-provided") + .containsEntry("count-query", "SELECT count(u) FROM User u WHERE u.emailAddress = ?1"); + } + + @Test // GH-3830 + void shouldDocumentNamedProcedure() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'namedProcedure')].query").isArray().first().isObject() + .containsEntry("procedure-name", "User.plus1IO"); + } + + @Test // GH-3830 + void shouldDocumentProvidedProcedure() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'providedProcedure')].query").isArray().first().isObject() + .containsEntry("procedure", "sp_add"); + } + + @Test // GH-3830 + void shouldDocumentBaseFragment() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.jpa.repository.support.SimpleJpaRepository"); + } + + private Resource getResource() { + + String location = UserRepository.class.getPackageName().replace('.', '/') + "/" + + UserRepository.class.getSimpleName() + ".json"; + return new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location)); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java index f9c4042d36..589b95a5f7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java @@ -107,7 +107,12 @@ public boolean isCustomMethod(Method method) { @Override public boolean isQueryMethod(Method method) { - return false; + + if (isBaseClassMethod(method)) { + return false; + } + + return true; } @Override @@ -124,4 +129,10 @@ public Class getRepositoryBaseClass() { public Method getTargetClassMethod(Method method) { return null; } + + @Override + public RepositoryComposition getRepositoryComposition() { + return baseComposition; + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index 3d5eeb930c..b95cd88377 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -33,7 +33,9 @@ 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.jpa.repository.query.Procedure; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; /** * @author Christoph Strobl @@ -236,6 +238,15 @@ interface UserRepository extends CrudRepository { @Query(value = "select u from OTHER u where u.emailAddress = ?1", queryRewriter = MyQueryRewriter.class) Page findAndApplyQueryRewriter(String emailAddress, Pageable pageable); + // ------------------------------------------------------------------------- + // Unsupported: Procedures + // ------------------------------------------------------------------------- + @Procedure(name = "User.plus1IO") // Named + Integer namedProcedure(@Param("arg") Integer arg); + + @Procedure(value = "sp_add") // Stored procedure + Integer providedProcedure(@Param("arg") Integer arg); + interface EmailOnly { String getEmailAddress(); } From 09a1eb6a0a690ff3a6539b9fdf8c0a5e4f847d61 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 9 Apr 2025 16:51:30 +0200 Subject: [PATCH 66/94] Polishing. Run AotMetamodel against live EntityManagerFactory, use Environment to check for AOT repository enabled flag. See #3830 --- .../data/jpa/repository/aot/AotMetamodel.java | 4 +- .../jpa/repository/aot/JpaCodeBlocks.java | 11 +- .../aot/JpaRepositoryContributor.java | 2 - .../config/JpaRepositoryConfigExtension.java | 21 +- .../data/jpa/repository/query/NamedQuery.java | 25 +- .../query/ParameterBindingParser.java | 427 ------------------ .../aot/TestJpaAotRepositoryContext.java | 9 + ...toryRegistrationAotProcessorUnitTests.java | 7 + .../query/DefaultEntityQueryUnitTests.java | 1 - 9 files changed, 60 insertions(+), 447 deletions(-) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java index 8b68214ab5..3c1ddd6e33 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java @@ -96,7 +96,6 @@ public EntityManager entityManager() { return entityManager.get(); } - // TODO: Capture an existing factory bean (e.g. EntityManagerFactoryInfo) to extract PersistenceInfo public EntityManagerFactory getEntityManagerFactory() { return entityManagerFactory.get(); } @@ -125,7 +124,8 @@ public void addTransformer(ClassTransformer classTransformer) { public List getManagedClassNames() { return persistenceUnitInfo.getManagedClassNames(); } - }, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build(); + }, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect", "hibernate.boot.allow_jdbc_metadata_access", + "false")).build(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index 5dacdd7cb9..f9c9b45e6b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -45,6 +45,7 @@ import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -140,6 +141,8 @@ public QueryBlockBuilder queryRewriter(@Nullable Class queryRewriter) { */ public CodeBlock build() { + Assert.notNull(queries, "Queries must not be null"); + boolean isProjecting = context.getReturnedType().isProjecting(); Class actualReturnType = isProjecting ? context.getActualReturnType().toClass() : context.getRepositoryInformation().getDomainType(); @@ -153,7 +156,6 @@ public CodeBlock build() { builder.add("\n"); String queryStringVariableName = null; - String queryRewriterName = null; if (queries.result() instanceof StringAotQuery && queryRewriter != QueryRewriter.IdentityQueryRewriter.class) { @@ -162,7 +164,7 @@ public CodeBlock build() { builder.addStatement("$T $L = new $T()", queryRewriter, queryRewriterName, queryRewriter); } - if (queries != null && queries.result() instanceof StringAotQuery sq) { + if (queries.result() instanceof StringAotQuery sq) { queryStringVariableName = "%sString".formatted(queryVariableName); builder.add(buildQueryString(sq, queryStringVariableName)); @@ -183,7 +185,8 @@ public CodeBlock build() { } if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType)) - && queries.result() instanceof StringAotQuery) { + && queries != null && queries.result() instanceof StringAotQuery + && StringUtils.hasText(queryStringVariableName)) { builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringVariableName, actualReturnType)); } @@ -605,7 +608,7 @@ public CodeBlock build() { } } else if (aotQuery != null && aotQuery.isExists()) { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); - } else { + } else if (aotQuery != null) { if (context.getReturnedType().isProjecting()) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 785c9133c6..54ae048b59 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -189,8 +189,6 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB MergedAnnotation entityGraph = context.getAnnotation(EntityGraph.class); MergedAnnotation modifying = context.getAnnotation(Modifying.class); - body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation, returnedType, queryMethod); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 7de820f3e9..99eec50100 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -18,6 +18,7 @@ import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*; import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceUnit; @@ -38,6 +39,7 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -325,22 +327,29 @@ static boolean isActive(@Nullable ClassLoader classLoader) { */ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor { - protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { - // don't register domain types nor annotations. - - if (!AotContext.aotGeneratedRepositoriesEnabled()) { + boolean enabled = Boolean.parseBoolean( + repositoryContext.getEnvironment().getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + if (!enabled) { return null; } - return new JpaRepositoryContributor(repositoryContext); + ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); + EntityManagerFactory emf = beanFactory.getBeanProvider(EntityManagerFactory.class).getIfAvailable(); + + return emf != null ? new JpaRepositoryContributor(repositoryContext, emf) + : new JpaRepositoryContributor(repositoryContext); } @Nullable @Override + @SuppressWarnings("NullAway") protected RepositoryConfiguration getRepositoryMetadata(RegisteredBean bean) { RepositoryConfiguration configuration = super.getRepositoryMetadata(bean); - if (!configuration.getRepositoryBaseClassName().isEmpty()) { + + if (configuration != null && configuration.getRepositoryBaseClassName().isPresent()) { return configuration; } return new Meh<>(configuration); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index 5bf986d4ba..a38bf9eaaa 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -34,6 +34,7 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.Lazy; +import org.springframework.util.StringUtils; /** * Implementation of {@link RepositoryQuery} based on {@link jakarta.persistence.NamedQuery}s. @@ -97,12 +98,26 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, JpaQueryConfiguratio String queryString = extractor.extractQueryString(namedQuery); - // TODO: What is queryString is null? DeclaredQuery declaredQuery; - if (method.isNativeQuery() || (namedQuery != null && namedQuery.toString().contains("NativeQuery"))) { - declaredQuery = DeclaredQuery.nativeQuery(queryString); - } else { - declaredQuery = DeclaredQuery.jpqlQuery(queryString); + if (StringUtils.hasText(queryString)) { + if (method.isNativeQuery() || namedQuery.toString().contains("NativeQuery")) { + declaredQuery = DeclaredQuery.nativeQuery(queryString); + } else { + declaredQuery = DeclaredQuery.jpqlQuery(queryString); + } + } + else { + declaredQuery = new DeclaredQuery() { + @Override + public boolean isNative() { + return false; + } + + @Override + public String getQueryString() { + return ""; + } + }; } this.entityQuery = Lazy.of(() -> EntityQuery.create(declaredQuery, queryConfiguration.getSelector())); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java deleted file mode 100644 index 371016577c..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java +++ /dev/null @@ -1,427 +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 static java.util.regex.Pattern.CASE_INSENSITIVE; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -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.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * A parser that extracts the parameter bindings from a given query string. - * - * @author Thomas Darimont - */ -public enum ParameterBindingParser { - - INSTANCE; - - private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; - public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; - // .....................................................................^ not followed by a hash or a letter. - // .................................................................^ zero or more digits. - // .............................................................^ start with a question mark. - private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); - private static final Pattern PARAMETER_BINDING_PATTERN; - private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit] - private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit] - 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"; - private static final int INDEXED_PARAMETER_GROUP = 4; - private static final int NAMED_PARAMETER_GROUP = 6; - private static final int COMPARISION_TYPE_GROUP = 1; - - public static class Metadata { - private boolean usesJdbcStyleParameters = false; - - public boolean usesJdbcStyleParameters() { - return usesJdbcStyleParameters; - } - } - - /** - * 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}. - * - * @author Mark Paluch - * @since 3.1.2 - */ - static class ParameterBindings { - - private final MultiValueMap methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); - - private final Consumer registration; - private int syntheticParameterIndex; - - public ParameterBindings(List bindings, Consumer registration, - int syntheticParameterIndex) { - - for (ParameterBinding binding : bindings) { - this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding))); - } - - this.registration = registration; - this.syntheticParameterIndex = syntheticParameterIndex; - } - - /** - * Return whether the identifier is already bound. - * - * @param identifier - * @return - */ - public boolean isBound(BindingIdentifier identifier) { - return !getBindings(identifier).isEmpty(); - } - - BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, - Function bindingFactory) { - - Assert.isInstanceOf(MethodInvocationArgument.class, origin); - - BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier(); - List bindingsForOrigin = getBindings(methodArgument); - - if (!isBound(identifier)) { - - ParameterBinding binding = bindingFactory.apply(identifier); - registration.accept(binding); - bindingsForOrigin.add(binding); - return binding.getIdentifier(); - } - - ParameterBinding binding = bindingFactory.apply(identifier); - - for (ParameterBinding existing : bindingsForOrigin) { - - if (existing.isCompatibleWith(binding)) { - return existing.getIdentifier(); - } - } - - BindingIdentifier syntheticIdentifier; - if (identifier.hasName() && methodArgument.hasName()) { - - int index = 0; - String newName = methodArgument.getName(); - while (existsBoundParameter(newName)) { - index++; - newName = methodArgument.getName() + "_" + index; - } - syntheticIdentifier = BindingIdentifier.of(newName); - } else { - syntheticIdentifier = BindingIdentifier.of(++syntheticParameterIndex); - } - - ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); - registration.accept(newBinding); - bindingsForOrigin.add(newBinding); - return newBinding.getIdentifier(); - } - - private boolean existsBoundParameter(String key) { - return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream) - .anyMatch(it -> key.equals(it.getName())); - } - - private List getBindings(BindingIdentifier identifier) { - return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); - } - - public void register(ParameterBinding parameterBinding) { - registration.accept(parameterBinding); - } - } - - static { - - List keywords = new ArrayList<>(); - - for (ParameterBindingType type : ParameterBindingType.values()) { - if (type.getKeyword() != null) { - keywords.add(type.getKeyword()); - } - } - - StringBuilder builder = new StringBuilder(); - builder.append("("); - builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords - builder.append(")?"); - builder.append("(?: )?"); // some whitespace - builder.append("\\(?"); // optional braces around parameters - builder.append("("); - builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index - builder.append("|"); // or - - // named parameter and the parameter name - builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?"); - - builder.append(")"); - builder.append("\\)?"); // optional braces around parameters - - PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); - } - - /** - * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns - * the cleaned up query. - */ - public String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings, - Metadata queryMeta) { - - int greatestParameterIndex = tryFindGreatestParameterIndexIn(query); - boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1; - - /* - * Prefer indexed access over named parameters if only SpEL Expression parameters are present. - */ - if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { - parametersShouldBeAccessedByIndex = true; - greatestParameterIndex = 0; - } - - ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, - parametersShouldBeAccessedByIndex, - greatestParameterIndex); - - String resultingQuery = parsedQuery.getQueryString(); - Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); - - int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; - int syntheticParameterIndex = expressionParameterIndex + parsedQuery.size(); - - ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings), - syntheticParameterIndex); - int currentIndex = 0; - - boolean usesJpaStyleParameters = false; - - while (matcher.find()) { - - if (parsedQuery.isQuoted(matcher.start())) { - continue; - } - - String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP); - String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP); - Integer parameterIndex = getParameterIndex(parameterIndexString); - - String match = matcher.group(0); - if (JDBC_STYLE_PARAM.matcher(match).find()) { - queryMeta.usesJdbcStyleParameters = true; - } - - if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { - usesJpaStyleParameters = true; - } - - if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) { - throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); - } - - String typeSource = matcher.group(COMPARISION_TYPE_GROUP); - Assert.isTrue(parameterIndexString != null || parameterName != null, - () -> String.format("We need either a name or an index; Offending query string: %s", query)); - ValueExpression expression = parsedQuery - .getParameter(parameterName == null ? parameterIndexString : parameterName); - String replacement = null; - - expressionParameterIndex++; - if ("".equals(parameterIndexString)) { - parameterIndex = expressionParameterIndex; - } - - BindingIdentifier queryParameter; - if (parameterIndex != null) { - queryParameter = BindingIdentifier.of(parameterIndex); - } else { - queryParameter = BindingIdentifier.of(parameterName); - } - ParameterOrigin origin = ObjectUtils.isEmpty(expression) - ? ParameterOrigin.ofParameter(parameterName, parameterIndex) - : ParameterOrigin.ofExpression(expression); - - BindingIdentifier targetBinding = queryParameter; - Function bindingFactory = switch (ParameterBindingType.of(typeSource)) { - case LIKE -> { - - Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); - yield (identifier) -> new 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. - default -> (identifier) -> new ParameterBinding(identifier, origin); - }; - - if (origin.isExpression()) { - parameterBindings.register(bindingFactory.apply(queryParameter)); - } else { - targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory); - } - - replacement = targetBinding.hasName() ? ":" + targetBinding.getName() - : ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?" - : "?" + targetBinding.getPosition()); - String result; - String substring = matcher.group(2); - - int index = resultingQuery.indexOf(substring, currentIndex); - if (index < 0) { - result = resultingQuery; - } else { - currentIndex = index + replacement.length(); - result = resultingQuery.substring(0, index) + replacement - + resultingQuery.substring(index + substring.length()); - } - - resultingQuery = result; - } - - return resultingQuery; - } - - private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, - boolean parametersShouldBeAccessedByIndex, - int greatestParameterIndex) { - - /* - * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to - * not mix-up with the actual parameter indices. - */ - int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; - - BiFunction indexToParameterName = parametersShouldBeAccessedByIndex - ? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1) - : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1); - - String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; - - BiFunction parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; - ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(), - indexToParameterName, parameterNameToReplacement); - - return rewriter.parse(queryWithSpel); - } - - @Nullable - private static Integer getParameterIndex(@Nullable String parameterIndexString) { - - if (parameterIndexString == null || parameterIndexString.isEmpty()) { - return null; - } - return Integer.valueOf(parameterIndexString); - } - - private static int tryFindGreatestParameterIndexIn(String query) { - - Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); - - int greatestParameterIndex = -1; - while (parameterIndexMatcher.find()) { - - String parameterIndexString = parameterIndexMatcher.group(1); - Integer parameterIndex = getParameterIndex(parameterIndexString); - if (parameterIndex != null) { - greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex); - } - } - - return greatestParameterIndex; - } - - private static void checkAndRegister(ParameterBinding binding, List bindings) { - - bindings.stream() // - .filter(it -> it.bindsTo(binding)) // - .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); - - if (!bindings.contains(binding)) { - bindings.add(binding); - } - } - - /** - * An enum for the different types of bindings. - * - * @author Thomas Darimont - * @author Oliver Gierke - */ - private enum ParameterBindingType { - - // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace - // character, while = does not. - LIKE("like "), IN("in "), AS_IS(null); - - private final @Nullable String keyword; - - ParameterBindingType(@Nullable String keyword) { - this.keyword = keyword; - } - - /** - * Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a - * keyword. - * - * @return the keyword - */ - @Nullable - public String getKeyword() { - return keyword; - } - - /** - * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in - * case no other {@link ParameterBindingType} could be found. - */ - static ParameterBindingType of(String typeSource) { - - if (!StringUtils.hasText(typeSource)) { - return AS_IS; - } - - for (ParameterBindingType type : values()) { - if (type.name().equalsIgnoreCase(typeSource.trim())) { - return type; - } - } - - throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s", typeSource)); - } - } -} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index aaf2e5218f..216ed8ee1a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -25,6 +25,8 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.test.tools.ClassFile; import org.springframework.data.jpa.domain.sample.Role; @@ -35,6 +37,8 @@ import org.springframework.lang.Nullable; /** + * Test {@link AotRepositoryContext} implementation for JPA repositories. + * * @author Christoph Strobl */ public class TestJpaAotRepositoryContext implements AotRepositoryContext { @@ -56,6 +60,11 @@ public ConfigurableListableBeanFactory getBeanFactory() { return null; } + @Override + public Environment getEnvironment() { + return new StandardEnvironment(); + } + @Override public TypeIntrospector introspectType(String typeName) { return null; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java index 714abc2afa..ba3f33f02d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java @@ -31,6 +31,8 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.javapoet.ClassName; @@ -116,6 +118,11 @@ public ConfigurableListableBeanFactory getBeanFactory() { return null; } + @Override + public Environment getEnvironment() { + return new StandardEnvironment(); + } + @Override public TypeIntrospector introspectType(String typeName) { return null; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java index 57ba338b0c..874ff77c99 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java @@ -28,7 +28,6 @@ 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.ParameterBindingParser.Metadata; import org.springframework.data.repository.query.parser.Part.Type; /** From 2a8e9a55d14507fb17eb14baa381f7eb5b286878 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 8 Apr 2025 15:43:10 +0200 Subject: [PATCH 67/94] Remove hardcoded repository fragment from test setup. See #3830 --- .../repository/aot/AotFragmentTestConfigurationSupport.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java index 670c871caa..b73f9cc0d8 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotFragmentTestConfigurationSupport.java @@ -57,13 +57,13 @@ class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { public AotFragmentTestConfigurationSupport(Class repositoryInterface) { this.repositoryInterface = repositoryInterface; - this.repositoryContext = new TestJpaAotRepositoryContext<>(UserRepository.class, null); + this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, null); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface); new JpaRepositoryContributor(repositoryContext).contribute(generationContext); From c239f730243078aeb9e222a78d3e69750534045f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 16:48:32 +0200 Subject: [PATCH 68/94] Allow HQL HAVING without enforcing GROUP BY. Closes #3840 --- .../springframework/data/jpa/repository/query/Hql.g4 | 4 ++-- .../jpa/repository/query/EqlQueryRendererTests.java | 8 ++++++++ .../jpa/repository/query/HqlQueryRendererTests.java | 12 ++++++++++++ .../jpa/repository/query/JpqlQueryRendererTests.java | 8 ++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 index da0518d08c..d18a924a51 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 @@ -89,8 +89,8 @@ orderedQuery ; query - : selectClause fromClause? whereClause? (groupByClause havingClause?)? # SelectQuery - | fromClause whereClause? (groupByClause havingClause?)? selectClause? # FromQuery + : selectClause fromClause? whereClause? groupByClause? havingClause? # SelectQuery + | fromClause whereClause? groupByClause? havingClause? selectClause? # FromQuery ; queryOrder diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java index f98f722c82..6ff1b23387 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java @@ -624,6 +624,14 @@ SELECT c.country, COUNT(c) GROUP BY c.country HAVING COUNT(c) > 30 """); + + assertQuery(""" + SELECT COUNT(f) + FROM FooEntity f + WHERE f.name IN ('Y', 'Basic', 'Remit') + AND f.size = 10 + HAVING COUNT(f) > 0 + """); } @Test diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index b35267da11..bcbdf8c178 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1144,6 +1144,18 @@ HAVING COUNT(o) >= 5 """); } + @Test + void shouldRenderHavingWithFunction() { + + assertQuery(""" + SELECT COUNT(f) + FROM FooEntity f + WHERE f.name IN ('Y', 'Basic', 'Remit') + AND f.size = 10 + HAVING COUNT(f) > 0 + """); + } + @Test void theRest8() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java index 1ec46fef2d..f84eb18f84 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java @@ -890,6 +890,14 @@ SELECT c.country, COUNT(c) GROUP BY c.country HAVING COUNT(c) > 30 """); + + assertQuery(""" + SELECT COUNT(f) + FROM FooEntity f + WHERE f.name IN ('Y', 'Basic', 'Remit') + AND f.size = 10 + HAVING COUNT(f) > 0 + """); } @Test From 4b34ac0a4561226e70008f7159ed8b7eedf35cf0 Mon Sep 17 00:00:00 2001 From: hgh1472 Date: Sat, 5 Apr 2025 00:10:44 +0900 Subject: [PATCH 69/94] Remove unnecessary parameter. Remove unnecessary boolean nativeQuery from checkHasNamedParameter of StringQueryUnitTests class. Signed-off-by: hgh1472 Closes #3827 Original pull request: #3828 --- .../query/DefaultEntityQueryUnitTests.java | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java index 874ff77c99..599fb05aa0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java @@ -41,6 +41,7 @@ * @author Diego Krupitza * @author Mark Paluch * @author Aleksei Elin + * @author Gunha Hwang */ class DefaultEntityQueryUnitTests { @@ -740,32 +741,32 @@ private void checkAlias(String query, String expected, String description, boole @Test // DATAJPA-1200 void testHasNamedParameter() { - checkHasNamedParameter("select something from x where id = :id", true, "named parameter", true); - checkHasNamedParameter("in the :id middle", true, "middle", false); - checkHasNamedParameter(":id start", true, "beginning", false); - checkHasNamedParameter(":id", true, "alone", false); - checkHasNamedParameter("select something from x where id = :id", true, "named parameter", true); - checkHasNamedParameter(":UPPERCASE", true, "uppercase", false); - checkHasNamedParameter(":lowercase", true, "lowercase", false); - checkHasNamedParameter(":2something", true, "beginning digit", false); - checkHasNamedParameter(":2", true, "only digit", false); - checkHasNamedParameter(":.something", true, "dot", false); - checkHasNamedParameter(":_something", true, "underscore", false); - checkHasNamedParameter(":$something", true, "dollar", false); - checkHasNamedParameter(":\uFE0F", true, "non basic latin emoji", false); // - checkHasNamedParameter(":\u4E01", true, "chinese japanese korean", false); - - checkHasNamedParameter("no bind variable", false, "no bind variable", false); - checkHasNamedParameter(":\u2004whitespace", false, "non basic latin whitespace", false); - checkHasNamedParameter("select something from x where id = ?1", false, "indexed parameter", true); - checkHasNamedParameter("::", false, "double colon", false); - checkHasNamedParameter(":", false, "end of query", false); - checkHasNamedParameter(":\u0003", false, "non-printable", false); - checkHasNamedParameter(":*", false, "basic latin emoji", false); - checkHasNamedParameter("\\:", false, "escaped colon", false); - checkHasNamedParameter("::id", false, "double colon with identifier", false); - checkHasNamedParameter("\\:id", false, "escaped colon with identifier", false); - checkHasNamedParameter("select something from x where id = #something", false, "hash", true); + checkHasNamedParameter("select something from x where id = :id", true, "named parameter"); + checkHasNamedParameter("in the :id middle", true, "middle"); + checkHasNamedParameter(":id start", true, "beginning"); + checkHasNamedParameter(":id", true, "alone"); + checkHasNamedParameter("select something from x where id = :id", true, "named parameter"); + checkHasNamedParameter(":UPPERCASE", true, "uppercase"); + checkHasNamedParameter(":lowercase", true, "lowercase"); + checkHasNamedParameter(":2something", true, "beginning digit"); + checkHasNamedParameter(":2", true, "only digit"); + checkHasNamedParameter(":.something", true, "dot"); + checkHasNamedParameter(":_something", true, "underscore"); + checkHasNamedParameter(":$something", true, "dollar"); + checkHasNamedParameter(":\uFE0F", true, "non basic latin emoji"); // + checkHasNamedParameter(":\u4E01", true, "chinese japanese korean"); + + checkHasNamedParameter("no bind variable", false, "no bind variable"); + checkHasNamedParameter(":\u2004whitespace", false, "non basic latin whitespace"); + checkHasNamedParameter("select something from x where id = ?1", false, "indexed parameter"); + checkHasNamedParameter("::", false, "double colon"); + checkHasNamedParameter(":", false, "end of query"); + checkHasNamedParameter(":\u0003", false, "non-printable"); + checkHasNamedParameter(":*", false, "basic latin emoji"); + checkHasNamedParameter("\\:", false, "escaped colon"); + checkHasNamedParameter("::id", false, "double colon with identifier"); + checkHasNamedParameter("\\:id", false, "escaped colon with identifier"); + checkHasNamedParameter("select something from x where id = #something", false, "hash"); } @Test // DATAJPA-1235 @@ -929,10 +930,10 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, .hasSize(expectedSize); } - private void checkHasNamedParameter(String query, boolean expected, String label, boolean nativeQuery) { + private void checkHasNamedParameter(String query, boolean expected, String label) { - DeclaredQuery source = nativeQuery ? DeclaredQuery.nativeQuery(query) : DeclaredQuery.jpqlQuery(query); - PreprocessedQuery bindableQuery = PreprocessedQuery.ParameterBindingParser.INSTANCE.parse(source.getQueryString(), + DeclaredQuery source = DeclaredQuery.jpqlQuery(query); + PreprocessedQuery bindableQuery = PreprocessedQuery.ParameterBindingParser.INSTANCE.parse(query, source::rewrite, it -> {}); assertThat(bindableQuery.getBindings().stream().anyMatch(it -> it.getIdentifier().hasName())) // From 2ce55db47e2767472be9c18b74be8acc93126ec8 Mon Sep 17 00:00:00 2001 From: yoobin_mion <113106136+yybmion@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:07:30 +0900 Subject: [PATCH 70/94] Fix typos in query method reference documentation. Signed-off-by: Yoobin Yoon <113106136+yybmion@users.noreply.github.com> Closes #3822 --- src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 13fddd3efb..eaa05b0b3b 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -313,7 +313,7 @@ A similar approach also works with named native queries, by adding the `.count` 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. The resulting map contains key/value pairs representing the actual database column name and the value. -.Native query retuning raw column name/value pairs +.Native query returning raw column name/value pairs ==== [source, java] ---- @@ -620,7 +620,7 @@ public interface ConcreteRepository In the preceding example, the `MappedTypeRepository` interface is the common parent interface for a few domain types extending `AbstractMappedType`. It also defines the generic `findAllByAttribute(…)` method, which can be used on instances of the specialized repository interfaces. -If you now invoke `findByAllAttribute(…)` on `ConcreteRepository`, the query becomes `select t from ConcreteType t where t.attribute = ?1`. +If you now invoke `findAllByAttribute(…)` on `ConcreteRepository`, the query becomes `select t from ConcreteType t where t.attribute = ?1`. You can also use Expressions to control arguments may also be used to control method arguments. In these expressions the entity name is not available, but the arguments are. From 1ca26d8ea7e53fb60cdec97970b615438e7bc4a6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 10:37:41 +0200 Subject: [PATCH 71/94] Polishing. Fix post-rebase conflicts. See #3622 --- .../modules/ROOT/pages/jpa/query-methods.adoc | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index eaa05b0b3b..5513309f4f 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -170,89 +170,6 @@ public interface UserRepository extends JpaRepository { ---- ==== -[[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. -Query rewriting applies to the actual query and, when applicable, to count queries. -Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery`. - - -.Declare a QueryRewriter using `@Query` -==== -[source, java] ----- -public interface MyRepository extends JpaRepository { - - @NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias", - queryRewriter = MyQueryRewriter.class) - List findByNativeQuery(String param); - - @Query(value = "select original_user_alias from User original_user_alias", - queryRewriter = MyQueryRewriter.class) - List 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, QueryRewriter { - - @Query(value = "select original_user_alias.* from SD_USER original_user_alias", - nativeQuery = true, - queryRewriter = MyRepository.class) - List findByNativeQuery(String param); - - @Query(value = "select original_user_alias from User original_user_alias", - queryRewriter = MyRepository.class) - List 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.advanced-like]] === Using Advanced `LIKE` Expressions From 9d3ecbbed489ab69c60f1b2415d06a94db90fd43 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 11:21:30 +0200 Subject: [PATCH 72/94] Polishing. Refine documentation. See #3815 --- .../modules/ROOT/pages/repositories/projections.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc index c5e113c8f4..0eb4682ff2 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -50,10 +50,10 @@ Consider the following queries: ---- interface UserRepository extends Repository { - @Query("SELECT u FROM USER u") <1> + @Query("SELECT u FROM USER u WHERE u.lastname = :lastname") <1> List findByLastname(String lastname); - @Query("SELECT u.firstname, u.lastname FROM USER u") <2> + @Query("SELECT u.firstname, u.lastname FROM USER u WHERE u.lastname = :lastname") <2> List findMultipleColumnsByLastname(String lastname); } @@ -61,9 +61,9 @@ record UserDto(String firstname, String lastname){} ---- <1> Selection of the top-level entity. -This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`. +This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname`. <2> Multi-select of `firstname` and `lastname` properties. -This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u`. +This query gets rewritten to `SELECT new UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname`. ==== [WARNING] From 07d1d55bfc81517871b07788a9fd5efe53f906b3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:29:21 +0200 Subject: [PATCH 73/94] Prepare 4.0 M2 (2025.1.0). See #3751 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 2b0f8c1740..d200eb5c32 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M2 @@ -38,7 +38,7 @@ 5.0 9.1.0 42.7.4 - 4.0.0-SNAPSHOT + 4.0.0-M2 0.10.3 org.hibernate @@ -270,20 +270,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 4ddd4b84c083ff976eef8730032752514bc36a8c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:29:38 +0200 Subject: [PATCH 74/94] Release version 4.0 M2 (2025.1.0). See #3751 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index d200eb5c32..0586e09ee5 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M2 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 43c08369f6..10dcde18ad 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-SNAPSHOT + 4.0.0-M2 org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M2 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..f64df8c572 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M2 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 1cc6674063..b4598fb076 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-SNAPSHOT + 4.0.0-M2 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M2 ../pom.xml From 5d8cf2ceea1989331747a033278cb3094b98b288 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:32:03 +0200 Subject: [PATCH 75/94] Prepare next development iteration. See #3751 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 0586e09ee5..d200eb5c32 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M2 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 10dcde18ad..43c08369f6 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-M2 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-M2 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index f64df8c572..af5244a230 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M2 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index b4598fb076..1cc6674063 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-M2 + 4.0.0-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M2 + 4.0.0-SNAPSHOT ../pom.xml From 3827c3c87549b7d83db7b240623088be1dd6824f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:32:04 +0200 Subject: [PATCH 76/94] After release cleanups. See #3751 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index d200eb5c32..2b0f8c1740 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M2 + 4.0.0-SNAPSHOT @@ -38,7 +38,7 @@ 5.0 9.1.0 42.7.4 - 4.0.0-M2 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -270,8 +270,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From d6482ffe7b5f77932afc984eebda40652d0694be Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Apr 2025 16:12:27 +0200 Subject: [PATCH 77/94] Use parameter names in derived JPQL queries. We also use improved parameter naming for keyset queries for easier correlation of values. Closes #3857 --- .../jpa/repository/aot/JpaCodeBlocks.java | 8 +-- .../query/JpaKeysetScrollQueryCreator.java | 52 +++++++++++++++---- .../jpa/repository/query/JpaQueryCreator.java | 31 ++++++++--- .../query/KeysetScrollDelegate.java | 9 ++-- .../query/KeysetScrollSpecification.java | 12 +++-- .../repository/query/ParameterBinding.java | 8 +++ .../query/ParameterMetadataProvider.java | 23 ++++++-- .../support/QuerydslJpaPredicateExecutor.java | 2 +- .../repository/UserRepositoryFinderTests.java | 15 ++++-- ...RepositoryContributorIntegrationTests.java | 7 +++ ...JpaRepositoryMetadataIntegrationTests.java | 4 +- .../jpa/repository/aot/UserRepository.java | 10 ++++ .../JpaKeysetScrollQueryCreatorTests.java | 8 +-- ...meterMetadataProviderIntegrationTests.java | 8 +-- .../PartTreeJpaQueryIntegrationTests.java | 2 +- .../jpa/repository/sample/UserRepository.java | 6 +++ 16 files changed, 152 insertions(+), 53 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index f9c9b45e6b..fe0a84eafa 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -427,13 +427,7 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, } private Object getParameterName(ParameterBinding.BindingIdentifier identifier) { - - if (identifier.hasPosition()) { - return identifier.getPosition(); - } - - return identifier.getName(); - + return identifier.hasName() ? identifier.getName() : Integer.valueOf(identifier.getPosition()); } private Object getParameter(ParameterBinding.ParameterOrigin origin) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java index 1acb62d768..776657b2af 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java @@ -19,14 +19,13 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashSet; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.data.domain.KeysetScrollPosition; +import java.util.Map; import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; @@ -76,12 +75,22 @@ protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(JpqlQueryBuilder.@Nulla JpqlQueryBuilder.Select query = buildQuery(keysetSpec.sort()); - AtomicInteger counter = new AtomicInteger(provider.getBindings().size()); - JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), value -> { + Map> cachedBindings = new LinkedHashMap<>(); + JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), + (property, value) -> { + + Map bindings = cachedBindings.computeIfAbsent(property, k -> new LinkedHashMap<>()); + + ParameterBinding parameterBinding = bindings.computeIfAbsent(value, o -> { + + ParameterBinding binding = provider.nextSynthetic(sanitize(property), value, scrollPosition); + syntheticBindings.add(binding); + return binding; + }); + + return placeholder(parameterBinding); + }); - syntheticBindings.add(provider.nextSynthetic(value, scrollPosition)); - return placeholder(counter.incrementAndGet()); - }); JpqlQueryBuilder.Predicate predicateToUse = getPredicate(predicate, keysetPredicate); if (predicateToUse != null) { @@ -91,6 +100,29 @@ protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(JpqlQueryBuilder.@Nulla return query; } + private static String sanitize(String property) { + + StringBuilder buffer = new StringBuilder(10 + property.length()); + + // max length 24 + buffer.append("keyset_"); + + char[] charArray = property.toCharArray(); + for (int i = 0; i < charArray.length; i++) { + + if (buffer.length() > 24) { + break; + } + + if (Character.isDigit(charArray[i]) || Character.isLetter(charArray[i])) { + buffer.append(charArray[i]); + } else if (charArray[i] == '.') { + buffer.append('_'); + } + } + + return buffer.toString(); + } private static JpqlQueryBuilder.@Nullable Predicate getPredicate(JpqlQueryBuilder.@Nullable Predicate predicate, JpqlQueryBuilder.@Nullable Predicate keysetPredicate) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 3eec07e417..c49baf6ff9 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -33,9 +33,9 @@ import java.util.List; import java.util.stream.Collectors; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder; import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding; @@ -73,6 +73,7 @@ public class JpaQueryCreator extends AbstractQueryCreator entityType; private final JpqlQueryBuilder.Entity entity; private final Metamodel metamodel; + private final boolean useNamedParameters; /** * Create a new {@link JpaQueryCreator}. @@ -96,6 +97,23 @@ public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvid this.tree = tree; this.returnedType = type; this.provider = provider; + + JpaParameters bindableParameters = provider.getParameters().getBindableParameters(); + + boolean useNamedParameters = false; + for (JpaParameters.JpaParameter bindableParameter : bindableParameters) { + + if (bindableParameter.isNamedParameter()) { + useNamedParameters = true; + } + + if (useNamedParameters && !bindableParameter.isNamedParameter()) { + useNamedParameters = false; + break; + } + } + + this.useNamedParameters = useNamedParameters; this.templates = templates; this.escape = provider.getEscape(); this.entityType = metamodel.entity(type.getDomainType()); @@ -274,11 +292,12 @@ Collection getRequiredSelection(Sort sort, ReturnedType returnedType) { } JpqlQueryBuilder.Expression placeholder(ParameterBinding binding) { - return placeholder(binding.getRequiredPosition()); - } - JpqlQueryBuilder.Expression placeholder(int position) { - return JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(position)); + if (useNamedParameters && binding.hasName()) { + return JpqlQueryBuilder.parameter(ParameterPlaceholder.named(binding.getRequiredName())); + } + + return JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(binding.getRequiredPosition())); } /** diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java index cfa65ccd17..a80de6e4a3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java @@ -22,9 +22,9 @@ import java.util.List; import java.util.Map; -import org.springframework.data.domain.KeysetScrollPosition; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition.Direction; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; @@ -104,7 +104,7 @@ public static Collection getProjectionInputProperties(JpaEntityInformati break; } - sortConstraint.add(strategy.compare(propertyExpression, o)); + sortConstraint.add(strategy.compare(inner.getProperty(), propertyExpression, o)); j++; } @@ -215,11 +215,12 @@ public interface QueryStrategy { /** * Create an equals-comparison object. * + * @param property name of the property. * @param propertyExpression must not be {@literal null}. * @param value the value to compare with. Can be {@literal null}. * @return an object representing the comparison predicate. */ - P compare(E propertyExpression, @Nullable Object value); + P compare(String property, E propertyExpression, @Nullable Object value); /** * AND-combine the {@code intermediate} predicates. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java index 504658726c..76b3ed0a29 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollSpecification.java @@ -117,7 +117,7 @@ public Predicate compare(Order order, Expression propertyExpression, } @Override - public Predicate compare(Expression propertyExpression, @Nullable Object value) { + public Predicate compare(String property, Expression propertyExpression, @Nullable Object value) { return value == null ? cb.isNull(propertyExpression) : cb.equal(propertyExpression, value); } @@ -163,15 +163,17 @@ public JpqlQueryBuilder.Predicate compare(Order order, JpqlQueryBuilder.Expressi if (value == null) { return order.isAscending() ? where.isNull() : where.isNotNull(); } - return order.isAscending() ? where.gt(factory.capture(value)) : where.lt(factory.capture(value)); + return order.isAscending() ? where.gt(factory.capture(order.getProperty(), value)) + : where.lt(factory.capture(order.getProperty(), value)); } @Override - public JpqlQueryBuilder.Predicate compare(JpqlQueryBuilder.Expression propertyExpression, @Nullable Object value) { + public JpqlQueryBuilder.Predicate compare(String property, JpqlQueryBuilder.Expression propertyExpression, + @Nullable Object value) { JpqlQueryBuilder.WhereStep where = JpqlQueryBuilder.where(propertyExpression); - return value == null ? where.isNull() : where.eq(factory.capture(value)); + return value == null ? where.isNull() : where.eq(factory.capture(property, value)); } @Override @@ -186,6 +188,6 @@ public JpqlQueryBuilder.Predicate compare(JpqlQueryBuilder.Expression propertyEx } public interface ParameterFactory { - JpqlQueryBuilder.Expression capture(Object value); + JpqlQueryBuilder.Expression capture(String name, Object value); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index b06b0f9711..040e84a8ed 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -81,6 +81,14 @@ public ParameterOrigin getOrigin() { return identifier.hasName() ? identifier.getName() : null; } + /** + * @return {@literal true} if the binding identifier is associated with a name. + * @since 4.0 + */ + boolean hasName() { + return identifier.hasName(); + } + /** * @return the name * @throws IllegalStateException if the name is not available. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java index 5071e23ff4..72d43ab5bd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java @@ -24,12 +24,14 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; -import org.springframework.data.jpa.provider.PersistenceProvider; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -60,6 +62,7 @@ public class ParameterMetadataProvider { private final Iterator parameters; private final List bindings; + private final Set syntheticParameterNames = new LinkedHashSet<>(); private final @Nullable Iterator bindableParameterValues; private final EscapeCharacter escape; private final JpqlQueryTemplates templates; @@ -176,7 +179,8 @@ private PartTreeParameterBinding next(Part part, Class type, Parameter pa int currentPosition = ++position; - BindingIdentifier bindingIdentifier = BindingIdentifier.of(currentPosition); + BindingIdentifier bindingIdentifier = parameter.getName().map(it -> BindingIdentifier.of(it, currentPosition)) + .orElseGet(() -> BindingIdentifier.of(currentPosition)); /* identifier refers to bindable parameters, not _all_ parameters index */ MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(bindingIdentifier); @@ -195,15 +199,24 @@ EscapeCharacter getEscape() { /** * Builds a new synthetic {@link ParameterBinding} for the given value. * + * @param nameHint * @param value * @param source * @return a new {@link ParameterBinding} for the given value and source. */ - public ParameterBinding nextSynthetic(Object value, Object source) { + public ParameterBinding nextSynthetic(String nameHint, Object value, Object source) { int currentPosition = ++position; + String bindingName = nameHint; + + if (!syntheticParameterNames.add(bindingName)) { + + bindingName = bindingName + "_" + currentPosition; + syntheticParameterNames.add(bindingName); + } - return new ParameterBinding(BindingIdentifier.of(currentPosition), ParameterOrigin.synthetic(value, source)); + return new ParameterBinding(BindingIdentifier.of(bindingName, currentPosition), + ParameterOrigin.synthetic(value, source)); } public JpaParameters getParameters() { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 8881ab84c0..0bbcee84bd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -386,7 +386,7 @@ public BooleanExpression compare(Order order, Expression propertyExpression, } @Override - public BooleanExpression compare(Expression propertyExpression, @Nullable Object value) { + public BooleanExpression compare(String property, Expression propertyExpression, @Nullable Object value) { return Expressions.booleanOperation(Ops.EQ, propertyExpression, value == null ? NullExpression.DEFAULT : ConstantImpl.create(value)); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 4e2a545653..18d3e3941e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -15,10 +15,8 @@ */ package org.springframework.data.jpa.repository; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.data.domain.Sort.Direction.ASC; -import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Direction.*; import jakarta.persistence.EntityManager; @@ -33,6 +31,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Limit; @@ -495,6 +494,14 @@ void dtoProjectionWithEntityAndAggregatedValueWithPageable() { }); } + @Test // GH-3857 + void shouldApplyParameterNames() { + + assertThat(userRepository.findAnnotatedWithParameterNameQuery(oliver.getLastname())).hasSize(2); + assertThat(userRepository.findWithParameterNameByLastnameStartingWithOrLastnameEndingWith(oliver.getLastname(), + oliver.getLastname())).hasSize(2); + } + @ParameterizedTest // GH-3076 @ValueSource(classes = { UserRoleCountDtoProjection.class, UserRoleCountInterfaceProjection.class }) void dynamicProjectionWithEntityAndAggregated(Class resultType) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java index 566a08cb76..0d649778ee 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorIntegrationTests.java @@ -320,6 +320,13 @@ void testPagingAnnotatedQueryWithSort() { "vader@empire.com"); } + @Test // GH-3857 + void appliesCustomParameterNaming() { + + assertThat(fragment.findAnnotatedWithParameterNameQuery("S")).hasSize(4); + assertThat(fragment.findWithParameterNameByLastnameStartingWithOrLastnameEndingWith("S", "S")).hasSize(4); + } + @Test // GH-3830 void testAnnotatedFinderReturningSlice() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java index 0cdde1ef04..3450bcf1a4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java @@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional; /** - * Integration tests for the {@link UserRepository} JSON metadata. + * Integration tests for the {@link UserRepository} JSON metadata via {@link JpaRepositoryContributor}. * * @author Mark Paluch */ @@ -77,7 +77,7 @@ void shouldDocumentDerivedQuery() throws IOException { assertThatJson(json).inPath("$.methods[0]").isObject().containsEntry("name", "countUsersByLastname"); assertThatJson(json).inPath("$.methods[0].query").isObject().containsEntry("query", - "SELECT COUNT(u) FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname = ?1"); + "SELECT COUNT(u) FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname = :lastname"); } @Test // GH-3830 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index b95cd88377..7279abf7dc 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -116,6 +116,16 @@ interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like ?1%") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + // ------------------------------------------------------------------------- + // Projections: Parameter naming + // ------------------------------------------------------------------------- + + @Query("select u from User u where u.lastname like %:name or u.lastname like :name% ORDER BY u.lastname") + List findAnnotatedWithParameterNameQuery(@Param("name") String lastname); + + List findWithParameterNameByLastnameStartingWithOrLastnameEndingWith(@Param("l1") String l1, + @Param("l2") String l2); + // ------------------------------------------------------------------------- // Value Expressions // ------------------------------------------------------------------------- diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java index d8bfd1fdb9..dd180bab52 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreatorTests.java @@ -77,10 +77,10 @@ void shouldCreateContinuationQuery() throws Exception { String query = creator.createQuery(); assertThat(query).containsIgnoringWhitespaces(""" - SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE (u.firstname LIKE ?1 ESCAPE '\\') - AND (u.firstname < ?2 - OR u.firstname = ?3 AND u.emailAddress < ?4 - OR u.firstname = ?5 AND u.emailAddress = ?6 AND u.id < ?7) + SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE (u.firstname LIKE :firstname ESCAPE '\\') + AND (u.firstname < :keyset_firstname + OR u.firstname = :keyset_firstname AND u.emailAddress < :keyset_emailAddress + OR u.firstname = :keyset_firstname AND u.emailAddress = :keyset_emailAddress AND u.id < :keyset_id) ORDER BY u.firstname desc, u.emailAddress desc, u.id desc """); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java index beb8e68a76..81e454c799 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java @@ -50,22 +50,22 @@ class ParameterMetadataProviderIntegrationTests { @PersistenceContext EntityManager em; @Test // DATAJPA-758 - void usesIndexedParametersForExplicityNamedParameters() throws Exception { + void usesNamedParametersForExplicitlyNamedParameters() throws Exception { ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByFirstname", String.class)); ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("firstname", User.class)); - assertThat(metadata.getName()).isNull(); + assertThat(metadata.getName()).isEqualTo("name"); assertThat(metadata.getPosition()).isEqualTo(1); } @Test // DATAJPA-758 - void usesIndexedParameters() throws Exception { + void usesNamedParameters() throws Exception { ParameterMetadataProvider provider = createProvider(Sample.class.getMethod("findByLastname", String.class)); ParameterBinding.PartTreeParameterBinding metadata = provider.next(new Part("lastname", User.class)); - assertThat(metadata.getName()).isNull(); + assertThat(metadata.getName()).isEqualTo("lastname"); assertThat(metadata.getPosition()).isEqualTo(1); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index b99e50071d..02d63e6770 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -112,7 +112,7 @@ void recreatesQueryIfNullValueIsGiven(String criteria) throws Exception { Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) })); assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))) - .contains("firstname %s ?".formatted(criteria.endsWith("Not") ? "!=" : "=")); + .contains("firstname %s :".formatted(criteria.endsWith("Not") ? "!=" : "=")); query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) })); 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 3c6d2ce4ea..80756c8c4d 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 @@ -740,6 +740,12 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter Window findBy(OffsetScrollPosition position); + @Query("select u from User u where u.lastname like %:name or u.lastname like :name% ORDER BY u.lastname") + List findAnnotatedWithParameterNameQuery(@Param("name") String lastname); + + List findWithParameterNameByLastnameStartingWithOrLastnameEndingWith(@Param("l1") String l1, + @Param("l2") String l2); + @Retention(RetentionPolicy.RUNTIME) @Query("select u, count(r) from User u left outer join u.roles r group by u") @interface UserRoleCountProjectingQuery { From 9fab81a571ec9c88132e7ba1db584481e6b4b45c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Apr 2025 17:46:27 +0200 Subject: [PATCH 78/94] Polishing. Add dynamic projection benchmark. --- pom.xml | 10 +-------- .../RepositoryQueryMethodBenchmarks.java | 7 ++++++ .../data/jpa/benchmark/model/PersonDto.java | 22 +++++++++++++++++++ .../repository/PersonRepository.java | 3 +++ 4 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java diff --git a/pom.xml b/pom.xml index 2b0f8c1740..5ccd77f57f 100755 --- a/pom.xml +++ b/pom.xml @@ -56,17 +56,9 @@ jmh - - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-junit5 - 0.4.0.RELEASE - test - - - jitpack.io + jitpack https://jitpack.io diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java index f49d658a00..0f20652d65 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java @@ -42,6 +42,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.benchmark.model.Person; +import org.springframework.data.jpa.benchmark.model.PersonDto; import org.springframework.data.jpa.benchmark.model.Profile; import org.springframework.data.jpa.benchmark.repository.PersonRepository; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; @@ -195,6 +196,12 @@ public List stringBasedQueryDynamicSort(BenchmarkParameters parameters) Sort.by(COLUMN_PERSON_FIRSTNAME)); } + @Benchmark + public List stringBasedQueryDynamicSortAndProjection(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, + Sort.by(COLUMN_PERSON_FIRSTNAME), PersonDto.class); + } + @Benchmark public List stringBasedNativeQuery(BenchmarkParameters parameters) { return parameters.repositoryProxy.findAllWithNativeQueryByFirstname(PERSON_FIRSTNAME); diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java new file mode 100644 index 0000000000..6241e6a439 --- /dev/null +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java @@ -0,0 +1,22 @@ +/* + * 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.benchmark.model; + +/** + * @author Mark Paluch + */ +public record PersonDto(String firstname, String lastname) { +} diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java index 491ab736a8..81950ab3fa 100644 --- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java +++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java @@ -38,6 +38,9 @@ public interface PersonRepository extends ListCrudRepository { @Query("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1") List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort); + @Query("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1") + List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort, Class projection); + @Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true) List findAllWithNativeQueryByFirstname(String firstname); From c94259c2b413b9ddb853542c0aad2823f9daa38f Mon Sep 17 00:00:00 2001 From: SWQXDBA <983110853@qq.com> Date: Wed, 23 Apr 2025 20:03:17 +0800 Subject: [PATCH 79/94] Fix handling of `null` predicate in `Specification.not()`. When toPredicate() returns null, Specification.not() now returns builder.disjunction() instead of builder.not(null). This change ensures proper handling of null predicates in negated specifications. Closes #3849 Original pull request: #3856 Signed-off-by: SWQXDBA <983110853@qq.com> --- .../data/jpa/domain/Specification.java | 2 +- .../data/jpa/domain/SpecificationUnitTests.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index b9994b79ad..25a5fb2ce2 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java @@ -156,7 +156,7 @@ static Specification not(Specification spec) { return (root, query, builder) -> { Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : null; + return predicate != null ? builder.not(predicate) : builder.disjunction(); }; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index c8c7228433..432c9c2fff 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -139,6 +139,17 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } + @Test // #3849 + void notWithNullPredicate() { + Specification spec = (r, q, cb) -> null; + + Specification notSpec = Specification.not(spec); + + notSpec.toPredicate(root, query, builder); + + verify(builder).disjunction(); + } + static class SerializableSpecification implements Serializable, Specification { @Override From 15b32419a30969d488db7b118021fca6c7bd5359 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 14:13:17 +0200 Subject: [PATCH 80/94] Polishing. Add fix to Update, Delete, and PredicateSpecification. Reformat code. Refine tests. See #3849 Original pull request: #3856 --- .../data/jpa/domain/DeleteSpecification.java | 6 +++--- .../data/jpa/domain/PredicateSpecification.java | 6 +++--- .../data/jpa/domain/UpdateSpecification.java | 6 +++--- .../data/jpa/domain/DeleteSpecificationUnitTests.java | 11 +++++++++++ .../jpa/domain/PredicateSpecificationUnitTests.java | 11 +++++++++++ .../data/jpa/domain/SpecificationUnitTests.java | 11 +++++------ .../data/jpa/domain/UpdateSpecificationUnitTests.java | 11 +++++++++++ 7 files changed, 47 insertions(+), 15 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java index 32278c7ba5..4c7deb638d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/DeleteSpecification.java @@ -24,9 +24,9 @@ import java.util.Arrays; import java.util.stream.StreamSupport; -import org.springframework.lang.CheckReturnValue; - import org.jspecify.annotations.Nullable; + +import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -159,7 +159,7 @@ static DeleteSpecification not(DeleteSpecification spec) { return (root, delete, builder) -> { Predicate predicate = spec.toPredicate(root, delete, builder); - return predicate != null ? builder.not(predicate) : null; + return predicate != null ? builder.not(predicate) : builder.disjunction(); }; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java index 5d9bd51065..daa39b9ba7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/PredicateSpecification.java @@ -23,9 +23,9 @@ import java.util.Arrays; import java.util.stream.StreamSupport; -import org.springframework.lang.CheckReturnValue; - import org.jspecify.annotations.Nullable; + +import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -113,7 +113,7 @@ static PredicateSpecification not(PredicateSpecification spec) { return (root, builder) -> { Predicate predicate = spec.toPredicate(root, builder); - return predicate != null ? builder.not(predicate) : null; + return predicate != null ? builder.not(predicate) : builder.disjunction(); }; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java index 9b4b9f5e4d..1a27d428a4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/UpdateSpecification.java @@ -24,9 +24,9 @@ import java.util.Arrays; import java.util.stream.StreamSupport; -import org.springframework.lang.CheckReturnValue; - import org.jspecify.annotations.Nullable; + +import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -180,7 +180,7 @@ static UpdateSpecification not(UpdateSpecification spec) { return (root, update, builder) -> { Predicate predicate = spec.toPredicate(root, update, builder); - return predicate != null ? builder.not(predicate) : null; + return predicate != null ? builder.not(predicate) : builder.disjunction(); }; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java index 02e59fa2db..8dfcb33bad 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/DeleteSpecificationUnitTests.java @@ -160,6 +160,17 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } + @Test // GH-3849 + void notWithNullPredicate() { + + when(builder.disjunction()).thenReturn(mock(Predicate.class)); + + DeleteSpecification notSpec = DeleteSpecification.not((r, q, cb) -> null); + + assertThat(notSpec.toPredicate(root, delete, builder)).isNotNull(); + verify(builder).disjunction(); + } + static class SerializableSpecification implements Serializable, DeleteSpecification { @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java index f0cd8ca085..d11d61d0a2 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/PredicateSpecificationUnitTests.java @@ -158,6 +158,17 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } + @Test // GH-3849 + void notWithNullPredicate() { + + when(builder.disjunction()).thenReturn(mock(Predicate.class)); + + PredicateSpecification notSpec = PredicateSpecification.not((r, cb) -> null); + + assertThat(notSpec.toPredicate(root, builder)).isNotNull(); + verify(builder).disjunction(); + } + static class SerializableSpecification implements Serializable, PredicateSpecification { @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index 432c9c2fff..8380816d52 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java @@ -131,7 +131,6 @@ void orCombinesSpecificationsInOrder() { Predicate secondPredicate = mock(Predicate.class); Specification first = ((root1, query1, criteriaBuilder) -> firstPredicate); - Specification second = ((root1, query1, criteriaBuilder) -> secondPredicate); first.or(second).toPredicate(root, query, builder); @@ -139,14 +138,14 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // #3849 + @Test // GH-3849 void notWithNullPredicate() { - Specification spec = (r, q, cb) -> null; - Specification notSpec = Specification.not(spec); + when(builder.disjunction()).thenReturn(mock(Predicate.class)); + + Specification notSpec = Specification.not((r, q, cb) -> null); - notSpec.toPredicate(root, query, builder); - + assertThat(notSpec.toPredicate(root, query, builder)).isNotNull(); verify(builder).disjunction(); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java index 540cc91e40..61c788d143 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/UpdateSpecificationUnitTests.java @@ -160,6 +160,17 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } + @Test // GH-3849 + void notWithNullPredicate() { + + when(builder.disjunction()).thenReturn(mock(Predicate.class)); + + UpdateSpecification notSpec = UpdateSpecification.not((r, q, cb) -> null); + + assertThat(notSpec.toPredicate(root, update, builder)).isNotNull(); + verify(builder).disjunction(); + } + static class SerializableSpecification implements Serializable, UpdateSpecification { @Override From 2a4559cd3e7ad5c014abd25638cd503d3cbbbd0e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 17:15:27 +0200 Subject: [PATCH 81/94] Avoid DTO Constructor Expression rewriting for selection of nested properties. We back off from rewriting String-based queries to use DTO Constructor expressions if the query selects a property that is assignable to the return type. Closes #3862 --- .../query/AbstractStringBasedJpaQuery.java | 131 +++++++++++++++++- .../repository/query/DefaultEntityQuery.java | 6 + .../query/EmptyIntrospectedQuery.java | 13 ++ .../jpa/repository/query/EntityQuery.java | 16 +++ .../repository/query/ParametrizedQuery.java | 2 +- .../data/jpa/domain/sample/Address.java | 24 ++++ .../data/jpa/domain/sample/Role.java | 15 ++ .../repository/UserRepositoryFinderTests.java | 29 ++++ .../jpa/repository/UserRepositoryTests.java | 1 - .../jpa/repository/sample/UserRepository.java | 28 ++++ 10 files changed, 262 insertions(+), 3 deletions(-) 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 013cb9d94e..64b21e8cb5 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,7 +18,10 @@ 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; @@ -26,12 +29,15 @@ import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.jpa.repository.QueryRewriter; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.PropertyReferenceException; 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; +import org.springframework.util.StringUtils; /** * Base class for {@link String} based JPA queries. @@ -49,6 +55,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final EntityQuery query; + private final Map, Boolean> knownProjections = new ConcurrentHashMap<>(); private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; @@ -132,7 +139,7 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { Sort sort = accessor.getSort(); ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - ReturnedType returnedType = processor.getReturnedType(); + ReturnedType returnedType = getReturnedType(processor); QueryProvider sortedQuery = getSortedQuery(sort, returnedType); Query query = createJpaQuery(sortedQuery, sort, accessor.getPageable(), returnedType); @@ -141,6 +148,86 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { 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 + */ + private ReturnedType getReturnedType(ResultProcessor processor) { + + ReturnedType returnedType = processor.getReturnedType(); + Class returnedJavaType = processor.getReturnedType().getReturnedType(); + + if (query.isDefaultProjection() || !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()) { + if (known == null) { + knownProjections.put(returnedJavaType, false); + } + return new NonProjectingReturnedType(returnedType); + } + + String projectionToUse = query.<@Nullable String> doWithEnhancer(queryEnhancer -> { + + String alias = queryEnhancer.detectAlias(); + String projection = queryEnhancer.getProjection(); + + // we can handle single-column and no function projections here only + if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) { + return null; + } + + if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) { + alias = alias.trim(); + projection = projection.trim(); + if (projection.startsWith(alias + ".")) { + projection = projection.substring(alias.length() + 1); + } + } + + int space = projection.indexOf(' '); + + if (space != -1) { + projection = projection.substring(0, space); + } + + return projection; + }); + + if (StringUtils.hasText(projectionToUse)) { + + Class propertyType; + + try { + PropertyPath from = PropertyPath.from(projectionToUse, getQueryMethod().getEntityInformation().getJavaType()); + propertyType = from.getLeafType(); + } catch (PropertyReferenceException ignored) { + propertyType = null; + } + + if (propertyType == null + || (returnedJavaType.isAssignableFrom(propertyType) || propertyType.isAssignableFrom(returnedJavaType))) { + knownProjections.put(returnedJavaType, false); + return new NonProjectingReturnedType(returnedType); + } else { + knownProjections.put(returnedJavaType, true); + } + } + + return returnedType; + } + QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) { return querySortRewriter.getSorted(query, sort, returnedType); } @@ -355,4 +442,46 @@ public int hashCode() { 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 getInputProperties() { + return delegate.getInputProperties(); + } + } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java index bde36d1535..d07e238f21 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultEntityQuery.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.query; import java.util.List; +import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -46,6 +47,11 @@ class DefaultEntityQuery implements EntityQuery, DeclaredQuery { this.queryEnhancer = queryEnhancerFactory.create(query); } + @Override + public T doWithEnhancer(Function function) { + return function.apply(queryEnhancer); + } + @Override public boolean isNative() { return query.isNative(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java index a0ef2363b6..188b0b8c23 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EmptyIntrospectedQuery.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -33,6 +34,8 @@ enum EmptyIntrospectedQuery implements EntityQuery { EmptyIntrospectedQuery() {} + + @Override public boolean hasParameterBindings() { return false; @@ -57,11 +60,21 @@ public List getParameterBindings() { return null; } + @Override + public T doWithEnhancer(Function function) { + return null; + } + @Override public boolean hasConstructorExpression() { return false; } + @Override + public boolean isNative() { + return false; + } + @Override public boolean isDefaultProjection() { return false; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java index f827e0b291..0e22efa28a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EntityQuery.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jpa.repository.query; +import java.util.function.Function; + import org.jspecify.annotations.Nullable; /** @@ -45,6 +47,15 @@ static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) { return new DefaultEntityQuery(preparsed, enhancerFactory); } + /** + * Apply a {@link Function} to the query enhancer used by this query. + * + * @param function the callback function. + * @return + * @param + */ + T doWithEnhancer(Function function); + /** * Returns whether the query is using a constructor expression. * @@ -52,6 +63,11 @@ static EntityQuery create(DeclaredQuery query, QueryEnhancerSelector selector) { */ boolean hasConstructorExpression(); + /** + * @return whether the underlying query has at least one named parameter. + */ + boolean isNative(); + /** * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query. */ diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java index 85a314127d..4736e091fc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParametrizedQuery.java @@ -30,7 +30,7 @@ * @see EntityQuery#create(DeclaredQuery, QueryEnhancerSelector) * @see TemplatedQuery#create(String, JpaQueryMethod, JpaQueryConfiguration) */ -interface ParametrizedQuery extends QueryProvider { +public interface ParametrizedQuery extends QueryProvider { /** * @return whether the underlying query has at least one parameter. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Address.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Address.java index e5db7bfddf..ccd97f7b74 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Address.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Address.java @@ -17,6 +17,8 @@ import jakarta.persistence.Embeddable; +import org.springframework.util.ObjectUtils; + /** * @author Thomas Darimont */ @@ -52,4 +54,26 @@ public String getStreetName() { public String getStreetNo() { return streetNo; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Address address)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(country, address.country)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(city, address.city)) { + return false; + } + if (!ObjectUtils.nullSafeEquals(streetName, address.streetName)) { + return false; + } + return ObjectUtils.nullSafeEquals(streetNo, address.streetNo); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHash(country, city, streetName, streetNo); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Role.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Role.java index 101a784ee2..bdde7ce8f9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Role.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Role.java @@ -19,6 +19,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import org.springframework.util.ObjectUtils; + /** * Sample domain class representing roles. Mapped with XML. * @@ -55,4 +57,17 @@ public String toString() { public boolean isNew() { return id == null; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Role role)) { + return false; + } + return ObjectUtils.nullSafeEquals(id, role.id); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHash(id); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 18d3e3941e..46721b1dfb 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -42,6 +42,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; +import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.PersistenceProvider; @@ -421,6 +422,11 @@ void dtoProjectionShouldApplyConstructorExpressionRewriting() { assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // .contains("Dave", "Carter", "Oliver August"); + + dtos = userRepository.findRecordProjectionWithFunctions(); + + assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::lastname) // + .contains("matthews", "beauford"); } @Test // GH-3076 @@ -441,6 +447,29 @@ void dynamicDtoProjection() { .contains("Dave", "Carter", "Oliver August"); } + @Test // GH-3862 + void shouldNotRewritePrimitiveSelectionToDtoProjection() { + + oliver.setAge(28); + em.persist(oliver); + + assertThat(userRepository.findAgeByAnnotatedQuery(oliver.getEmailAddress())).contains(28); + } + + @Test // GH-3862 + void shouldNotRewritePropertySelectionToDtoProjection() { + + Address address = new Address("DE", "Dresden", "some street", "12345"); + dave.setAddress(address); + userRepository.save(dave); + em.flush(); + em.clear(); + + assertThat(userRepository.findAddressByAnnotatedQuery(dave.getEmailAddress())).contains(address); + assertThat(userRepository.findCityByAnnotatedQuery(dave.getEmailAddress())).contains("Dresden"); + assertThat(userRepository.findRolesByAnnotatedQuery(dave.getEmailAddress())).contains(singer); + } + @Test // GH-3076 void dtoProjectionWithEntityAndAggregatedValue() { 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 05f16dc09b..0ebf726932 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 @@ -456,7 +456,6 @@ void testOverwritingFinder() { @Test void testUsesQueryAnnotation() { - assertThat(repository.findByAnnotatedQuery("gierke@synyx.de")).isNull(); } 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 80756c8c4d..2fc34657f8 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 @@ -39,6 +39,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; +import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.SpecialUser; import org.springframework.data.jpa.domain.sample.User; @@ -717,12 +718,39 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter @Query("select u from User u") List findRecordProjection(); + @Query("select u.firstname, LOWER(u.lastname) from User u") + List findRecordProjectionWithFunctions(); + @Query("select u from User u") List findRecordProjection(Class projectionType); @Query("select u.firstname, u.lastname from User u") List findMultiselectRecordProjection(); + /** + * Retrieves a user age by email. + */ + @Query("select u.age from User u where u.emailAddress = ?1") + Optional findAgeByAnnotatedQuery(String emailAddress); + + /** + * Retrieves a user address by email. + */ + @Query("select u.address from User u where u.emailAddress = ?1") + Optional
        findAddressByAnnotatedQuery(String emailAddress); + + /** + * Retrieves a user roles by email. + */ + @Query("select u.roles from User u where u.emailAddress = ?1") + Set findRolesByAnnotatedQuery(String emailAddress); + + /** + * Retrieves a user address city by email. + */ + @Query("select u.address.city from User u where u.emailAddress = ?1") + String findCityByAnnotatedQuery(String emailAddress); + @UserRoleCountProjectingQuery List dtoProjectionEntityAndAggregatedValue(); From 7520e2920c088b16ce463a9d2867bd4182995375 Mon Sep 17 00:00:00 2001 From: Diego Pedregal Date: Mon, 5 May 2025 11:13:59 +0200 Subject: [PATCH 82/94] Removes `PlainSelect` casting in `JSqlParserQueryEnhancer`. Closes: #3869 Original pull request: #3870 Signed-off-by: Diego Pedregal --- .../jpa/repository/query/JSqlParserQueryEnhancer.java | 3 ++- .../query/JSqlParserQueryEnhancerUnitTests.java | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) 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 82cae525c1..936ef47d93 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 @@ -66,6 +66,7 @@ * @author Geoffrey Deremetz * @author Yanming Zhou * @author Christoph Strobl + * @author Diego Pedregal * @since 2.7.0 */ public class JSqlParserQueryEnhancer implements QueryEnhancer { @@ -241,7 +242,7 @@ private static String detectProjection(Statement statement) { } StringJoiner joiner = new StringJoiner(", "); - for (SelectItem selectItem : ((PlainSelect) selectBody).getSelectItems()) { + for (SelectItem selectItem : selectBody.getPlainSelect().getSelectItems()) { joiner.add(selectItem.toString()); } return joiner.toString().trim(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index 4a0be8de58..31869a859a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -263,4 +263,13 @@ private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); } + @Test // GH-3869 + void shouldWorkWithoutFromClause() { + String query = "SELECT is_contained_in(:innerId, :outerId)"; + + StringQuery stringQuery = new StringQuery(query, true); + + assertThat(stringQuery.getQueryString()).isEqualTo(query); + } + } From f02a21c3236c497818f77ad30782afa35f12c896 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 15:45:08 +0200 Subject: [PATCH 83/94] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce doWithPlainSelect(…) callback for easier filtering of Select subtypes. Add test for known (previously) failing case. See: #3869 Original pull request: #3870 --- .../query/JSqlParserQueryEnhancer.java | 166 +++++++++++------- .../JSqlParserQueryEnhancerUnitTests.java | 22 +-- 2 files changed, 113 insertions(+), 75 deletions(-) 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 936ef47d93..a5d907354d 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 @@ -48,10 +48,13 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; +import java.util.function.Predicate; +import java.util.function.Supplier; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; +import org.springframework.data.util.Predicates; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -149,24 +152,8 @@ static T parseStatement(String sql, Class classOfT) { if (ParsedType.SELECT.equals(parsedType)) { - Select selectStatement = (Select) statement; - - /* - * For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide - * alias since: - * ValuesStatement has no alias - * SetOperation can have multiple alias for each operation item - */ - if (!(selectStatement instanceof PlainSelect selectBody)) { - return null; - } - - if (selectBody.getFromItem() == null) { - return null; - } - - Alias alias = selectBody.getFromItem().getAlias(); - return alias == null ? null : alias.getName(); + return doWithPlainSelect(statement, it -> it.getFromItem() == null || it.getFromItem().getAlias() == null, + it -> it.getFromItem().getAlias().getName(), () -> null); } return null; @@ -179,20 +166,24 @@ static T parseStatement(String sql, Class classOfT) { */ private static Set getSelectionAliases(Statement statement) { - if (!(statement instanceof PlainSelect select) || CollectionUtils.isEmpty(select.getSelectItems())) { - return Collections.emptySet(); + if (statement instanceof SetOperationList sel) { + statement = sel.getSelect(0); } - Set set = new HashSet<>(select.getSelectItems().size()); + return doWithPlainSelect(statement, it -> CollectionUtils.isEmpty(it.getSelectItems()), it -> { + + Set set = new HashSet<>(it.getSelectItems().size(), 1.0f); - for (SelectItem selectItem : select.getSelectItems()) { - Alias alias = selectItem.getAlias(); - if (alias != null) { - set.add(alias.getName()); + for (SelectItem selectItem : it.getSelectItems()) { + Alias alias = selectItem.getAlias(); + if (alias != null) { + set.add(alias.getName()); + } } - } - return set; + return set; + + }, Collections::emptySet); } /** @@ -202,21 +193,74 @@ private static Set getSelectionAliases(Statement statement) { */ private static Set getJoinAliases(Statement statement) { - if (!(statement instanceof PlainSelect selectBody) || CollectionUtils.isEmpty(selectBody.getJoins())) { - return Collections.emptySet(); + if (statement instanceof SetOperationList sel) { + statement = sel.getSelect(0); } - Set set = new HashSet<>(selectBody.getJoins().size()); + return doWithPlainSelect(statement, it -> CollectionUtils.isEmpty(it.getJoins()), it -> { - for (Join join : selectBody.getJoins()) { - Alias alias = join.getRightItem().getAlias(); - if (alias != null) { - set.add(alias.getName()); + Set set = new HashSet<>(it.getJoins().size(), 1.0f); + + for (Join join : it.getJoins()) { + Alias alias = join.getRightItem().getAlias(); + if (alias != null) { + set.add(alias.getName()); + } } + return set; + + }, Collections::emptySet); + } + + /** + * Apply a {@link java.util.function.Function mapping function} to the {@link PlainSelect} of the given + * {@link Statement} is or contains a {@link PlainSelect}. + * + * @param statement + * @param mapper + * @param fallback + * @return + * @param + */ + private static T doWithPlainSelect(Statement statement, java.util.function.Function mapper, + Supplier fallback) { + + Predicate neverSkip = Predicates.isFalse(); + return doWithPlainSelect(statement, neverSkip, mapper, fallback); + } + + /** + * Apply a {@link java.util.function.Function mapping function} to the {@link PlainSelect} of the given + * {@link Statement} is or contains a {@link PlainSelect}. + *

        + * The operation is only applied if {@link Predicate skipIf} returns {@literal false} for the given statement + * returning the fallback value from {@code fallback}. + * + * @param statement + * @param skipIf + * @param mapper + * @param fallback + * @return + * @param + */ + private static T doWithPlainSelect(Statement statement, Predicate skipIf, + java.util.function.Function mapper, Supplier fallback) { + + if (!(statement instanceof Select select)) { + return fallback.get(); } - return set; + try { + if (skipIf.test(select.getPlainSelect())) { + return fallback.get(); + } + } + // e.g. SetOperationList is a subclass of Select but it is not a PlainSelect + catch (ClassCastException e) { + return fallback.get(); + } + return mapper.apply(select.getPlainSelect()); } private static String detectProjection(Statement statement) { @@ -235,18 +279,17 @@ private static String detectProjection(Statement statement) { // using the first one since for setoperations the projection has to be the same selectBody = setOperationList.getSelects().get(0); - - if (!(selectBody instanceof PlainSelect)) { - return ""; - } } - StringJoiner joiner = new StringJoiner(", "); - for (SelectItem selectItem : selectBody.getPlainSelect().getSelectItems()) { - joiner.add(selectItem.toString()); - } - return joiner.toString().trim(); + return doWithPlainSelect(selectBody, it -> CollectionUtils.isEmpty(it.getSelectItems()), it -> { + + StringJoiner joiner = new StringJoiner(", "); + for (SelectItem selectItem : it.getSelectItems()) { + joiner.add(selectItem.toString()); + } + return joiner.toString().trim(); + }, () -> ""); } /** @@ -317,24 +360,22 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab return applySortingToSetOperationList(setOperationList, sort); } - if (!(selectStatement instanceof PlainSelect selectBody)) { - if (selectStatement != null) { - return selectStatement.toString(); + doWithPlainSelect(selectStatement, it -> { + + List orderByElements = new ArrayList<>(16); + for (Sort.Order order : sort) { + orderByElements.add(getOrderClause(joinAliases, selectAliases, alias, order)); + } + + if (CollectionUtils.isEmpty(it.getOrderByElements())) { + it.setOrderByElements(orderByElements); } else { - throw new IllegalArgumentException("Select must not be null"); + it.getOrderByElements().addAll(orderByElements); } - } - List orderByElements = new ArrayList<>(16); - for (Sort.Order order : sort) { - orderByElements.add(getOrderClause(joinAliases, selectAliases, alias, order)); - } + return null; - if (CollectionUtils.isEmpty(selectBody.getOrderByElements())) { - selectBody.setOrderByElements(orderByElements); - } else { - selectBody.getOrderByElements().addAll(orderByElements); - } + }, () -> ""); return selectStatement.toString(); } @@ -349,14 +390,9 @@ public String createCountQueryFor(@Nullable String countProjection) { Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty"); Statement statement = (Statement) deserialize(this.serialized); - /* - We only support count queries for {@link PlainSelect}. - */ - if (!(statement instanceof PlainSelect selectBody)) { - return this.query.getQueryString(); - } - return createCountQueryFor(selectBody, countProjection, primaryAlias); + return doWithPlainSelect(statement, it -> createCountQueryFor(it, countProjection, primaryAlias), + this.query::getQueryString); } private static String createCountQueryFor(PlainSelect selectBody, @Nullable String countProjection, diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index 31869a859a..126868890d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -36,7 +36,7 @@ * @author Geoffrey Deremetz * @author Christoph Strobl */ -public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { +class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { @@ -232,6 +232,17 @@ void truncateStatementShouldWork() { assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); } + @Test // GH-3869 + void shouldWorkWithParenthesedSelect() { + + DefaultEntityQuery query = new TestEntityQuery("(SELECT is_contained_in(:innerId, :outerId))", true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query).create(query); + + assertThat(query.getQueryString()).isEqualTo("(SELECT is_contained_in(:innerId, :outerId))"); + assertThat(query.getAlias()).isNull(); + assertThat(queryEnhancer.getProjection()).isEqualTo("is_contained_in(:innerId, :outerId)"); + } + @ParameterizedTest // GH-2641 @MethodSource("mergeStatementWorksSource") void mergeStatementWorksWithJSqlParser(String queryString, String alias) { @@ -263,13 +274,4 @@ private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())); } - @Test // GH-3869 - void shouldWorkWithoutFromClause() { - String query = "SELECT is_contained_in(:innerId, :outerId)"; - - StringQuery stringQuery = new StringQuery(query, true); - - assertThat(stringQuery.getQueryString()).isEqualTo(query); - } - } From 3503b33e2f305566adbef4c29aab1e8715dfc832 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 8 May 2025 12:38:58 +0200 Subject: [PATCH 84/94] Upgrade to Hibernate 7.0.0.CR1. Closes: #3872 --- pom.xml | 2 +- .../HqlOrderExpressionVisitorUnitTests.java | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 5ccd77f57f..4847874dd6 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.2 5.0.0-B07 5.0.0-SNAPSHOT - 7.0.0.Beta5 + 7.0.0.CR1 7.0.0-SNAPSHOT 2.7.4

        2.3.232

        diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java index 98ac54ca79..5c0eb36bc3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlOrderExpressionVisitorUnitTests.java @@ -15,7 +15,10 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -27,11 +30,11 @@ import java.util.Locale; +import org.hibernate.query.sqm.tree.SqmRenderContext; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.ContextConfiguration; @@ -125,26 +128,26 @@ void temporalLiterals() { // JDBC assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2024-01-01 12:34:56'}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + 2024-01-01T12:34:56"); + .startsWithIgnoringCase("order by u.createdAt + '2024-01-01T12:34:56'"); assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2012-01-03 09:00:00.000000001'}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + 2012-01-03T09:00:00.000000001"); + .startsWithIgnoringCase("order by u.createdAt + '2012-01-03T09:00:00.000000001'"); // Hibernate NPE - assertThatNullPointerException().isThrownBy(() -> renderOrderBy(JpaSort.unsafe("createdAt + {t '12:34:56'}"), "u")); + assertThatIllegalArgumentException().isThrownBy(() -> renderOrderBy(JpaSort.unsafe("createdAt + {t '12:34:56'}"), "u")); assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d '2024-01-01'}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + 2024-01-01"); + .startsWithIgnoringCase("order by u.createdAt + '2024-01-01'"); // JPQL assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts 2024-01-01 12:34:56}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + 2024-01-01T12:34:56"); + .startsWithIgnoringCase("order by u.createdAt + '2024-01-01T12:34:56'"); assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {t 12:34:56}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + 12:34:56"); + .startsWithIgnoringCase("order by u.createdAt + '12:34:56'"); assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d 2024-01-01}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + 2024-01-01"); + .startsWithIgnoringCase("order by u.createdAt + '2024-01-01'"); } @Test // GH-3172 @@ -262,7 +265,7 @@ String renderQuery(JpaSort sort, String alias) { SqmSelectStatement s = (SqmSelectStatement) q; StringBuilder builder = new StringBuilder(); - s.appendHqlString(builder); + s.appendHqlString(builder, SqmRenderContext.simpleContext()); return builder.toString(); } From 86c1d381e5265c58e5a3d44680f8027a6d63170e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 7 May 2025 17:11:38 +0200 Subject: [PATCH 85/94] Provide `JpaRepositoryFragmentsContributor` in JPA Repository Factory and Repository Factory Bean. Closes #3874 --- .../config/JpaRepositoryConfigExtension.java | 130 +----------------- .../support/JpaEntityInformationSupport.java | 2 +- .../support/JpaRepositoryFactory.java | 46 +++---- .../support/JpaRepositoryFactoryBean.java | 49 +++++-- .../JpaRepositoryFragmentsContributor.java | 84 +++++++++++ .../support/QuerydslContributor.java | 78 +++++++++++ .../aot/AotContributionIntegrationTests.java | 84 +++++++++++ ...JpaRepositoryMetadataIntegrationTests.java | 2 +- .../aot/QuerydslUserRepository.java | 28 ++++ .../aot/TestJpaAotRepositoryContext.java | 5 + ...toryRegistrationAotProcessorUnitTests.java | 5 + .../JpaRepositoryFactoryUnitTests.java | 16 ++- ...positoryFragmentsContributorUnitTests.java | 96 +++++++++++++ 13 files changed, 454 insertions(+), 171 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 99eec50100..eb89f0af8d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -43,13 +43,10 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.TypeFilter; import org.springframework.dao.DataAccessException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.data.aot.AotContext; @@ -63,14 +60,10 @@ import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.config.ImplementationDetectionConfiguration; -import org.springframework.data.repository.config.ImplementationLookupConfiguration; -import org.springframework.data.repository.config.RepositoryConfiguration; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; -import org.springframework.data.util.Streamable; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -105,6 +98,11 @@ public String getModuleName() { return "JPA"; } + @Override + public String getRepositoryBaseClassName() { + return SimpleJpaRepository.class.getName(); + } + @Override public String getRepositoryFactoryBeanClassName() { return JpaRepositoryFactoryBean.class.getName(); @@ -342,123 +340,5 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi return emf != null ? new JpaRepositoryContributor(repositoryContext, emf) : new JpaRepositoryContributor(repositoryContext); } - - @Nullable - @Override - @SuppressWarnings("NullAway") - protected RepositoryConfiguration getRepositoryMetadata(RegisteredBean bean) { - RepositoryConfiguration configuration = super.getRepositoryMetadata(bean); - - if (configuration != null && configuration.getRepositoryBaseClassName().isPresent()) { - return configuration; - } - return new Meh<>(configuration); - } - } - - /** - * I'm just a dirty hack so we can refine the {@link #getRepositoryBaseClassName()} method as we cannot instantiate - * the bean safely to extract it form the repository factory in data commons. So we either have a configurable - * {@link RepositoryConfiguration} return from - * {@link RepositoryRegistrationAotProcessor#getRepositoryMetadata(RegisteredBean)} or change the arrangement and - * maybe move the type out of the factoy. - * - * @param - */ - static class Meh implements RepositoryConfiguration { - - private RepositoryConfiguration configuration; - - public Meh(RepositoryConfiguration configuration) { - this.configuration = configuration; - } - - @Nullable - @Override - public Object getSource() { - return configuration.getSource(); - } - - @Override - public T getConfigurationSource() { - return (T) configuration.getConfigurationSource(); - } - - @Override - public boolean isLazyInit() { - return configuration.isLazyInit(); - } - - @Override - public boolean isPrimary() { - return configuration.isPrimary(); - } - - @Override - public Streamable getBasePackages() { - return configuration.getBasePackages(); - } - - @Override - public Streamable getImplementationBasePackages() { - return configuration.getImplementationBasePackages(); - } - - @Override - public String getRepositoryInterface() { - return configuration.getRepositoryInterface(); - } - - @Override - public Optional getQueryLookupStrategyKey() { - return Optional.ofNullable(configuration.getQueryLookupStrategyKey()); - } - - @Override - public Optional getNamedQueriesLocation() { - return configuration.getNamedQueriesLocation(); - } - - @Override - public Optional getRepositoryBaseClassName() { - String name = SimpleJpaRepository.class.getName(); - return Optional.of(name); - } - - @Override - public String getRepositoryFactoryBeanClassName() { - return configuration.getRepositoryFactoryBeanClassName(); - } - - @Override - public String getImplementationBeanName() { - return configuration.getImplementationBeanName(); - } - - @Override - public String getRepositoryBeanName() { - return configuration.getRepositoryBeanName(); - } - - @Override - public Streamable getExcludeFilters() { - return configuration.getExcludeFilters(); - } - - @Override - public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) { - return configuration.toImplementationDetectionConfiguration(factory); - } - - @Override - public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) { - return configuration.toLookupConfiguration(factory); - } - - @Nullable - @Override - public String getResourceDescription() { - return configuration.getResourceDescription(); - } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java index 6d8c0ba8dc..62af516073 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupport.java @@ -35,7 +35,7 @@ public abstract class JpaEntityInformationSupport extends AbstractEntityInformation implements JpaEntityInformation { - private JpaEntityMetadata metadata; + private final JpaEntityMetadata metadata; /** * Creates a new {@link JpaEntityInformationSupport} with the given domain class. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 91314ed115..bbccb5b979 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -15,8 +15,6 @@ */ package org.springframework.data.jpa.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - import jakarta.persistence.EntityManager; import jakarta.persistence.Tuple; @@ -32,7 +30,6 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.JpaRepository; @@ -40,7 +37,6 @@ import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -79,6 +75,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private JpaRepositoryFragmentsContributor fragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT; private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR; private JpaQueryMethodFactory queryMethodFactory; private QueryRewriterProvider queryRewriterProvider; @@ -159,6 +156,17 @@ public void setEscapeCharacter(EscapeCharacter escapeCharacter) { this.escapeCharacter = escapeCharacter; } + /** + * Configures the {@link JpaRepositoryFragmentsContributor} to be used. Defaults to + * {@link JpaRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 4.0 + */ + public void setFragmentsContributor(JpaRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + /** * Configures the {@link JpaQueryMethodFactory} to be used. Defaults to {@link DefaultJpaQueryMethodFactory}. * @@ -259,51 +267,39 @@ protected Optional getQueryLookupStrategy(@Nullable Key key @Override @SuppressWarnings("unchecked") public JpaEntityInformation getEntityInformation(Class domainClass) { - return (JpaEntityInformation) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager); } @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - return getRepositoryFragments(metadata, entityManager, entityPathResolver, this.crudMethodMetadata); } /** - * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically, * adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl. *

        - * Can be overridden by subclasses to customize {@link RepositoryFragments}. + * Built-in fragment contribution can be customized by configuring {@link JpaRepositoryFragmentsContributor}. * * @param metadata repository metadata. * @param entityManager the entity manager. * @param resolver resolver to translate a plain domain class into a {@link EntityPath}. * @param crudMethodMetadata metadata about the invoked CRUD methods. - * @return + * @return {@link RepositoryFragments} to be added to the repository. * @since 2.5.1 */ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, EntityManager entityManager, EntityPathResolver resolver, CrudMethodMetadata crudMethodMetadata) { - boolean isQueryDslRepository = QUERY_DSL_PRESENT - && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - if (metadata.isReactiveRepository()) { - throw new InvalidDataAccessApiUsageException( - "Cannot combine Querydsl and reactive repository support in a single interface"); - } - - QuerydslJpaPredicateExecutor querydslJpaPredicateExecutor = new QuerydslJpaPredicateExecutor<>( - getEntityInformation(metadata.getDomainType()), entityManager, resolver, crudMethodMetadata); - invokeAwareMethods(querydslJpaPredicateExecutor); + RepositoryFragments fragments = this.fragmentsContributor.contribute(metadata, + getEntityInformation(metadata.getDomainType()), entityManager, resolver); - return RepositoryFragments - .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, querydslJpaPredicateExecutor)); + for (RepositoryFragment fragment : fragments) { + fragment.getImplementation().filter(JpaRepositoryConfigurationAware.class::isInstance) + .ifPresent(it -> invokeAwareMethods((JpaRepositoryConfigurationAware) it)); } - return RepositoryFragments.empty(); + return fragments; } private void invokeAwareMethods(JpaRepositoryConfigurationAware repository) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index a9d8622a4b..30461fcabb 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -55,9 +55,10 @@ public class JpaRepositoryFactoryBean, S, ID> private @Nullable BeanFactory beanFactory; private @Nullable EntityManager entityManager; private EntityPathResolver entityPathResolver = SimpleEntityPathResolver.INSTANCE; + private JpaRepositoryFragmentsContributor repositoryFragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; private @Nullable JpaQueryMethodFactory queryMethodFactory; - private @Nullable Function queryEnhancerSelectorSource; + private @Nullable Function<@Nullable BeanFactory, QueryEnhancerSelector> queryEnhancerSelectorSource; /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. @@ -100,20 +101,24 @@ public void setEntityPathResolver(ObjectProvider resolver) { this.entityPathResolver = resolver.getIfAvailable(() -> SimpleEntityPathResolver.INSTANCE); } + @Override + public JpaRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + /** - * Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will - * fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is - * available. + * Configures the {@link JpaRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. * - * @param resolver may be {@literal null}. + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 4.0 */ - @Autowired - public void setQueryMethodFactory(ObjectProvider resolver) { // TODO: nullable insteand of ObjectProvider + public void setRepositoryFragmentsContributor(JpaRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } - JpaQueryMethodFactory factory = resolver.getIfAvailable(); - if (factory != null) { - this.queryMethodFactory = factory; - } + public void setEscapeCharacter(char escapeCharacter) { + this.escapeCharacter = EscapeCharacter.of(escapeCharacter); } /** @@ -153,6 +158,23 @@ public void setQueryEnhancerSelector(Class quer }; } + /** + * Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will + * fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is + * available. + * + * @param resolver may be {@literal null}. + */ + @Autowired + public void setQueryMethodFactory(ObjectProvider resolver) { // TODO: nullable insteand of + // ObjectProvider + + JpaQueryMethodFactory factory = resolver.getIfAvailable(); + if (factory != null) { + this.queryMethodFactory = factory; + } + } + @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { @@ -169,6 +191,7 @@ protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityM JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager); factory.setEntityPathResolver(entityPathResolver); factory.setEscapeCharacter(escapeCharacter); + factory.setFragmentsContributor(getRepositoryFragmentsContributor()); if (queryMethodFactory != null) { factory.setQueryMethodFactory(queryMethodFactory); @@ -189,8 +212,4 @@ public void afterPropertiesSet() { super.afterPropertiesSet(); } - public void setEscapeCharacter(char escapeCharacter) { - - this.escapeCharacter = EscapeCharacter.of(escapeCharacter); - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java new file mode 100644 index 0000000000..03d072b435 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributor.java @@ -0,0 +1,84 @@ +/* + * 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.support; + +import jakarta.persistence.EntityManager; + +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +import com.querydsl.core.types.EntityPath; + +/** + * JPA-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

        + * Implementations must define a no-args constructor. + *

        + * Contributed fragments may implement the {@link JpaRepositoryConfigurationAware} interface to access configuration + * settings. + * + * @author Mark Paluch + * @since 4.0 + */ +public interface JpaRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + JpaRepositoryFragmentsContributor DEFAULT = QuerydslContributor.INSTANCE; + + /** + * Returns a composed {@code JpaRepositoryFragmentsContributor} that first applies this contributor to its inputs, and + * then applies the {@code after} contributor concatenating effectively both results. If evaluation of either + * contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default JpaRepositoryFragmentsContributor andThen(JpaRepositoryFragmentsContributor after) { + + Assert.notNull(after, "JpaRepositoryFragmentsContributor must not be null"); + + return new JpaRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver) { + return JpaRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, entityManager, resolver) + .append(after.contribute(metadata, entityInformation, entityManager, resolver)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return JpaRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific + * extensions. Typically, adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param entityManager the entity manager. + * @param resolver resolver to translate a plain domain class into a {@link EntityPath}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver); + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java new file mode 100644 index 0000000000..5f5e819c7b --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java @@ -0,0 +1,78 @@ +/* + * 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.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import jakarta.persistence.EntityManager; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * JPA-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository implements + * {@link QuerydslPredicateExecutor}. + * + * @author Mark Paluch + * @since 4.0 + * @see QuerydslJpaPredicateExecutor + */ +enum QuerydslContributor implements JpaRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver) { + + if (isQuerydslRepository(metadata)) { + + if (metadata.isReactiveRepository()) { + throw new InvalidDataAccessApiUsageException( + "Cannot combine Querydsl and reactive repository support in a single interface"); + } + + QuerydslJpaPredicateExecutor executor = new QuerydslJpaPredicateExecutor<>(entityInformation, entityManager, + resolver, null); + + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + + if (isQuerydslRepository(metadata)) { + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslJpaPredicateExecutor.class)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java new file mode 100644 index 0000000000..76390740ad --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java @@ -0,0 +1,84 @@ +/* + * 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.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.aot.AotContext; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.config.InfrastructureConfig; +import org.springframework.mock.env.MockPropertySource; + +/** + * Integration tests for AOT processing. + * + * @author Mark Paluch + */ +class AotContributionIntegrationTests { + + @EnableJpaRepositories(considerNestedRepositories = true, includeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = QuerydslUserRepository.class) }) + static class AotConfiguration extends InfrastructureConfig { + + } + + @Test // GH-3830 + void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { + + TestGenerationContext generationContext = generate(AotConfiguration.class); + + InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE, + QuerydslUserRepository.class.getName().replace('.', '/') + ".json"); + + InputStreamResource isr = new InputStreamResource(metadata); + String json = isr.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() + .containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor") + .containsEntry("fragment", "org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.jpa.repository.support.SimpleJpaRepository"); + } + + private static TestGenerationContext generate(Class... configurationClasses) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + context.register(configurationClasses); + + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(context, generationContext); + return generationContext; + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java index 3450bcf1a4..0a65cd5c32 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryMetadataIntegrationTests.java @@ -61,7 +61,7 @@ void shouldDocumentBase() throws IOException { assertThatJson(json).isObject() // .containsEntry("name", UserRepository.class.getName()) // - .containsEntry("module", "") // TODO: JPA should be here + .containsEntry("module", "JPA") // .containsEntry("type", "IMPERATIVE"); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java new file mode 100644 index 0000000000..6c551c482d --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QuerydslUserRepository.java @@ -0,0 +1,28 @@ +/* + * 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.aot; + +import java.util.List; + +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; + +interface QuerydslUserRepository extends CrudRepository, QuerydslPredicateExecutor { + + List findUserNoArgumentsBy(); + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index 216ed8ee1a..6fc63defab 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java @@ -80,6 +80,11 @@ public String getBeanName() { return "dummyRepository"; } + @Override + public String getModuleName() { + return "JPA"; + } + @Override public Set getBasePackages() { return Set.of("org.springframework.data.dummy.repository.aot"); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java index ba3f33f02d..44c260dcb5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryRegistrationAotProcessorUnitTests.java @@ -88,6 +88,11 @@ public String getBeanName() { return "jpaRepository"; } + @Override + public String getModuleName() { + return "JPA"; + } + @Override public Set getBasePackages() { return Collections.singleton(this.getClass().getPackageName()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java index 4b5ad4cf3e..bdc1a67a94 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java @@ -15,13 +15,15 @@ */ package org.springframework.data.jpa.repository.support; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnitUtil; +import jakarta.persistence.metamodel.IdentifiableType; +import jakarta.persistence.metamodel.ManagedType; import jakarta.persistence.metamodel.Metamodel; import java.io.IOException; @@ -35,6 +37,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.aop.framework.Advised; import org.springframework.core.OverridingClassLoader; import org.springframework.data.jpa.domain.sample.User; @@ -62,6 +65,7 @@ class JpaRepositoryFactoryUnitTests { private JpaRepositoryFactory factory; @Mock EntityManager entityManager; + @Mock PersistenceUnitUtil persistenceUnitUtil; @Mock Metamodel metamodel; @Mock @SuppressWarnings("rawtypes") JpaEntityInformation entityInformation; @@ -74,6 +78,7 @@ void setUp() { when(entityManager.getEntityManagerFactory()).thenReturn(emf); when(entityManager.getDelegate()).thenReturn(entityManager); when(emf.createEntityManager()).thenReturn(entityManager); + when(emf.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); // Setup standard factory configuration factory = new JpaRepositoryFactory(entityManager) { @@ -140,6 +145,9 @@ void handlesCheckedExceptionsCorrectly() { @Test void createsProxyWithCustomBaseClass() { + when(metamodel.managedType(any())) + .thenReturn(mock(ManagedType.class, withSettings().extraInterfaces(IdentifiableType.class))); + JpaRepositoryFactory factory = new CustomGenericJpaRepositoryFactory(entityManager); factory.setQueryLookupStrategyKey(Key.CREATE_IF_NOT_FOUND); UserCustomExtendedRepository repository = factory.getRepository(UserCustomExtendedRepository.class); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..7825534a32 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,96 @@ +/* + * 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.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import jakarta.persistence.EntityManager; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.jpa.domain.sample.QCustomer; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +import com.querydsl.core.types.EntityPath; + +/** + * Unit tests for {@link JpaRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class JpaRepositoryFragmentsContributorUnitTests { + + @Test // GH-3279 + void composedContributorShouldCreateFragments() { + + JpaRepositoryFragmentsContributor contributor = JpaRepositoryFragmentsContributor.DEFAULT + .andThen(MyJpaRepositoryFragmentsContributor.INSTANCE); + + EntityPathResolver entityPathResolver = mock(EntityPathResolver.class); + when(entityPathResolver.createPath(any())).thenReturn((EntityPath) QCustomer.customer); + + EntityManager entityManager = mock(EntityManager.class); + when(entityManager.getDelegate()).thenReturn(entityManager); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), + new JpaEntityInformationSupportUnitTests.DummyJpaEntityInformation<>(QuerydslUserRepository.class), + entityManager, entityPathResolver); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(QuerydslJpaPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyJpaRepositoryFragmentsContributor implements JpaRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + JpaEntityInformation entityInformation, EntityManager entityManager, EntityPathResolver resolver) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslUserRepository extends Repository, QuerydslPredicateExecutor {} + +} From beab651e35f15789c7459a8f03703d3ecfe05ca5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 May 2025 16:27:07 +0200 Subject: [PATCH 86/94] Use `LocalVariableNameFactory` in repository contributor. Closes #3875 --- .../jpa/repository/aot/JpaCodeBlocks.java | 116 +++++++++++------- .../aot/JpaRepositoryContributor.java | 51 ++++---- .../jpa/repository/aot/UserRepository.java | 3 +- .../src/test/resources/logback.xml | 3 +- 4 files changed, 96 insertions(+), 77 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java index fe0a84eafa..2cb7d332f4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaCodeBlocks.java @@ -81,7 +81,7 @@ static class QueryBlockBuilder { private final AotQueryMethodGenerationContext context; private final JpaQueryMethod queryMethod; - private String queryVariableName = "query"; + private String queryVariableName; private @Nullable AotQueries queries; private MergedAnnotation queryHints = MergedAnnotation.missing(); private @Nullable AotEntityGraph entityGraph; @@ -92,11 +92,12 @@ static class QueryBlockBuilder { private QueryBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { this.context = context; this.queryMethod = queryMethod; + this.queryVariableName = context.localVariable("query"); } public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { - this.queryVariableName = queryVariableName; + this.queryVariableName = context.localVariable(queryVariableName); return this; } @@ -153,14 +154,13 @@ public CodeBlock build() { } CodeBlock.Builder builder = CodeBlock.builder(); - builder.add("\n"); String queryStringVariableName = null; String queryRewriterName = null; if (queries.result() instanceof StringAotQuery && queryRewriter != QueryRewriter.IdentityQueryRewriter.class) { - queryRewriterName = "queryRewriter"; + queryRewriterName = context.localVariable("queryRewriter"); builder.addStatement("$T $L = new $T()", queryRewriter, queryRewriterName, queryRewriter); } @@ -171,11 +171,13 @@ public CodeBlock build() { } String countQueryStringNameVariableName = null; - String countQueryVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName)); + String countQueryVariableName = context + .localVariable("count%s".formatted(StringUtils.capitalize(queryVariableName))); if (queryMethod.isPageQuery() && queries.count() instanceof StringAotQuery sq) { - countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName)); + countQueryStringNameVariableName = context + .localVariable("count%sString".formatted(StringUtils.capitalize(queryVariableName))); builder.add(buildQueryString(sq, countQueryStringNameVariableName)); } @@ -201,7 +203,7 @@ public CodeBlock build() { if (queryMethod.isPageQuery()) { - builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); + builder.beginControlFlow("$T $L = () ->", LongSupplier.class, context.localVariable("countAll")); boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); @@ -235,17 +237,21 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe builder.beginControlFlow("if ($L.isSorted())", sort); } - builder.addStatement("$T declaredQuery = $T.$L($L)", DeclaredQuery.class, DeclaredQuery.class, + builder.addStatement("$T $L = $T.$L($L)", DeclaredQuery.class, context.localVariable("declaredQuery"), + DeclaredQuery.class, queries != null && queries.isNative() ? "nativeQuery" : "jpqlQuery", queryString); boolean hasDynamicReturnType = StringUtils.hasText(dynamicReturnType); if (hasSort && hasDynamicReturnType) { - builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $L)", queryString, sort, dynamicReturnType); + builder.addStatement("$L = rewriteQuery($L, $L, $L)", queryString, context.localVariable("declaredQuery"), sort, + dynamicReturnType); } else if (hasSort) { - builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType); + builder.addStatement("$L = rewriteQuery($L, $L, $T.class)", queryString, context.localVariable("declaredQuery"), + sort, actualReturnType); } else if (hasDynamicReturnType) { - builder.addStatement("$L = rewriteQuery(declaredQuery, $T.unsorted(), $L)", queryString, Sort.class, + builder.addStatement("$L = rewriteQuery($L, $T.unsorted(), $L)", context.localVariable("declaredQuery"), + queryString, Sort.class, dynamicReturnType); } @@ -470,19 +476,21 @@ private CodeBlock applyEntityGraph(AotEntityGraph entityGraph, String queryVaria if (StringUtils.hasText(entityGraph.name())) { - builder.addStatement("$T entityGraph = $L.getEntityGraph($S)", jakarta.persistence.EntityGraph.class, + builder.addStatement("$T $L = $L.getEntityGraph($S)", jakarta.persistence.EntityGraph.class, + context.localVariable("entityGraph"), context.fieldNameOf(EntityManager.class), entityGraph.name()); } else { - builder.addStatement("$T<$T> entityGraph = $L.createEntityGraph($T.class)", + builder.addStatement("$T<$T> $L = $L.createEntityGraph($T.class)", jakarta.persistence.EntityGraph.class, context.getActualReturnType().getType(), + context.localVariable("entityGraph"), context.fieldNameOf(EntityManager.class), context.getActualReturnType().getType()); for (String attributePath : entityGraph.attributePaths()) { String[] pathComponents = StringUtils.delimitedListToStringArray(attributePath, "."); - StringBuilder chain = new StringBuilder("entityGraph"); + StringBuilder chain = new StringBuilder(context.localVariable("entityGraph")); for (int i = 0; i < pathComponents.length; i++) { if (i < pathComponents.length - 1) { @@ -495,7 +503,8 @@ private CodeBlock applyEntityGraph(AotEntityGraph entityGraph, String queryVaria builder.addStatement(chain.toString(), (Object[]) pathComponents); } - builder.addStatement("$L.setHint($S, entityGraph)", queryVariableName, entityGraph.type().getKey()); + builder.addStatement("$L.setHint($S, $L)", queryVariableName, entityGraph.type().getKey(), + context.localVariable("entityGraph")); } return builder.build(); @@ -521,17 +530,19 @@ static class QueryExecutionBlockBuilder { private final AotQueryMethodGenerationContext context; private final JpaQueryMethod queryMethod; private @Nullable AotQuery aotQuery; - private String queryVariableName = "query"; + private String queryVariableName; private MergedAnnotation modifying = MergedAnnotation.missing(); private QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { + this.context = context; this.queryMethod = queryMethod; + this.queryVariableName = context.localVariable("query"); } public QueryExecutionBlockBuilder referencing(String queryVariableName) { - this.queryVariableName = queryVariableName; + this.queryVariableName = context.localVariable(queryVariableName); return this; } @@ -567,7 +578,7 @@ public CodeBlock build() { Class returnType = context.getMethod().getReturnType(); if (returnsModifying(returnType)) { - builder.addStatement("int result = $L.executeUpdate()", queryVariableName); + builder.addStatement("int $L = $L.executeUpdate()", context.localVariable("result"), queryVariableName); } else { builder.addStatement("$L.executeUpdate()", queryVariableName); } @@ -577,11 +588,11 @@ public CodeBlock build() { } if (returnType == int.class || returnType == long.class || returnType == Integer.class) { - builder.addStatement("return result"); + builder.addStatement("return $L", context.localVariable("result")); } if (returnType == Long.class) { - builder.addStatement("return (long) result"); + builder.addStatement("return (long) $L", context.localVariable("result")); } return builder.build(); @@ -589,16 +600,20 @@ public CodeBlock build() { if (aotQuery != null && aotQuery.isDelete()) { - builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); - builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class)); + builder.addStatement("$T<$T> $L = $L.getResultList()", List.class, actualReturnType, + context.localVariable("resultList"), queryVariableName); + builder.addStatement("$L.forEach($L::remove)", context.localVariable("resultList"), + context.fieldNameOf(EntityManager.class)); if (!context.getReturnType().isAssignableFrom(List.class)) { if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { - builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType()); + builder.addStatement("return $T.valueOf($L.size())", context.getMethod().getReturnType(), + context.localVariable("resultList")); } else { - builder.addStatement("return resultList.isEmpty() ? null : resultList.iterator().next()"); + builder.addStatement("return $L.isEmpty() ? null : $L.iterator().next()", + context.localVariable("resultList"), context.localVariable("resultList")); } } else { - builder.addStatement("return resultList"); + builder.addStatement("return $L", context.localVariable("resultList")); } } else if (aotQuery != null && aotQuery.isExists()) { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); @@ -609,25 +624,29 @@ public CodeBlock build() { TypeName queryResultType = TypeName.get(context.getActualReturnType().toClass()); if (queryMethod.isCollectionQuery()) { - builder.addStatement("return ($T) convertMany(query.getResultList(), $L, $T.class)", - context.getReturnTypeName(), aotQuery.isNative(), queryResultType); + builder.addStatement("return ($T) convertMany($L.getResultList(), $L, $T.class)", + context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType); } else if (queryMethod.isStreamQuery()) { - builder.addStatement("return ($T) convertMany(query.getResultStream(), $L, $T.class)", - context.getReturnTypeName(), aotQuery.isNative(), queryResultType); + builder.addStatement("return ($T) convertMany($L.getResultStream(), $L, $T.class)", + context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType); } else if (queryMethod.isPageQuery()) { builder.addStatement( - "return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, countAll)", + "return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, $L)", PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, aotQuery.isNative(), - queryResultType, context.getPageableParameterName()); + queryResultType, context.getPageableParameterName(), context.localVariable("countAll")); } else if (queryMethod.isSliceQuery()) { - builder.addStatement("$T<$T> resultList = ($T<$T>) convertMany($L.getResultList(), $L, $T.class)", - List.class, actualReturnType, List.class, actualReturnType, queryVariableName, aotQuery.isNative(), + builder.addStatement("$T<$T> $L = ($T<$T>) convertMany($L.getResultList(), $L, $T.class)", List.class, + actualReturnType, context.localVariable("resultList"), List.class, actualReturnType, queryVariableName, + aotQuery.isNative(), queryResultType); - builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", - context.getPageableParameterName(), context.getPageableParameterName()); + builder.addStatement("boolean $L = $L.isPaged() && $L.size() > $L.getPageSize()", + context.localVariable("hasNext"), context.getPageableParameterName(), + context.localVariable("resultList"), context.getPageableParameterName()); builder.addStatement( - "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", - SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + "return new $T<>($L ? $L.subList(0, $L.getPageSize()) : $L, $L, $L)", SliceImpl.class, + context.localVariable("hasNext"), context.localVariable("resultList"), + context.getPageableParameterName(), context.localVariable("resultList"), + context.getPageableParameterName(), context.localVariable("hasNext")); } else { if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { @@ -642,21 +661,24 @@ public CodeBlock build() { } else { if (queryMethod.isCollectionQuery()) { - builder.addStatement("return ($T) query.getResultList()", context.getReturnTypeName()); + builder.addStatement("return ($T) $L.getResultList()", context.getReturnTypeName(), queryVariableName); } else if (queryMethod.isStreamQuery()) { - builder.addStatement("return ($T) query.getResultStream()", context.getReturnTypeName()); + builder.addStatement("return ($T) $L.getResultStream()", context.getReturnTypeName(), queryVariableName); } else if (queryMethod.isPageQuery()) { - builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", + builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, $L)", PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, - context.getPageableParameterName()); + context.getPageableParameterName(), context.localVariable("countAll")); } else if (queryMethod.isSliceQuery()) { - builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, - queryVariableName); - builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", - context.getPageableParameterName(), context.getPageableParameterName()); + builder.addStatement("$T<$T> $L = $L.getResultList()", List.class, actualReturnType, + context.localVariable("resultList"), queryVariableName); + builder.addStatement("boolean $L = $L.isPaged() && $L.size() > $L.getPageSize()", + context.localVariable("hasNext"), context.getPageableParameterName(), + context.localVariable("resultList"), context.getPageableParameterName()); builder.addStatement( - "return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", - SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + "return new $T<>($L ? $L.subList(0, $L.getPageSize()) : $L, $L, $L)", SliceImpl.class, + context.localVariable("hasNext"), context.localVariable("resultList"), + context.getPageableParameterName(), context.localVariable("resultList"), + context.getPageableParameterName(), context.localVariable("hasNext")); } else { if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 54ae048b59..cc921ff78b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -36,20 +36,18 @@ import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.data.repository.aot.generate.AotRepositoryClassBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata; import org.springframework.data.repository.aot.generate.MethodContributor; import org.springframework.data.repository.aot.generate.QueryMetadata; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; -import org.springframework.javapoet.TypeSpec; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -88,9 +86,8 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityMa } @Override - protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata, - TypeSpec.Builder builder) { - builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class)); + protected void customizeClass(AotRepositoryClassBuilder classBuilder) { + classBuilder.customize(builder -> builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class))); } @Override @@ -102,16 +99,15 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB constructorBuilder.addParameter("context", RepositoryFactoryBeanSupport.FragmentCreationContext.class); // TODO: Pick up the configured QueryEnhancerSelector - constructorBuilder.customize((repositoryInformation, builder) -> { + constructorBuilder.customize(builder -> { builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class); }); } @Override - protected @Nullable MethodContributor contributeQueryMethod(Method method, - RepositoryInformation repositoryInformation) { + protected @Nullable MethodContributor contributeQueryMethod(Method method) { - JpaQueryMethod queryMethod = new JpaQueryMethod(method, repositoryInformation, getProjectionFactory(), + JpaQueryMethod queryMethod = new JpaQueryMethod(method, getRepositoryInformation(), getProjectionFactory(), persistenceProvider); // meh! @@ -125,7 +121,6 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB MethodContributor.QueryMethodMetadataContributorBuilder builder = MethodContributor .forQueryMethod(queryMethod); - if (procedure != null) { if (StringUtils.hasText(procedure.name())) { @@ -150,7 +145,7 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB MergedAnnotation query = MergedAnnotations.from(method).get(Query.class); - AotQueries aotQueries = queriesFactory.createQueries(repositoryInformation, query, selector, queryMethod, + AotQueries aotQueries = queriesFactory.createQueries(getRepositoryInformation(), query, selector, queryMethod, returnedType); // no KeysetScrolling for now. @@ -167,7 +162,7 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB if (queryMethod.isModifyingQuery()) { - TypeInformation returnType = repositoryInformation.getReturnType(method); + TypeInformation returnType = getRepositoryInformation().getReturnType(method); boolean returnsCount = JpaCodeBlocks.QueryExecutionBlockBuilder.returnsModifying(returnType.getType()); @@ -182,26 +177,26 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB return MethodContributor.forQueryMethod(queryMethod).withMetadata(aotQueries.toMetadata(queryMethod.isPageQuery())) .contribute(context -> { - CodeBlock.Builder body = CodeBlock.builder(); + CodeBlock.Builder body = CodeBlock.builder(); - MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class); - MergedAnnotation queryHints = context.getAnnotation(QueryHints.class); - MergedAnnotation entityGraph = context.getAnnotation(EntityGraph.class); - MergedAnnotation modifying = context.getAnnotation(Modifying.class); + MergedAnnotation nativeQuery = context.getAnnotation(NativeQuery.class); + MergedAnnotation queryHints = context.getAnnotation(QueryHints.class); + MergedAnnotation entityGraph = context.getAnnotation(EntityGraph.class); + MergedAnnotation modifying = context.getAnnotation(Modifying.class); - AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation, - returnedType, queryMethod); + AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, getRepositoryInformation(), + returnedType, queryMethod); - body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) - .queryReturnType(QueriesFactory.getQueryReturnType(aotQueries.result(), returnedType, context)) - .nativeQuery(nativeQuery).queryHints(queryHints).entityGraph(aotEntityGraph) - .queryRewriter(query.isPresent() ? query.getClass("queryRewriter") : null).build()); + body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries) + .queryReturnType(QueriesFactory.getQueryReturnType(aotQueries.result(), returnedType, context)) + .nativeQuery(nativeQuery).queryHints(queryHints).entityGraph(aotEntityGraph) + .queryRewriter(query.isPresent() ? query.getClass("queryRewriter") : null).build()); - body.add( - JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build()); + body.add(JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()) + .build()); - return body.build(); - }); + return body.build(); + }); } record StoredProcedureMetadata(String procedure) implements QueryMetadata { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java index 7279abf7dc..d53facc7ec 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/UserRepository.java @@ -104,8 +104,9 @@ interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like ?1%") List findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort); + // nasty parameter names @Query("select u from User u where u.lastname like ?1%") - List findAnnotatedQueryByLastname(String lastname, Pageable pageable); + List findAnnotatedQueryByLastname(String query, Pageable queryString); @Query("select u from User u where u.lastname like ?1%") Page findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable); diff --git a/spring-data-jpa/src/test/resources/logback.xml b/spring-data-jpa/src/test/resources/logback.xml index 2df750b92a..b16caaa18c 100644 --- a/spring-data-jpa/src/test/resources/logback.xml +++ b/spring-data-jpa/src/test/resources/logback.xml @@ -19,7 +19,8 @@ - + From cf6dee91413873bbdf9380217cdd0445b17bc8c6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 May 2025 16:45:09 +0200 Subject: [PATCH 87/94] Use isolated Hibernate `EntityManager` for AOT contribution. Closes #3876 --- .../aot/JpaRepositoryContributor.java | 2 ++ .../config/JpaRepositoryConfigExtension.java | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index cc921ff78b..01d7c92f05 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -68,6 +68,7 @@ public class JpaRepositoryContributor extends RepositoryContributor { private final EntityGraphLookup entityGraphLookup; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { + super(repositoryContext); AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes()); @@ -78,6 +79,7 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { + super(repositoryContext); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index eb89f0af8d..ce3218593f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -325,6 +325,8 @@ static boolean isActive(@Nullable ClassLoader classLoader) { */ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor { + String GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER = "spring.aot.jpa.repositories.use-entitymanager"; + protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { @@ -334,11 +336,20 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi return null; } - ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); - EntityManagerFactory emf = beanFactory.getBeanProvider(EntityManagerFactory.class).getIfAvailable(); + boolean useEntityManager = Boolean.parseBoolean( + repositoryContext.getEnvironment().getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); + + if (useEntityManager) { + + ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); + EntityManagerFactory emf = beanFactory.getBeanProvider(EntityManagerFactory.class).getIfAvailable(); + + if (emf != null) { + return new JpaRepositoryContributor(repositoryContext, emf); + } + } - return emf != null ? new JpaRepositoryContributor(repositoryContext, emf) - : new JpaRepositoryContributor(repositoryContext); + return new JpaRepositoryContributor(repositoryContext); } } } From bc54aec62a7f187b286a66c6c066cbd212bdaec5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 10:26:27 +0200 Subject: [PATCH 88/94] Use dynamic copyright years in documentation, adopt to feature flags. See #3853 --- src/main/antora/modules/ROOT/pages/index.adoc | 3 +-- .../modules/ROOT/pages/repositories/query-methods-details.adoc | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/index.adoc b/src/main/antora/modules/ROOT/pages/index.adoc index 4f9d18adce..37753da700 100644 --- a/src/main/antora/modules/ROOT/pages/index.adoc +++ b/src/main/antora/modules/ROOT/pages/index.adoc @@ -2,7 +2,6 @@ = Spring Data JPA :revnumber: {version} :revdate: {localdate} -:feature-scroll: true _Spring Data JPA provides repository support for the Jakarta Persistence API (JPA). It eases development of applications with a consistent programming model that need to access JPA data sources._ @@ -15,7 +14,7 @@ Upgrade Notes, Supported Versions, additional cross-version information. Oliver Gierke, Thomas Darimont, Christoph Strobl, Mark Paluch, Jay Bryant, Greg Turnquist -(C) 2008-2025 VMware, Inc. +(C) 2008-{copyright-year} VMware, Inc. Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc index dfe4814955..614da0b059 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc @@ -1 +1,2 @@ +:feature-scroll: include::{commons}@data-commons::page$repositories/query-methods-details.adoc[] From aaa1cac3bc6b39b4cdb71ea1adfa30ee68688dc0 Mon Sep 17 00:00:00 2001 From: Lidoca <32785562+Lidoca@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:35:03 +0900 Subject: [PATCH 89/94] Document OpenFeign Querydsl usage. Signed-off-by: Lidoca <32785562+Lidoca@users.noreply.github.com> Closes #3859 --- .../pages/repositories/core-extensions.adoc | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc index 251542dbff..e720f30621 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc @@ -80,6 +80,99 @@ dependencies { ==== ====== +Or if you use an OpenFeign fork: + +[tabs] +====== +Maven:: ++ +[source,xml,indent=0,subs="verbatim,quotes",role="primary"] +---- + + + io.github.openfeign.querydsl + querydsl-jpa + ${querydslVersion} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + io.github.openfeign.querydsl + querydsl-apt + ${querydslVersion} + jpa + + + jakarta.persistence + jakarta.persistence-api + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-source + generate-sources + + add-source + + + + target/generated-sources + + + + + add-test-source + generate-test-sources + + add-test-source + + + + target/generated-test-sources + + + + + + + +---- + +Gradle:: ++ +==== +[source,groovy,indent=0,subs="verbatim,quotes",role="secondary"] +---- +dependencies { + + implementation "io.github.openfeign.querydsl:querydsl-jpa:${querydslVersion}" + annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:${querydslVersion}:jpa" + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + + testAnnotationProcessor "io.github.openfeign.querydsl:querydsl-apt:${querydslVersion}:jpa" + testAnnotationProcessor 'jakarta.persistence:jakarta.persistence-api' +} +---- +==== +====== + Note that the setup above shows the simplemost usage omitting any other options or dependencies that your project might require. include::{commons}@data-commons::page$repositories/core-extensions-web.adoc[leveloffset=1] From ef7761c711486f2a85f45c783158610b3d9ae321 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 11:28:34 +0200 Subject: [PATCH 90/94] Polishing. Merge Openfeign setup tabs. See #3859 --- .../pages/repositories/core-extensions.adoc | 71 +++++-------------- .../resources/antora-resources/antora.yml | 1 + 2 files changed, 19 insertions(+), 53 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc index e720f30621..754f08c357 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc @@ -38,20 +38,20 @@ Maven:: org.apache.maven.plugins maven-compiler-plugin - - - - com.querydsl - querydsl-apt - ${querydslVersion} - jakarta - - - jakarta.persistence - jakarta.persistence-api - - + + com.querydsl + querydsl-apt + ${querydslVersion} + jakarta + + + jakarta.persistence + jakarta.persistence-api + + target/generated-test-sources @@ -78,13 +78,8 @@ dependencies { } ---- ==== -====== - -Or if you use an OpenFeign fork: -[tabs] -====== -Maven:: +Maven (OpenFeign):: + [source,xml,indent=0,subs="verbatim,quotes",role="primary"] ---- @@ -116,46 +111,16 @@ Maven:: jakarta.persistence-api + + target/generated-test-sources + target/generated-sources - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.0 - - - add-source - generate-sources - - add-source - - - - target/generated-sources - - - - - add-test-source - generate-test-sources - - add-test-source - - - - target/generated-test-sources - - - - - ---- -Gradle:: +Gradle (OpenFeign):: + ==== [source,groovy,indent=0,subs="verbatim,quotes",role="secondary"] diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index ed14d8c6d8..eedc4999e3 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -4,6 +4,7 @@ prerelease: ${antora-component.prerelease} asciidoc: attributes: version: ${project.version} + copyright-year: ${current.year} springversionshort: ${spring.short} springversion: ${spring} attribute-missing: 'warn' From db5a441b850a1c4537a2f7666a53c93868bd23b7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 12:14:17 +0200 Subject: [PATCH 91/94] Fliter jakarta.persistence types from AOT Metamodel. Otherwise, Hibernate fails with weird resolution errors. See #3872 --- .../springframework/data/jpa/repository/aot/AotMetamodel.java | 2 +- .../data/jpa/repository/aot/JpaRepositoryContributor.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java index 3c1ddd6e33..2b3f49bb28 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/AotMetamodel.java @@ -47,7 +47,7 @@ class AotMetamodel implements Metamodel { private final Lazy entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); public AotMetamodel(Set> managedTypes) { - this("dynamic-tests", managedTypes); + this("AotMetamodel", managedTypes); } private AotMetamodel(String persistenceUnit, Set> managedTypes) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java index 01d7c92f05..1dcb10809b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributor.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.Map; +import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -71,7 +72,8 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); - AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes()); + AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes().stream() + .filter(it -> !it.getName().startsWith("jakarta.persistence")).collect(Collectors.toSet())); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory()); this.queriesFactory = new QueriesFactory(amm.getEntityManagerFactory(), amm); From cdcac861547eebb65d911fc5d426a0aa8f2d0538 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 31 Jan 2025 09:58:20 +0100 Subject: [PATCH 92/94] Use `SelectionQuery.getResultCount()` for count queries if possible. We now use Hibernate's built-in mechanism to obtain the result count if there is an enclosing transaction. Without the transaction, the session is being closed and we cannot run the query. Closes #3456 --- .../jpa/provider/PersistenceProvider.java | 31 +++++++++++++++++ .../repository/query/AbstractJpaQuery.java | 11 ++++++- .../query/AbstractStringBasedJpaQuery.java | 7 ++-- .../repository/query/JpaQueryExecution.java | 33 +++++++++++++++++-- .../data/jpa/repository/query/NamedQuery.java | 5 +++ .../repository/query/PartTreeJpaQuery.java | 5 +++ .../query/StoredProcedureJpaQuery.java | 5 +++ .../query/AbstractJpaQueryTests.java | 5 +++ .../query/JpaQueryExecutionUnitTests.java | 13 ++++---- .../modules/ROOT/pages/jpa/query-methods.adoc | 2 +- 10 files changed, 104 insertions(+), 13 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 4d604b452c..9755f19f09 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Set; +import java.util.function.LongSupplier; import org.eclipse.persistence.config.QueryHints; import org.eclipse.persistence.jpa.JpaQuery; @@ -37,6 +38,7 @@ import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.proxy.HibernateProxy; +import org.hibernate.query.SelectionQuery; import org.jspecify.annotations.Nullable; import org.springframework.data.util.CloseableIterator; @@ -117,6 +119,17 @@ public String getCommentHintKey() { return "org.hibernate.comment"; } + @Override + public long getResultCount(Query resultQuery, LongSupplier countSupplier) { + + if (TransactionSynchronizationManager.isActualTransactionActive() + && resultQuery instanceof SelectionQuery sq) { + return sq.getResultCount(); + } + + return super.getResultCount(resultQuery, countSupplier); + } + }, /** @@ -160,6 +173,7 @@ public String getCommentHintKey() { public String getCommentHintValue(String comment) { return "/* " + comment + " */"; } + }, /** @@ -197,6 +211,7 @@ public boolean shouldUseAccessorFor(Object entity) { public @Nullable String getCommentHintKey() { return null; } + }; private static final @Nullable Class typedParameterValueClass; @@ -406,6 +421,18 @@ public boolean isPresent() { return this.present; } + /** + * Obtain the result count from a {@link Query} returning the result or fall back to {@code countSupplier} if the + * query does not provide the result count. + * + * @param resultQuery the query that has returned {@link Query#getResultList()} + * @param countSupplier fallback supplier to provide the count if the query does not provide it. + * @return the result count. + */ + public long getResultCount(Query resultQuery, LongSupplier countSupplier) { + return countSupplier.getAsLong(); + } + /** * Holds the PersistenceProvider specific interface names. * @@ -427,6 +454,7 @@ interface Constants { String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel"; String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; + } public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { @@ -482,6 +510,7 @@ public void close() { scrollableResults.close(); } } + } /** @@ -531,5 +560,7 @@ public void close() { scrollableCursor.close(); } } + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 4e672ccc80..ef604e1f5b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -106,7 +106,7 @@ public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) { } else if (method.isSliceQuery()) { return new SlicedExecution(); } else if (method.isPageQuery()) { - return new PagedExecution(); + return new PagedExecution(this.provider); } else if (method.isModifyingQuery()) { return null; } else { @@ -120,6 +120,15 @@ public JpaQueryMethod getQueryMethod() { return method; } + /** + * Returns {@literal true} if the query has a dedicated count query associated with it or {@literal false} if the + * count query shall be derived. + * + * @return {@literal true} if the query has a dedicated count query {@literal false} if the * count query is derived. + * @since 3.5 + */ + public abstract boolean hasDeclaredCountQuery(); + /** * Returns the {@link EntityManager}. * 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 64b21e8cb5..c288d4a350 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 @@ -62,6 +62,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final QuerySortRewriter querySortRewriter; private final Lazy countParameterBinder; private final ValueEvaluationContextProvider valueExpressionContextProvider; + private final boolean hasDeclaredCountQuery; /** * Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and @@ -101,6 +102,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Decl this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); this.query = TemplatedQuery.create(query, method.getEntityInformation(), queryConfiguration); + this.hasDeclaredCountQuery = countQuery != null; this.countQuery = Lazy.of(() -> { @@ -130,8 +132,9 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Decl "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 boolean hasDeclaredCountQuery() { + return hasDeclaredCountQuery; } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 338a2204e8..be0a09bc4c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java @@ -188,6 +188,12 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso */ static class PagedExecution extends JpaQueryExecution { + private final PersistenceProvider provider; + + PagedExecution(PersistenceProvider provider) { + this.provider = provider; + } + @Override @SuppressWarnings("unchecked") protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) { @@ -195,13 +201,34 @@ protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParame Query query = repositoryQuery.createQuery(accessor); return PageableExecutionUtils.getPage(query.getResultList(), accessor.getPageable(), - () -> count(repositoryQuery, accessor)); + () -> count(query, repositoryQuery, accessor)); + } + + private long count(Query resultQuery, AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) { + + if (repositoryQuery.hasDeclaredCountQuery()) { + return doCount(repositoryQuery, accessor); + } + + return provider.getResultCount(resultQuery, () -> doCount(repositoryQuery, accessor)); } - private long count(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) { + long doCount(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) { List totals = repositoryQuery.createCountQuery(accessor).getResultList(); - return (totals.size() == 1 ? CONVERSION_SERVICE.convert(totals.get(0), Long.class) : totals.size()); + + if (totals.size() == 1) { + Object result = totals.get(0); + + if (result instanceof Number n) { + return n.longValue(); + } + + return CONVERSION_SERVICE.convert(result, Long.class); + } + + // group by count + return totals.size(); } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index a38bf9eaaa..125ec40c66 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -182,6 +182,11 @@ static boolean hasNamedQuery(EntityManager em, String queryName) { return query; } + @Override + public boolean hasDeclaredCountQuery() { + return namedCountQueryIsPresent; + } + @Override protected Query doCreateQuery(JpaParametersParameterAccessor accessor) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index 66dac47929..bf254c46ba 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -112,6 +112,11 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { } } + @Override + public boolean hasDeclaredCountQuery() { + return false; + } + @Override public Query doCreateQuery(JpaParametersParameterAccessor accessor) { return queryPreparer.createQuery(accessor); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java index 3423c71e45..caece33d0f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java @@ -81,6 +81,11 @@ private static boolean useNamedParameters(QueryMethod method) { return false; } + @Override + public boolean hasDeclaredCountQuery() { + return false; + } + @Override protected StoredProcedureQuery createQuery(JpaParametersParameterAccessor accessor) { return applyHints(doCreateQuery(accessor), getQueryMethod()); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractJpaQueryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractJpaQueryTests.java index 8728e03229..fdcbabf84b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractJpaQueryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractJpaQueryTests.java @@ -230,6 +230,11 @@ protected Query doCreateQuery(JpaParametersParameterAccessor accessor) { return query; } + @Override + public boolean hasDeclaredCountQuery() { + return true; + } + @Override protected TypedQuery doCreateCountQuery(JpaParametersParameterAccessor accessor) { return countQuery; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java index e8907f16fc..c7d160d6d1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java @@ -40,6 +40,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution; @@ -183,7 +184,7 @@ void pagedExecutionRetrievesObjectsForPageableOutOfRange() throws Exception { when(jpaQuery.createQuery(Mockito.any())).thenReturn(query); when(countQuery.getResultList()).thenReturn(Arrays.asList(20L)); - PagedExecution execution = new PagedExecution(); + PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA); execution.doExecute(jpaQuery, new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(2, 10) })); @@ -199,7 +200,7 @@ void pagedExecutionShouldNotGenerateCountQueryIfQueryReportedNoResults() throws when(jpaQuery.createQuery(Mockito.any())).thenReturn(query); when(query.getResultList()).thenReturn(Arrays.asList(0L)); - PagedExecution execution = new PagedExecution(); + PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA); execution.doExecute(jpaQuery, new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(0, 10) })); @@ -215,7 +216,7 @@ void pagedExecutionShouldUseCountFromResultIfOffsetIsZeroAndResultsWithinPageSiz when(jpaQuery.createQuery(Mockito.any())).thenReturn(query); when(query.getResultList()).thenReturn(Arrays.asList(new Object(), new Object(), new Object(), new Object())); - PagedExecution execution = new PagedExecution(); + PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA); execution.doExecute(jpaQuery, new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(0, 10) })); @@ -230,7 +231,7 @@ void pagedExecutionShouldUseCountFromResultWithOffsetAndResultsWithinPageSize() when(jpaQuery.createQuery(Mockito.any())).thenReturn(query); when(query.getResultList()).thenReturn(Arrays.asList(new Object(), new Object(), new Object(), new Object())); - PagedExecution execution = new PagedExecution(); + PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA); execution.doExecute(jpaQuery, new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(5, 10) })); @@ -247,7 +248,7 @@ void pagedExecutionShouldUseRequestCountFromResultWithOffsetAndResultsHitLowerPa when(jpaQuery.createCountQuery(Mockito.any())).thenReturn(query); when(countQuery.getResultList()).thenReturn(Arrays.asList(20L)); - PagedExecution execution = new PagedExecution(); + PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA); execution.doExecute(jpaQuery, new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(4, 4) })); @@ -264,7 +265,7 @@ void pagedExecutionShouldUseRequestCountFromResultWithOffsetAndResultsHitUpperPa when(jpaQuery.createCountQuery(Mockito.any())).thenReturn(query); when(countQuery.getResultList()).thenReturn(Arrays.asList(20L)); - PagedExecution execution = new PagedExecution(); + PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA); execution.doExecute(jpaQuery, new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(4, 4) })); diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 5513309f4f..b947fca73f 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -294,7 +294,7 @@ Sometimes, no matter how many features you try to apply, it seems impossible to 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. Query rewriting applies to the actual query and, when applicable, to count queries. -Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery`. +Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery` if there is an enclosing transaction. .Declare a QueryRewriter using `@Query` ==== From 67421cef127a6dc2ff92baa2c1d4f8544e40fc8c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 14:44:46 +0200 Subject: [PATCH 93/94] Prepare issue branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 4847874dd6..8abc92784d 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3877-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 43c08369f6..ff3b6b44b4 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 4.0.0-SNAPSHOT + 4.0.0-GH-3877-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3877-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..9afab6d3ee 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3877-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 1cc6674063..6ca6787c8e 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-jpa - 4.0.0-SNAPSHOT + 4.0.0-GH-3877-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-GH-3877-SNAPSHOT ../pom.xml From 74cd50a7056473d6b8c1a5819640883a3978e679 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 14:53:01 +0200 Subject: [PATCH 94/94] Add `delete(Predicate)` to `QuerydslJpaPredicateExecutor`. We now define a delete method to remove entities by a Querydsl Predicate and return the count of deleted elements. --- .../support/QuerydslContributor.java | 4 +-- .../support/QuerydslJpaPredicateExecutor.java | 27 +++++++++++++++++++ .../support/SimpleJpaRepository.java | 13 +++++++-- .../jpa/repository/UserRepositoryTests.java | 11 ++++++++ .../aot/AotContributionIntegrationTests.java | 2 +- .../jpa/repository/sample/UserRepository.java | 5 ++++ ...QuerydslJpaPredicateExecutorUnitTests.java | 11 ++++++++ 7 files changed, 68 insertions(+), 5 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java index 5f5e819c7b..280ac954c3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslContributor.java @@ -54,7 +54,7 @@ public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata m resolver, null); return RepositoryComposition.RepositoryFragments - .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor)); + .of(RepositoryFragment.implemented(QuerydslJpaPredicateExecutor.class, executor)); } return RepositoryComposition.RepositoryFragments.empty(); @@ -65,7 +65,7 @@ public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata met if (isQuerydslRepository(metadata)) { return RepositoryComposition.RepositoryFragments - .of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslJpaPredicateExecutor.class)); + .of(RepositoryFragment.structural(QuerydslJpaPredicateExecutor.class, QuerydslJpaPredicateExecutor.class)); } return RepositoryComposition.RepositoryFragments.empty(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 0bbcee84bd..d04ca6d8b5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -263,6 +263,33 @@ public long count(Predicate predicate) { return createQuery(predicate).fetchCount(); } + /** + * Delete entities by the given {@link Predicate} by loading and removing these. + *

        + * This method is useful for a small amount of entities. For large amounts of entities, consider using batch deletes + * by declaring a delete query yourself. + * + * @param predicate the {@link Predicate} to delete entities by, must not be {@literal null}. + * @return number of deleted entities. + * @since 4.0 + */ + public long delete(Predicate predicate) { + + Assert.notNull(predicate, PREDICATE_MUST_NOT_BE_NULL); + + List results = (List) createQuery(predicate).fetch(); + + int deleted = 0; + + for (T entity : results) { + if (SimpleJpaRepository.doDelete(entityManager, entityInformation, entity)) { + deleted++; + } + } + + return deleted; + } + @Override public boolean exists(Predicate predicate) { return createQuery(predicate).select(Expressions.ONE).fetchFirst() != null; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 4226891175..89395e4fcf 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -205,13 +205,18 @@ public void delete(T entity) { Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL); + doDelete(entityManager, entityInformation, entity); + } + + static boolean doDelete(EntityManager entityManager, JpaEntityInformation entityInformation, T entity) { + if (entityInformation.isNew(entity)) { - return; + return false; } if (entityManager.contains(entity)) { entityManager.remove(entity); - return; + return true; } Class type = ProxyUtils.getUserClass(entity); @@ -220,7 +225,11 @@ public void delete(T entity) { T existing = (T) entityManager.find(type, entityInformation.getId(entity)); if (existing != null) { entityManager.remove(entityManager.merge(entity)); + + return true; } + + return false; } @Override 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 0ebf726932..8980836d8d 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 @@ -2859,6 +2859,17 @@ void findByFluentSpecificationWithSimplePropertyPathsDoesntLoadUnrequestedPaths( ); } + @Test // GH-3877 + void delete() { + + flushTestUsers(); + em.clear(); + + long delete = repository.delete(QUser.user.firstname.eq(firstUser.getFirstname())); + + assertThat(delete).isEqualTo(1); + } + @Test // GH-2820 void findByFluentPredicateWithProjectionAndPageRequest() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java index 76390740ad..7f9cd170ec 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/AotContributionIntegrationTests.java @@ -60,7 +60,7 @@ void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOExcep String json = isr.getContentAsString(StandardCharsets.UTF_8); assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() - .containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor") + .containsEntry("interface", "org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor") .containsEntry("fragment", "org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor"); assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() 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 2fc34657f8..efe9d564a2 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 @@ -55,6 +55,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import com.querydsl.core.types.Predicate; + /** * Repository interface for {@code User}s. * @@ -774,6 +776,9 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter List findWithParameterNameByLastnameStartingWithOrLastnameEndingWith(@Param("l1") String l1, @Param("l2") String l2); + // surface QuerydslJpaPredicateExecutor.delete(…) method + long delete(Predicate predicate); + @Retention(RetentionPolicy.RUNTIME) @Query("select u, count(r) from User u left outer join u.roles r group by u") @interface UserRoleCountProjectingQuery { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java index d988dc72d5..8304430499 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutorUnitTests.java @@ -551,6 +551,17 @@ void findByFluentPredicateWithComplexPropertyPathsDoesntLoadsRequestedPaths() { assertThat(users).allMatch(u -> u.getRoles().isEmpty()); } + @Test // GH-3877 + void deleteShouldDeleteUsers() { + + long deleted = predicateExecutor.delete(user.dateOfBirth.isNull()); + + assertThat(deleted).isEqualTo(3); + em.flush(); + + assertThat(predicateExecutor.findAll(user.dateOfBirth.isNull())).isEmpty(); + } + private interface UserProjectionInterfaceBased { String getFirstname();