From 845e169619a48027d65061bbfe65f0e6c6702e0a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:50:31 +0200 Subject: [PATCH 001/224] Prepare next development iteration. See #3853 --- pom.xml | 10 +++++----- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index e5fefdec28..b1d77baee9 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.0 + 4.0.0-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.5 - 3.5.0 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -173,8 +173,8 @@ - - + + diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 40c50b7016..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 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.0 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 9f3f2ad5c4..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 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 2628916dc8..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 + 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 + 4.0.0-SNAPSHOT ../pom.xml From 7f97137ca03a15ce65704371bbcc77283fe0f9cc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 11:06:32 +0100 Subject: [PATCH 002/224] 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 d131eb7a4a4af54f968c11c5ba466dacdc30c7cd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 23 Aug 2024 10:23:22 +0200 Subject: [PATCH 003/224] 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 851a3918e0..2e6572959a 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 @@ -57,7 +57,6 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final Map, Boolean> knownProjections = new ConcurrentHashMap<>(); 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; @@ -130,11 +129,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); } /** @@ -238,9 +235,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 9ebddf394b..42f8e168db 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 @@ -3318,6 +3318,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 c624ec1d30..63991208fc 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 b69ef2b1213f3b10cc62eda9026629283769fd2c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 5 Nov 2024 16:17:01 +0100 Subject: [PATCH 004/224] 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 39e776cb6e8dc5813ac1339410539bda79f4b38d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Nov 2024 14:50:07 +0100 Subject: [PATCH 005/224] 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 ded2a63497dcab62cb8b7462b71a05035751149c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 21 Nov 2024 10:44:50 +0100 Subject: [PATCH 006/224] 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 6624f4b26a21f34df8a3a438fbccc253beadfbcb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:10:28 +0100 Subject: [PATCH 007/224] 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 b1d77baee9..5d84d548de 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.2 9.2.0 42.7.5 @@ -111,6 +111,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 @@ -151,6 +189,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 e983e1832a8eba60d313b45c120a69a8407bcd88 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:10:53 +0100 Subject: [PATCH 008/224] Upgrade to Hibernate 7.0 Beta1. Closes: #3671 Original Pull Request: #3695 --- Jenkinsfile | 44 ----------------- pom.xml | 47 ++----------------- .../query/QueryUtilsIntegrationTests.java | 2 +- 3 files changed, 4 insertions(+), 89 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 915e46ddb7..de8ad4ec91 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 5d84d548de..c2c66e62d2 100755 --- a/pom.xml +++ b/pom.xml @@ -28,12 +28,9 @@ 4.13.0 - 4.0.6 - 4.0.7-SNAPSHOT - 6.6.15.Final - 6.2.36.Final - 6.6.16-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

@@ -57,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 58a4768747d282a7f927fd51b17e914369c0c18c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:17:58 +0100 Subject: [PATCH 009/224] 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 c2c66e62d2..d6eb587d1c 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 88efe091e0..d9a74429a3 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 @@ -547,7 +547,7 @@ List findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity List findRolesAndFirstnameBy(); - @Query(value = "FROM User u") + @Query(value = "SELECT u FROM User u") List findIdOnly(); // DATAJPA-1172 @@ -643,13 +643,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 7c5eccc76f51f10ce018df1995cf2fe5b947a0e8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Dec 2024 08:46:59 +0100 Subject: [PATCH 010/224] 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 a3a9c927ed59011821452f03e4329b8089b15441 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:26:55 +0100 Subject: [PATCH 011/224] 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 ebda35d3856b7face39b7e0444b2fc604a050dca Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Fri, 1 Sep 2023 13:48:34 -0500 Subject: [PATCH 012/224] Add support for JPA 3.2 additions to JPQL. See: #3136 Original Pull Request: #3695 --- .../data/jpa/repository/query/Jpql.g4 | 11 ++- .../repository/query/JpqlQueryRenderer.java | 47 ++++++++++ .../query/HqlQueryRendererTests.java | 90 ++++++++++++------- .../query/JpqlQueryRendererTests.java | 53 +++++++++++ 4 files changed, 170 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 e0e2a0a35c..24e18e1cb3 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 @@ -443,6 +449,7 @@ string_expression | aggregate_expression | case_expression | function_invocation + | string_expression op='||' string_expression | string_cast_function | type_cast_function | '(' subquery ')' @@ -908,6 +915,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; @@ -972,6 +980,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 41ca183967..37295be4f5 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 @@ -80,6 +80,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; } @@ -800,6 +823,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; } @@ -1518,6 +1560,11 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(visit(ctx.type_cast_function())); } 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 6abc8b5048..8102c99ccd 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 @@ -1651,29 +1651,25 @@ 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-3882 @@ -1693,14 +1689,12 @@ void shouldSupportLimitOffset() { @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 @@ -1883,6 +1877,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 d3daa9e723..7cc49745e7 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 @@ -1034,6 +1034,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 262e05eda83ca436c0a9b8f99969a2bcd352a383 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 21 Jun 2024 18:14:07 +0200 Subject: [PATCH 013/224] Add support for JPA 3.2 additions to EQL. See: #3136 Original Pull Request: #3695 --- .../data/jpa/repository/query/Eql.g4 | 24 +++- .../data/jpa/repository/query/Jpql.g4 | 47 +++++++- .../repository/query/EqlQueryRenderer.java | 50 ++++++++- .../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, 345 insertions(+), 18 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 0c5468091a..266890d8ab 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 ; @@ -458,6 +459,7 @@ string_expression | string_cast_function | type_cast_function | '(' subquery ')' + | string_expression '||' string_expression ; datetime_expression @@ -542,6 +544,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 @@ -625,6 +630,14 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; +type_literal + : STRING + | INTEGER + | LONG + | FLOAT + | DOUBLE + ; + /******************* Gaps in the spec. *******************/ @@ -637,6 +650,7 @@ trim_character identification_variable : IDENTIFICATION_VARIABLE | f=(COUNT + | AS | DATE | FROM | INNER @@ -646,11 +660,13 @@ identification_variable | ORDER | OUTER | POWER + | RIGHT | FLOOR | SIGN | TIME | TYPE | VALUE) + | type_literal ; constructor_name @@ -832,6 +848,8 @@ reserved_word |OR |ORDER |OUTER + |REPLACE + |RIGHT |POWER |ROUND |SELECT @@ -928,6 +946,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; FLOAT : F L O A T; FROM : F R O M; @@ -937,6 +956,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; INTEGER : I N T E G E R; @@ -969,6 +989,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; @@ -976,6 +998,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; STRING : S T R I N G; SUM : S U M; @@ -996,7 +1019,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 24e18e1cb3..070874edbe 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 ; @@ -453,6 +466,7 @@ string_expression | string_cast_function | type_cast_function | '(' subquery ')' + | string_expression '||' string_expression ; datetime_expression @@ -536,7 +550,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 @@ -620,6 +637,10 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; +cast_expression + : CAST '(' string_expression AS type_literal ')' + ; + /******************* Gaps in the spec. *******************/ @@ -641,6 +662,7 @@ identification_variable | ORDER | OUTER | POWER + | RIGHT | FLOOR | SIGN | TIME @@ -691,6 +713,14 @@ numeric_literal | LONGLITERAL ; +type_literal + : STRING + | INTEGER + | LONG + | FLOAT + | DOUBLE + ; + boolean_literal : TRUE | FALSE @@ -827,6 +857,8 @@ reserved_word |ORDER |OUTER |POWER + |REPLACE + |RIGHT |ROUND |SELECT |SET @@ -922,6 +954,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; FLOAT : F L O A T; FROM : F R O M; @@ -931,6 +964,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; INTEGER : I N T E G E R; @@ -961,6 +995,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; REGEXP : R E G E X P; ROUND : R O U N D; @@ -970,6 +1006,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; STRING : S T R I N G; SUM : S U M; 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 22b264cc16..516b7c360d 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 @@ -24,6 +24,7 @@ import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. @@ -1009,6 +1010,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; @@ -1610,6 +1613,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; @@ -1941,6 +1949,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; @@ -1986,7 +2020,7 @@ public QueryTokenStream visitType_cast_function(EqlParser.Type_cast_functionCont if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendInline(visit(ctx.identification_variable())); + builder.appendInline(QueryTokenStream.concat(ctx.identification_variable(), this::visit, TOKEN_SPACE)); if (!CollectionUtils.isEmpty(ctx.numeric_literal())) { @@ -2106,6 +2140,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) { @@ -2226,9 +2268,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 37295be4f5..f07e8a0de8 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,17 +15,34 @@ */ 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.CollectionUtils; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. @@ -56,8 +73,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(); @@ -80,11 +106,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 @@ -800,6 +826,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) { @@ -974,6 +1013,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(); @@ -1570,6 +1611,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; @@ -1890,6 +1936,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; @@ -2048,6 +2117,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 8102c99ccd..03286b1d2f 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 @@ -1983,6 +1983,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 2b562420a678c36c873790ca0734b260ce48f466 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 28 Jun 2024 11:46:42 +0200 Subject: [PATCH 014/224] 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 3effe559296c3057813c1957abed78beed9648d0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Nov 2024 08:25:32 +0100 Subject: [PATCH 015/224] 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 | 33 +- .../query/EqlCountQueryTransformer.java | 4 +- .../repository/query/EqlQueryRenderer.java | 440 +++++++++--------- .../query/EqlSortedQueryTransformer.java | 15 +- .../query/JpqlCountQueryTransformer.java | 12 +- .../repository/query/JpqlQueryRenderer.java | 244 +++++----- .../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, 712 insertions(+), 425 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 266890d8ab..2181baec6c 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)? ; @@ -675,6 +679,7 @@ constructor_name literal : STRINGLITERAL + | JAVASTRINGLITERAL | INTLITERAL | FLOATLITERAL | LONGLITERAL @@ -848,9 +853,9 @@ reserved_word |OR |ORDER |OUTER + |POWER |REPLACE |RIGHT - |POWER |ROUND |SELECT |SET @@ -1021,6 +1026,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 070874edbe..90e590cd11 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 ; @@ -550,8 +542,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 ')' ; @@ -637,9 +629,6 @@ nullif_expression : NULLIF '(' scalar_expression ',' scalar_expression ')' ; -cast_expression - : CAST '(' string_expression AS type_literal ')' - ; /******************* Gaps in the spec. @@ -653,6 +642,7 @@ trim_character identification_variable : IDENTIFICATION_VARIABLE | f=(COUNT + | AS | DATE | FROM | INNER @@ -668,6 +658,7 @@ identification_variable | TIME | TYPE | VALUE) + | type_literal ; constructor_name @@ -695,6 +686,9 @@ pattern_value date_time_timestamp_literal : STRINGLITERAL + | DATELITERAL + | TIMELITERAL + | TIMESTAMPLITERAL ; entity_type_literal @@ -995,10 +989,10 @@ 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; 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; @@ -1033,4 +1027,7 @@ 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 516b7c360d..b36a7fb986 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 @@ -79,30 +79,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; @@ -228,9 +206,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())); } @@ -293,8 +273,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(); @@ -306,31 +285,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; @@ -452,8 +425,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(); @@ -496,12 +468,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; @@ -854,15 +829,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) { @@ -884,12 +859,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; @@ -994,83 +1001,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())); @@ -1083,27 +1089,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 @@ -1113,7 +1117,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())); @@ -1139,7 +1143,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())); @@ -1160,10 +1164,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())); @@ -1176,7 +1180,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) { @@ -1217,7 +1220,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())); } @@ -1239,11 +1243,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) { @@ -1265,7 +1269,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())); @@ -1280,7 +1284,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())); } @@ -1296,25 +1300,21 @@ 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) { return visit(ctx.identification_variable()); } else if (ctx.input_parameter() != null) { @@ -1368,13 +1368,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; @@ -1389,9 +1389,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; @@ -1411,9 +1411,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; @@ -1428,9 +1428,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; @@ -1445,9 +1445,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; @@ -1458,13 +1458,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; @@ -1477,7 +1477,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; } @@ -1496,42 +1496,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 @@ -1542,7 +1537,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; } @@ -1705,45 +1701,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 @@ -1918,7 +1908,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); @@ -1935,7 +1925,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) { @@ -1947,10 +1937,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))); @@ -1958,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.RIGHT() != null) { - builder.append(QueryTokens.token(ctx.RIGHT())); builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); @@ -1966,7 +1954,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))); @@ -1984,11 +1971,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())); } } @@ -2076,12 +2063,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; @@ -2096,12 +2086,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; @@ -2154,10 +2147,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())); @@ -2172,9 +2162,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; } @@ -2185,14 +2175,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; @@ -2253,11 +2240,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(); } } @@ -2265,14 +2252,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 @@ -2283,23 +2270,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 @@ -2355,7 +2342,7 @@ public QueryTokenStream visitEntity_type_literal(EqlParser.Entity_type_literalCo public QueryTokenStream visitEscape_character(EqlParser.Escape_characterContext ctx) { if (ctx.CHARACTER() != null) { - return QueryRendererBuilder.from(QueryTokens.token(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 if (ctx.string_literal() != null) { @@ -2369,13 +2356,13 @@ public QueryTokenStream visitEscape_character(EqlParser.Escape_characterContext 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(); } } @@ -2383,11 +2370,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(); } } @@ -2400,11 +2387,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(); } } @@ -2461,7 +2448,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 @@ -2492,26 +2479,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 f07e8a0de8..03b4866880 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; @@ -73,17 +59,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(); @@ -106,32 +83,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) { @@ -230,14 +188,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; @@ -248,9 +211,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; } @@ -301,23 +274,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; @@ -477,12 +452,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; @@ -826,19 +804,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) { @@ -854,48 +819,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; @@ -1013,8 +990,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(); @@ -1248,9 +1225,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; @@ -1601,11 +1580,6 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte builder.append(visit(ctx.type_cast_function())); } 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); @@ -1859,6 +1833,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; @@ -1886,6 +1862,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; @@ -1907,6 +1885,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) { @@ -2053,12 +2032,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; @@ -2073,12 +2055,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; @@ -2117,18 +2102,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) { @@ -2222,7 +2195,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); @@ -2248,7 +2221,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 { @@ -2308,7 +2283,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 @@ -2421,7 +2407,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 9ad73bae54..f78dc6a1f7 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 @@ -1041,6 +1041,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", @@ -1089,4 +1142,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 03286b1d2f..1f273c6391 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 @@ -1669,7 +1669,7 @@ void orderByWithNullsFirstOrLastShouldWork() { from Element a where a.erstelltDurch = :variable order by mutationAm desc nulls last - """); + """); } @Test // GH-3882 @@ -1690,7 +1690,7 @@ void shouldSupportLimitOffset() { 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 17249e2e34b78f39633dd495853c5e4aa9a88871 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Nov 2024 11:12:06 +0100 Subject: [PATCH 016/224] 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 +++++ 4 files changed, 23 insertions(+), 17 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 2181baec6c..24ac884f69 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 b36a7fb986..bc68ccf222 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 @@ -696,19 +696,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 6b1bf850e7..0d61c95861 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 @@ -1302,7 +1302,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 @@ -1331,12 +1331,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 @@ -1407,37 +1407,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() { From 7acd18f7520270b08289f037ea8c16718b246a79 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Nov 2024 14:55:47 +0100 Subject: [PATCH 017/224] 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 bc68ccf222..8e4878fa89 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 @@ -164,13 +164,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; } @@ -589,7 +584,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(); } @@ -1496,7 +1491,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 @@ -1916,16 +1911,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) { @@ -1971,11 +1969,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()); } } @@ -2240,7 +2238,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 { @@ -2252,11 +2250,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(); } @@ -2271,15 +2269,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) { @@ -2321,13 +2319,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(); } @@ -2342,7 +2340,7 @@ public QueryTokenStream visitEntity_type_literal(EqlParser.Entity_type_literalCo public QueryTokenStream visitEscape_character(EqlParser.Escape_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 if (ctx.string_literal() != null) { @@ -2356,11 +2354,11 @@ public QueryTokenStream visitEscape_character(EqlParser.Escape_characterContext 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(); } @@ -2370,9 +2368,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(); } @@ -2387,9 +2385,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(); } @@ -2482,7 +2480,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 { @@ -2493,9 +2491,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 0d61c95861..2a423e5830 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; @@ -661,7 +661,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 { @@ -693,7 +693,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 { @@ -705,9 +705,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(); } @@ -771,9 +771,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) { @@ -1022,13 +1022,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) { @@ -1048,9 +1048,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(); } @@ -1060,19 +1060,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(); } @@ -1444,25 +1444,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(); } @@ -1540,7 +1540,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 @@ -1553,11 +1553,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); } @@ -1734,7 +1730,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; @@ -2156,12 +2152,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 @@ -2173,12 +2169,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; @@ -2201,7 +2201,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())); @@ -2435,7 +2435,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 @@ -2497,7 +2497,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 @@ -2996,10 +2996,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) { @@ -3242,7 +3239,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 @@ -3259,7 +3256,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); @@ -3349,7 +3346,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()); @@ -3499,11 +3496,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(); @@ -3513,11 +3510,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(); @@ -3861,11 +3858,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(); @@ -3901,7 +3897,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(); } @@ -3967,15 +3963,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(); @@ -3985,11 +3981,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 eadee9496d..eff0050b7c 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 03b4866880..e0e40cd415 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 @@ -154,15 +154,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; } @@ -572,7 +567,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(); } @@ -1468,7 +1463,7 @@ public QueryTokenStream visitRegexpComparison(JpqlParser.RegexpComparisonContext @Override public QueryTokenStream visitComparison_operator(JpqlParser.Comparison_operatorContext ctx) { - return QueryRendererBuilder.from(QueryTokens.ventilated(ctx.op)); + return QueryTokenStream.from(QueryTokens.ventilated(ctx.op)); } @Override @@ -1892,16 +1887,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) { @@ -1947,11 +1945,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()); } } @@ -2209,7 +2207,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 { @@ -2221,11 +2219,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(); } @@ -2240,15 +2238,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) { @@ -2285,13 +2283,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(); } @@ -2306,7 +2304,7 @@ public QueryTokenStream visitEntity_type_literal(JpqlParser.Entity_type_literalC public QueryTokenStream visitEscape_character(JpqlParser.Escape_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 if (ctx.string_literal() != null) { @@ -2320,11 +2318,11 @@ public QueryTokenStream visitEscape_character(JpqlParser.Escape_characterContext 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(); } @@ -2334,9 +2332,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(); } @@ -2351,9 +2349,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(); } @@ -2442,7 +2440,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 { @@ -2453,9 +2451,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 6c9c5316645f5850153a5901518fc58d85570247 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 9 Dec 2024 14:34:01 +0100 Subject: [PATCH 018/224] 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 9f36e39a6611c193e0a9eb0631c79923d6afdd1a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Dec 2024 09:39:33 +0100 Subject: [PATCH 019/224] 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 a7e28aa94b4bda5e318d3319af99161a90d843d7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Dec 2024 14:07:53 +0100 Subject: [PATCH 020/224] 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 2e6572959a..411fb2b4d3 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 @@ -98,9 +98,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 57f547a06a..112affd341 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 @@ -102,7 +102,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) { @@ -560,7 +561,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 7b7264691ce2c4c484dd363130257cd2f08510b7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 10:00:26 +0100 Subject: [PATCH 021/224] 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 647b870276078b40cb3c8a85b28b2f42953c0d2b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 11:35:57 +0100 Subject: [PATCH 022/224] 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 8c5f169186f27cd294d4f6c8f606ef56adc2c496 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 Aug 2024 09:30:05 +0200 Subject: [PATCH 023/224] 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 | 163 ++++++--- .../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(+), 138 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 9755052ed0..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,87 +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) -> { - - Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); - }; + 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 */ @@ -135,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 */ @@ -156,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 eba6ed8851..d45dd113dd 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 42f8e168db..4406270e69 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; @@ -469,7 +470,7 @@ void testExecutionOfProjectingMethod() { void executesSpecificationCorrectly() { flushTestUsers(); - assertThat(repository.findAll(where(userHasFirstname("Oliver")))).hasSize(1); + assertThat(repository.findAll(Specification.where(userHasFirstname("Oliver")))).hasSize(1); } @Test @@ -499,11 +500,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); @@ -516,7 +517,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); } @@ -525,18 +527,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); @@ -591,7 +593,7 @@ void executesSimpleNotCorrectly() { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll((Specification) null)); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.all())); } @Test @@ -607,15 +609,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); } @@ -3395,8 +3423,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(); @@ -3421,7 +3449,7 @@ void deleteWithSpec() { flushTestUsers(); - Specification usersWithEInTheirName = userHasFirstnameLike("e"); + PredicateSpecification usersWithEInTheirName = userHasFirstnameLike("e"); long initialCount = repository.count(); assertThat(repository.delete(usersWithEInTheirName)).isEqualTo(3L); @@ -3568,16 +3596,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 87edcd1ab6889a5a7aa8181bedd178dae75c7430 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 19 Aug 2024 15:41:07 +0200 Subject: [PATCH 024/224] 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 08858fe133c645c619f0c4e20641af63a3c448b4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 20 Aug 2024 14:47:34 +0200 Subject: [PATCH 025/224] 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 d45dd113dd..83998fb193 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 4a13c4469763739c0b9048700f82650de220c8d0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 7 Nov 2024 15:56:30 +0100 Subject: [PATCH 026/224] 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 2e58485e06be896ba8434a63a2eb1b183d6653b1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 11:24:51 +0100 Subject: [PATCH 027/224] 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 4406270e69..9c693de0db 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 @@ -593,7 +593,7 @@ void executesSimpleNotCorrectly() { void returnsSameListIfNoSpecGiven() { flushTestUsers(); - assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.all())); + assertSameElements(repository.findAll(), repository.findAll(PredicateSpecification.unrestricted())); } @Test @@ -609,7 +609,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 @@ -633,7 +633,7 @@ void predicateSpecificationRemovesAll() { flushTestUsers(); - repository.delete(DeleteSpecification.all()); + repository.delete(DeleteSpecification.unrestricted()); assertThat(repository.count()).isEqualTo(0L); } @@ -643,7 +643,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 19571e438647fd7fc29696bf6bab95a62ba5f73c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Jan 2025 11:37:41 +0100 Subject: [PATCH 028/224] 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 b6a90c5599..579d890670 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 @@ -168,6 +168,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 2a423e5830..1656d37082 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 @@ -329,8 +329,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; } @@ -389,6 +389,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())); @@ -746,7 +747,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())); @@ -3377,7 +3378,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())); @@ -3412,7 +3413,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())); @@ -3801,9 +3802,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); @@ -3836,9 +3837,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 d2abc6176fd73d34035551e68d7e00b8988ee2bb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Jan 2025 11:37:56 +0100 Subject: [PATCH 029/224] 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 f78dc6a1f7..17188c06fb 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 @@ -347,6 +347,38 @@ select cast(i as string) from Item i where cast(i.date as date) <= cast(:current assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); } + @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() { @@ -458,18 +490,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 1f273c6391..ea44e5a326 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() { @@ -972,7 +1325,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 """); @@ -1121,85 +1474,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 " + // @@ -1211,336 +1587,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), " + // @@ -1548,100 +1924,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 " + // @@ -1984,7 +2359,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 7cc49745e7..3d9b1bf1b2 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 */ @@ -348,6 +608,38 @@ select cast(i as string) from Item i where cast(i.date as date) <= cast(:current assertQuery("SELECT e FROM Employee e WHERE CAST(e.salary NUMERIC(10, 2)) > 0.0"); } + @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 0d46e0536238c5a9dc06b2b95274ae485af7fead Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Jan 2025 14:35:49 +0100 Subject: [PATCH 030/224] 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 9c693de0db..f41cc7e9b5 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 @@ -803,9 +803,6 @@ void executesFinderWithFalseKeywordCorrectly() { assertThat(repository.findByActiveFalse()).containsOnly(firstUser); } - /** - * Ignored until the query declaration is supported by OpenJPA. - */ @Test void executesAnnotatedCollectionMethodCorrectly() { @@ -1618,11 +1615,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 d9a74429a3..1770da4564 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,13 +299,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 63e09c926bea03bb96c08ad2053de5b5cc7e792e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 13 Jan 2025 11:51:05 +0100 Subject: [PATCH 031/224] 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 d6eb587d1c..0a34310621 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 65d0c51d4d18bb8d430d4420ac38e0c01e5ad7a2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 15 Jan 2025 10:41:54 +0100 Subject: [PATCH 032/224] 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 e6c2008e8e08831420cf967433f7948b4a5e74c5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 22 Jan 2025 14:15:39 +0100 Subject: [PATCH 033/224] =?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 f670b10a0b8127ee9e78c7dd6a101310632a04c8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 22 Jan 2025 14:17:33 +0100 Subject: [PATCH 034/224] 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 e5df40b0e22fe69f24582fc609a933ef799ac8a7 Mon Sep 17 00:00:00 2001 From: Joshua Chen <27291761@qq.com> Date: Sat, 28 Dec 2024 15:02:46 +0800 Subject: [PATCH 035/224] =?UTF-8?q?Support=20custom=20countSpec=20in=20Sim?= =?UTF-8?q?pleJpaRepository.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 f41cc7e9b5..3973ec98cf 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 7a6cddf7207f94949e1be5e3adc94ccde6ca73da Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 23 Jan 2025 11:26:38 +0100 Subject: [PATCH 036/224] 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 3973ec98cf..0e2c25a86e 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 @@ -2800,6 +2800,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 01f3f413625f4dd007af909fbe39dfb01d81a05a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:47:59 +0100 Subject: [PATCH 037/224] Prepare 4.0 M1 (2025.1.0). See #3680 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0a34310621..b243fa26cc 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.2 9.2.0 42.7.5 - 4.0.0-SNAPSHOT + 4.0.0-M1 0.10.3 org.hibernate From 393161f86315305fe81bb3ecfd9c7f1ed6552885 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:48:55 +0100 Subject: [PATCH 038/224] 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 b243fa26cc..a9b11d0c37 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 dece8de6866a98cf263a24fd5ab755bf10b9dd74 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:09 +0100 Subject: [PATCH 039/224] 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 a9b11d0c37..b243fa26cc 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 1096088c41975374bd05ea7eaac08147a427a764 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:11 +0100 Subject: [PATCH 040/224] After release cleanups. See #3680 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index b243fa26cc..cf494b7072 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.2 9.2.0 42.7.5 - 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 b61c51d8c4a09c2db86af86e012f092dd828a171 Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Mon, 27 Jan 2025 15:54:33 +0100 Subject: [PATCH 041/224] 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 3cb7e909cbe77683c7d764e8f443e3668e224830 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 28 Jan 2025 10:06:52 +0100 Subject: [PATCH 042/224] 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 0e2c25a86e..c7891101fa 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; @@ -3227,6 +3228,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 63991208fc..044e5268c2 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 e90f2fea9154e2035d696a70a4136387b2315b7c Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Sun, 28 Apr 2024 09:36:37 +0800 Subject: [PATCH 043/224] 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 83998fb193..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 @@ -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 c40c90bc8454199d3677649110969a9fa1631e0e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 17 Feb 2025 11:22:46 +0100 Subject: [PATCH 044/224] 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 | 29 +++++-- .../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, 674 insertions(+), 504 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 411fb2b4d3..2df2fa6a21 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 @@ -24,6 +24,8 @@ import java.util.concurrent.ConcurrentHashMap; 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; @@ -33,7 +35,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; @@ -318,7 +319,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) { @@ -344,7 +345,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 579d890670..a5c034e5da 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 eff0050b7c..ba95930d2c 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 112affd341..6e9b672f95 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; @@ -53,9 +54,9 @@ import org.springframework.data.domain.Sort; import org.springframework.data.util.Predicates; -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; @@ -80,7 +81,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}. @@ -96,6 +97,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); } @@ -135,8 +138,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)) { @@ -317,7 +319,7 @@ public boolean hasConstructorExpression() { } @Override - public String detectAlias() { + public @Nullable String detectAlias() { return this.primaryAlias; } @@ -363,10 +365,10 @@ 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); @@ -570,7 +572,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) { @@ -580,4 +585,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 c7891101fa..c4f2485b82 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 1770da4564..f7fdb5f34a 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; @@ -51,7 +53,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 59e9f3459c36842ff98069c2d136fb11f891865c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 19 Feb 2025 08:56:11 +0100 Subject: [PATCH 045/224] 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 047fa2bc38c114ca698357bbe31ff8f64f4b84ed Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Feb 2025 13:23:29 +0100 Subject: [PATCH 046/224] 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 | 17 +-- .../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, 62 insertions(+), 221 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 6e9b672f95..141d61b5f1 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; @@ -52,6 +51,8 @@ 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; @@ -81,7 +82,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}. @@ -98,7 +99,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); } @@ -374,7 +375,7 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab return applySortingToSetOperationList(setOperationList, sort); } - doWithPlainSelect(selectStatement, it -> { + doWithPlainSelect (selectStatement , it -> { List orderByElements = new ArrayList<>(16); for (Sort.Order order : sort) { @@ -572,8 +573,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))) { @@ -585,9 +586,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 c4f2485b82..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 @@ -2801,44 +2801,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 878cd8cd4d5e46169c3a7cde4ed1fc5b8fbcbb6d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 27 Jun 2024 11:14:02 +0200 Subject: [PATCH 047/224] 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 | 57 +++--- .../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, 1063 insertions(+), 703 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 2df2fa6a21..e4216bdd78 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 @@ -56,7 +56,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { private final StringQuery query; private final Map, Boolean> knownProjections = new ConcurrentHashMap<>(); - private final Lazy countQuery; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -71,37 +71,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(); @@ -115,7 +110,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"); } @@ -217,7 +212,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); } @@ -243,14 +238,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(); } @@ -292,8 +287,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())); } @@ -301,7 +295,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); } /** @@ -311,9 +305,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)); } } @@ -321,7 +314,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"); @@ -329,7 +322,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)); } @@ -348,7 +341,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()) { @@ -373,21 +366,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 96411755fb..84d83aeb22 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 @@ -39,14 +39,14 @@ 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")); @@ -69,13 +69,13 @@ void shouldApplySortingWithNullsPrecedence() { @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(); @@ -98,7 +98,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"); @@ -121,7 +121,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"); @@ -148,7 +148,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"); @@ -168,7 +168,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(); @@ -189,7 +189,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"); @@ -212,7 +212,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"); @@ -232,7 +232,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(); @@ -250,7 +250,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 beb206724d..4fa844108b 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 @@ -912,12 +912,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 5da86f1bf02581520df7b6e5b66e56316113547d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 4 Mar 2025 13:41:10 +0100 Subject: [PATCH 048/224] 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 | 22 ++-- ...rIndexedQueryParameterSetterUnitTests.java | 8 +- .../query/QueryEnhancerFactoryUnitTests.java | 4 +- .../query/QueryEnhancerTckTests.java | 8 +- .../query/QueryEnhancerUnitTests.java | 6 +- .../query/StringQueryUnitTests.java | 9 +- 27 files changed, 305 insertions(+), 207 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 141d61b5f1..d1fab10326 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 @@ -74,7 +74,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; @@ -87,7 +87,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); @@ -339,7 +339,7 @@ public Set getSelectionAliases() { } @Override - public DeclaredQuery getQuery() { + public StructuredQuery getQuery() { return this.query; } @@ -410,8 +410,8 @@ public String createCountQueryFor(@Nullable String countProjection) { this.query::getQueryString); } - 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 84d83aeb22..6279919b36 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 @@ -46,7 +46,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")); @@ -56,7 +56,7 @@ void shouldApplySorting() { @Test // GH-3886 void shouldApplySortingWithNullsPrecedence() { - QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.of("SELECT e FROM Employee e", true)); + QueryEnhancer enhancer = createQueryEnhancer(DeclaredQuery.jpqlQuery("SELECT e FROM Employee e")); String sql = enhancer.rewrite(new DefaultQueryRewriteInformation( Sort.by(Sort.Order.asc("foo").with(Sort.NullHandling.NULLS_LAST), @@ -69,7 +69,7 @@ void shouldApplySortingWithNullsPrecedence() { @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 @@ -98,7 +98,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"); @@ -121,7 +121,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"); @@ -148,7 +148,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"); @@ -168,7 +168,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(); @@ -189,7 +189,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"); @@ -212,7 +212,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"); @@ -232,7 +232,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(); @@ -250,7 +250,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 4fa844108b..155b291a5d 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; /** @@ -926,11 +926,10 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label) { - 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 e2446caf38541ff25e6e5a31ea01c0a11d2db19b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 Mar 2025 15:19:59 +0100 Subject: [PATCH 049/224] 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 | 95 ++-- .../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 | 128 ++--- .../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, 1469 insertions(+), 1177 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 e4216bdd78..148567a9ea 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 @@ -23,9 +23,9 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -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; @@ -54,9 +54,9 @@ */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { - private final StringQuery query; + private final EntityQuery query; private final Map, Boolean> knownProjections = new ConcurrentHashMap<>(); - private final Lazy countQuery; + private final Lazy countQuery; private final ValueExpressionDelegate valueExpressionDelegate; private final QueryRewriter queryRewriter; private final QuerySortRewriter querySortRewriter; @@ -70,25 +70,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()); @@ -114,14 +131,18 @@ 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 = getReturnedType(processor); - 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. @@ -212,7 +233,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); } @@ -245,7 +266,7 @@ public EntityQuery getQuery() { /** * @return the countQuery */ - public IntrospectedQuery getCountQuery() { + public ParametrizedQuery getCountQuery() { return countQuery.get(); } @@ -253,11 +274,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); @@ -286,8 +307,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())); } @@ -295,7 +316,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); } /** @@ -305,28 +326,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; } } @@ -335,22 +356,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)); @@ -366,12 +387,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(); @@ -379,7 +400,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 d1fab10326..1733df96e7 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 @@ -74,7 +74,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; @@ -87,7 +87,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); @@ -329,35 +329,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"); @@ -410,8 +395,8 @@ public String createCountQueryFor(@Nullable String countProjection) { this.query::getQueryString); } - 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 155b291a5d..3077ded6bc 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 @@ -44,13 +43,13 @@ * @author Aleksei Elin * @author Gunha Hwang */ -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); @@ -67,8 +66,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()) @@ -91,7 +90,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(); @@ -117,7 +116,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"); @@ -134,7 +134,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); @@ -165,7 +165,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); @@ -198,7 +198,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); @@ -225,7 +225,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(); @@ -239,7 +239,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); @@ -247,7 +247,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()) @@ -257,7 +258,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); @@ -265,7 +266,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"); @@ -274,7 +275,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); @@ -296,8 +297,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) @@ -310,8 +311,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) @@ -323,7 +324,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); @@ -338,7 +339,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); @@ -355,7 +356,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)"); @@ -376,7 +377,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)"); @@ -397,7 +398,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); @@ -411,7 +412,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(); @@ -424,7 +425,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); @@ -443,7 +444,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"); } @@ -452,7 +453,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"); } @@ -460,17 +461,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); @@ -485,17 +486,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); @@ -511,7 +512,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); @@ -527,13 +528,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); @@ -544,8 +545,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(); @@ -560,7 +563,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(); @@ -571,7 +575,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(); @@ -582,7 +586,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(); @@ -593,7 +597,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(); @@ -604,7 +608,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(); @@ -615,7 +619,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(); @@ -626,7 +630,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(); @@ -637,8 +641,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"); @@ -654,12 +658,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(); } /** @@ -670,14 +673,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(); @@ -704,7 +709,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); } @@ -728,7 +733,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); } @@ -781,7 +786,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(); @@ -801,16 +806,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( // @@ -821,7 +828,7 @@ void makesUsageOfJdbcStyleParameterAvailable() { for (String testQuery : testQueries) { - assertThat(new StringQuery(testQuery, false) // + assertThat(new TestEntityQuery(testQuery, false) // .usesJdbcStyleParameters()) // .describedAs(testQuery) // .describedAs(testQuery) // @@ -833,7 +840,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(); @@ -853,7 +860,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(); } @@ -870,7 +877,7 @@ void isNotDefaultProjection() { ); for (String queryString : queriesWithDefaultProjection) { - assertThat(new StringQuery(queryString, true).isDefaultProjection()) // + assertThat(new TestEntityQuery(queryString, true).isDefaultProjection()) // .describedAs(queryString) // .isTrue(); } @@ -880,7 +887,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(); @@ -892,7 +899,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) // @@ -903,7 +910,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) // @@ -912,9 +919,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) // @@ -927,7 +933,8 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, private void checkHasNamedParameter(String query, boolean expected, String label) { 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 6279919b36..bc2e0236b7 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 @@ -48,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"); } @@ -77,7 +78,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"); } @@ -97,16 +98,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(); @@ -120,16 +121,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(); @@ -147,16 +148,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(); @@ -167,16 +168,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(); @@ -188,18 +188,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().toLowerCase()).isEqualToIgnoringWhitespace( + assertThat(queryEnhancer.createCountQueryFor(null).toLowerCase()).isEqualToIgnoringWhitespace( "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(); @@ -211,18 +211,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().toLowerCase()).isEqualToIgnoringWhitespace( + assertThat(queryEnhancer.createCountQueryFor(null).toLowerCase()).isEqualToIgnoringWhitespace( "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(); @@ -231,15 +231,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(); @@ -247,15 +247,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(); @@ -285,4 +284,9 @@ void shouldWorkWithParenthesedSelect() { assertThat(queryEnhancer.getProjection()).isEqualTo("is_contained_in(:innerId, :outerId)"); } + 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 044e5268c2..52b3e8a2c5 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 1529ec252a6697a19418af41ca3a7a7145f1ee7c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Mar 2025 10:06:27 +0100 Subject: [PATCH 050/224] 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 148567a9ea..61d5ea7f30 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 @@ -112,7 +112,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 52b3e8a2c5..eaa05b0b3b 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 be5fcb30ce807599f028886316715eb5ae1ff690 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 09:45:03 +0200 Subject: [PATCH 051/224] 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 cf494b7072..681c3e2c90 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 b58c423bdf8f9beee5a2efcb8ad5ee1319dc390a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 09:47:15 +0200 Subject: [PATCH 052/224] 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 681c3e2c90..dd759f8499 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 831d04dd2cee22c51459615e08552131dcc9beaa Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 23 Sep 2024 08:15:32 +0200 Subject: [PATCH 053/224] 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 dd759f8499..b63358380a 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 3077ded6bc..a88c2912fc 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 c399ca2b54bcf2ff10a777ca81ea6ed53ac40050 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 24 Mar 2025 15:52:23 +0100 Subject: [PATCH 054/224] 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 802a8db1d1472e208dff75c21dfd8dff884918a8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 08:56:51 +0100 Subject: [PATCH 055/224] 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 daae010e21979bdeaabc57f22af12f1c95c5788d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 09:13:38 +0100 Subject: [PATCH 056/224] 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 b419041cb65c269a631da41114e90cf1f4cabffc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 10:12:06 +0100 Subject: [PATCH 057/224] Add support for Value Expressions, Stream, Named and Modifying Queries. See #3830 --- .../aot/generated/AotMetaModel.java | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java 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 deleted file mode 100644 index 797e7a45a4..0000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java +++ /dev/null @@ -1,124 +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.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 - */ -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 ignore 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(); - } -} From 22dfb1c22ba6a6c480a4775799ee4a1402d54f1b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Mar 2025 10:12:06 +0100 Subject: [PATCH 058/224] 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 +- .../aot/generated/AotMetamodel.java | 131 ++++++++ .../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, 1022 insertions(+), 187 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/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 new file mode 100644 index 0000000000..fcdd221cf9 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetamodel.java @@ -0,0 +1,131 @@ +/* + * 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 + * @since 4.0 + */ +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(); + } + + // TODO: Capture an existing factory bean (e.g. EntityManagerFactoryInfo) to extract PersistenceInfo + public EntityManagerFactory getEntityManagerFactory() { + return entityManagerFactory.get(); + } + + EntityManagerFactory init() { + + MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { + @Override + public ClassLoader getNewTempClassLoader() { + return new SimpleThrowawayClassLoader(this.getClass().getClassLoader()); + } + + @Override + public void addTransformer(ClassTransformer classTransformer) { + // just ignore 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/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 f7fdb5f34a..988e743cf0 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; @@ -300,6 +300,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 4b0a83a97b789d09e3dcc495b515cd2d7c25a290 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Mar 2025 12:02:07 +0100 Subject: [PATCH 059/224] 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 988e743cf0..1833a10355 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 @@ -300,10 +300,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 e38b219898eb943ef42bef584f1dab6feaca7b98 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 2 Apr 2025 14:11:38 +0200 Subject: [PATCH 060/224] 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 f2b0dca62b12369d6949362d609a0fd6d0a1c4ac Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 11:43:42 +0200 Subject: [PATCH 061/224] 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 9fe7d423ef0c7b685863da09ddbd280b1de087b4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Mar 2025 16:57:39 +0100 Subject: [PATCH 062/224] 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 d3cc7da3d9bf27a6c99bfcc93b625736ff9e6f81 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 16:06:10 +0200 Subject: [PATCH 063/224] 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 be4d8528b3ef63191142c2cc51780b056e025da3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Apr 2025 13:00:19 +0200 Subject: [PATCH 064/224] 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 b63358380a..3a62411170 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 eb1266888fcf74cdcd9005bedf4c5de31cf6e867 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 7 Apr 2025 12:21:14 +0200 Subject: [PATCH 065/224] 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 fbd214f72efa062518baa54313c70af67c9afa79 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 9 Apr 2025 16:51:30 +0200 Subject: [PATCH 066/224] 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 a88c2912fc..3077ded6bc 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 0e7aef080f36dbb575c1f6a1912325d6a05b07d3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 8 Apr 2025 15:43:10 +0200 Subject: [PATCH 067/224] 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 1781819e59aeabd7325d5707d59b06cba78c0a9c Mon Sep 17 00:00:00 2001 From: hgh1472 Date: Sat, 5 Apr 2025 00:10:44 +0900 Subject: [PATCH 068/224] Remove unnecessary parameter. Remove unnecessary boolean nativeQuery from checkHasNamedParameter of StringQueryUnitTests class. Signed-off-by: hgh1472 Closes #3827 Original pull request: #3828 --- .../jpa/repository/query/DefaultEntityQueryUnitTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 3077ded6bc..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 @@ -932,8 +932,8 @@ void checkNumberOfNamedParameters(String query, int expectedSize, String label, 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 fe3c22dad0630a793ea60ed9da192961919bc3d8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 10:37:41 +0200 Subject: [PATCH 069/224] 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 8ac384c96e1d92ef44f304c160640e7d9cdb1e7d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:29:21 +0200 Subject: [PATCH 070/224] 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 3a62411170..3feb0f6395 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.2 9.2.0 42.7.5 - 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 68fb398b4b753128a429c0fc49d8a4ebb30c4849 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:29:38 +0200 Subject: [PATCH 071/224] 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 3feb0f6395..8b1b9ccdf1 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 c57ee667bf2ed89a1e3024878cc5543c328311de Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:32:03 +0200 Subject: [PATCH 072/224] 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 8b1b9ccdf1..3feb0f6395 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 ddeac0cacdab66628aba6ffd6846270355068255 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:32:04 +0200 Subject: [PATCH 073/224] After release cleanups. See #3751 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 3feb0f6395..3a62411170 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.2 9.2.0 42.7.5 - 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 403e1b4d014d71dec0979ccf1324ea301e7277d5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Apr 2025 16:12:27 +0200 Subject: [PATCH 074/224] 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 | 8 +++ ...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, 149 insertions(+), 49 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 7eb4e4cc6c..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 @@ -523,6 +523,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 1833a10355..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 @@ -768,6 +768,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 5ebfdb656644983ffab30457484e8fb61ac807fd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Apr 2025 17:46:27 +0200 Subject: [PATCH 075/224] 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 3a62411170..8c09d913d5 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 dc854df7412d3a0dbe71bc05c440bcb152039e46 Mon Sep 17 00:00:00 2001 From: SWQXDBA <983110853@qq.com> Date: Wed, 23 Apr 2025 20:03:17 +0800 Subject: [PATCH 076/224] 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> --- .../java/org/springframework/data/jpa/domain/Specification.java | 2 +- 1 file changed, 1 insertion(+), 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(); }; } From 41bfd415caad3aa340b49e8e7ba45fe5d8bd45af Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 14:13:17 +0200 Subject: [PATCH 077/224] 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/UpdateSpecificationUnitTests.java | 11 +++++++++++ 6 files changed, 42 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 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/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 2435807db4c00179a847bf2f98e4ee81697ab14d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 17:15:27 +0200 Subject: [PATCH 078/224] 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 | 37 +++++++++++-------- .../repository/query/DefaultEntityQuery.java | 6 +++ .../query/EmptyIntrospectedQuery.java | 13 +++++++ .../jpa/repository/query/EntityQuery.java | 16 ++++++++ .../repository/query/ParametrizedQuery.java | 2 +- 5 files changed, 57 insertions(+), 17 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 61d5ea7f30..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 @@ -161,7 +161,7 @@ private ReturnedType getReturnedType(ResultProcessor processor) { Class returnedJavaType = processor.getReturnedType().getReturnedType(); if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface() - || query.isNativeQuery()) { + || query.isNative()) { return returnedType; } @@ -178,23 +178,23 @@ private ReturnedType getReturnedType(ResultProcessor processor) { return new NonProjectingReturnedType(returnedType); } - String alias = query.getAlias(); - String projection = query.getProjection(); + String projectionToUse = query.<@Nullable String> doWithEnhancer(queryEnhancer -> { - // we can handle single-column and no function projections here only - if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) { - return returnedType; - } + String alias = queryEnhancer.detectAlias(); + String projection = queryEnhancer.getProjection(); - if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) { - alias = alias.trim(); - projection = projection.trim(); - if (projection.startsWith(alias + ".")) { - projection = projection.substring(alias.length() + 1); + // 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(projection)) { + 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(' '); @@ -202,10 +202,15 @@ private ReturnedType getReturnedType(ResultProcessor processor) { projection = projection.substring(0, space); } + return projection; + }); + + if (StringUtils.hasText(projectionToUse)) { + Class propertyType; try { - PropertyPath from = PropertyPath.from(projection, getQueryMethod().getEntityInformation().getJavaType()); + PropertyPath from = PropertyPath.from(projectionToUse, getQueryMethod().getEntityInformation().getJavaType()); propertyType = from.getLeafType(); } catch (PropertyReferenceException ignored) { propertyType = null; @@ -223,7 +228,7 @@ private ReturnedType getReturnedType(ResultProcessor processor) { return returnedType; } - String getSortedQueryString(Sort sort, ReturnedType returnedType) { + QueryProvider getSortedQuery(Sort sort, ReturnedType returnedType) { return querySortRewriter.getSorted(query, sort, returnedType); } 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. From b3c0210f17ae87e7e13efcc857e4d07335fb432d Mon Sep 17 00:00:00 2001 From: Diego Pedregal Date: Mon, 5 May 2025 11:13:59 +0200 Subject: [PATCH 079/224] Removes `PlainSelect` casting in `JSqlParserQueryEnhancer`. Closes: #3869 Original pull request: #3870 Signed-off-by: Diego Pedregal --- .../query/JSqlParserQueryEnhancerUnitTests.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 bc2e0236b7..07f04372b7 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 @@ -289,4 +289,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 7f20f10b305a5ceee26f68693f78ac576d5483d5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 May 2025 15:45:08 +0200 Subject: [PATCH 080/224] 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 | 2 +- .../JSqlParserQueryEnhancerUnitTests.java | 33 +++++++------------ 2 files changed, 12 insertions(+), 23 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 1733df96e7..4b17555c55 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 @@ -360,7 +360,7 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab return applySortingToSetOperationList(setOperationList, sort); } - doWithPlainSelect (selectStatement , it -> { + doWithPlainSelect(selectStatement, it -> { List orderByElements = new ArrayList<>(16); for (Sort.Order order : sort) { 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 07f04372b7..e756ea0273 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 @@ -245,6 +245,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) { @@ -271,31 +282,9 @@ static Stream mergeStatementWorksSource() { null)); } - @Test // GH-3869 - void shouldWorkWithParenthesedSelect() { - - String query = "(SELECT is_contained_in(:innerId, :outerId))"; - - StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getQueryString()).isEqualTo(query); - assertThat(stringQuery.getAlias()).isNull(); - assertThat(queryEnhancer.getProjection()).isEqualTo("is_contained_in(:innerId, :outerId)"); - } - private static DefaultQueryRewriteInformation getRewriteInformation(Sort sort) { return new DefaultQueryRewriteInformation(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 eb4ad0923d175947522f7ed5c093438dd1ee3877 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 8 May 2025 12:38:58 +0200 Subject: [PATCH 081/224] 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 8c09d913d5..9076272ffc 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 65214fb0b185a5a5391aec5fd98229b5a297bc84 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 7 May 2025 17:11:38 +0200 Subject: [PATCH 082/224] 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 e1cec313f3d0aea286bc3ef6afab085fd8102759 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 May 2025 16:27:07 +0200 Subject: [PATCH 083/224] 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 01e4e756178dd21dfc25d20df065fd7d2a44bdf5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 May 2025 16:45:09 +0200 Subject: [PATCH 084/224] 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 219e5c58e4cd8aae27389af5964a51d7ea33cf76 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 12:14:17 +0200 Subject: [PATCH 085/224] 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 b66c96d363f0804c8142abada4d55e4bed3742c6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 31 Jan 2025 09:58:20 +0100 Subject: [PATCH 086/224] 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 e6a008cf08915185bffb74a911d75a10456dfc76 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 15:51:17 +0200 Subject: [PATCH 087/224] =?UTF-8?q?Fix=20JPQL=20and=20EQL=20`CAST(?= =?UTF-8?q?=E2=80=A6)`=20function=20parsing.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now define arithmetic, string, and typed cast functions to support all potential variants of casting supported by JPQL and EQL. Closes #3863 --- .../data/jpa/repository/query/Eql.g4 | 4 ---- .../data/jpa/repository/query/Jpql.g4 | 5 ----- .../repository/query/EqlQueryRenderer.java | 16 +++++++-------- .../repository/query/JpqlQueryRenderer.java | 20 ++++++++----------- .../EqlParserQueryEnhancerUnitTests.java | 4 ++-- .../HqlParserQueryEnhancerUnitTests.java | 4 ++-- .../JpqlParserQueryEnhancerUnitTests.java | 4 ++-- 7 files changed, 21 insertions(+), 36 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 24ac884f69..86b0111ca1 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 @@ -314,7 +314,6 @@ scalar_expression | datetime_expression | boolean_expression | case_expression - | cast_function | entity_type_expression ; @@ -954,7 +953,6 @@ FETCH : F E T C H; FIRST : F I R S T; FLOAT : F L O A T; FLOOR : F L O O R; -FLOAT : F L O A T; FROM : F R O M; FUNCTION : F U N C T I O N; GROUP : G R O U P; @@ -965,7 +963,6 @@ 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; -INTEGER : I N T E G E R; JOIN : J O I N; KEY : K E Y; LAST : L A S T; @@ -1006,7 +1003,6 @@ 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; -STRING : S T R I N G; SUM : S U M; THEN : T H E N; TIME : T I M E; 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 90e590cd11..b3afdb9b1e 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 @@ -307,7 +307,6 @@ scalar_expression | datetime_expression | boolean_expression | case_expression - | cast_function | entity_type_expression ; @@ -454,7 +453,6 @@ string_expression | aggregate_expression | case_expression | function_invocation - | string_expression op='||' string_expression | string_cast_function | type_cast_function | '(' subquery ')' @@ -950,7 +948,6 @@ FETCH : F E T C H; FIRST : F I R S T; FLOAT : F L O A T; FLOOR : F L O O R; -FLOAT : F L O A T; FROM : F R O M; FUNCTION : F U N C T I O N; GROUP : G R O U P; @@ -961,7 +958,6 @@ 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; -INTEGER : I N T E G E R; JOIN : J O I N; KEY : K E Y; LAST : L A S T; @@ -1002,7 +998,6 @@ 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; -STRING : S T R I N G; SUM : S U M; THEN : T H E N; TIME : T I M E; 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 8e4878fa89..51767d9c90 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 @@ -1010,8 +1010,6 @@ public QueryTokenStream visitScalar_expression(EqlParser.Scalar_expressionContex return visit(ctx.case_expression()); } else if (ctx.entity_type_expression() != null) { return visit(ctx.entity_type_expression()); - } else if (ctx.cast_function() != null) { - return (visit(ctx.cast_function())); } return QueryTokenStream.empty(); @@ -1363,8 +1361,8 @@ public QueryTokenStream visitStringComparison(EqlParser.StringComparisonContext QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.string_expression(0))); - builder.appendExpression(visit(ctx.comparison_operator())); + builder.appendInline(visit(ctx.string_expression(0))); + builder.appendInline(visit(ctx.comparison_operator())); if (ctx.string_expression(1) != null) { builder.appendExpression(visit(ctx.string_expression(1))); @@ -1420,7 +1418,7 @@ public QueryTokenStream visitDatetimeComparison(EqlParser.DatetimeComparisonCont QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendInline(visit(ctx.datetime_expression(0))); - builder.append(QueryTokens.ventilated(ctx.comparison_operator().op)); + builder.appendInline(visit(ctx.comparison_operator())); if (ctx.datetime_expression(1) != null) { builder.appendExpression(visit(ctx.datetime_expression(1))); @@ -1453,8 +1451,8 @@ public QueryTokenStream visitArithmeticComparison(EqlParser.ArithmeticComparison QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.arithmetic_expression(0))); - builder.appendExpression(visit(ctx.comparison_operator())); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.appendInline(visit(ctx.comparison_operator())); if (ctx.arithmetic_expression(1) != null) { builder.appendExpression(visit(ctx.arithmetic_expression(1))); @@ -1491,7 +1489,7 @@ public QueryTokenStream visitRegexpComparison(EqlParser.RegexpComparisonContext @Override public QueryTokenStream visitComparison_operator(EqlParser.Comparison_operatorContext ctx) { - return QueryTokenStream.ofToken(ctx.op); + return QueryTokenStream.from(QueryTokens.ventilated(ctx.op)); } @Override @@ -2005,7 +2003,7 @@ public QueryTokenStream visitType_cast_function(EqlParser.Type_cast_functionCont if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendInline(QueryTokenStream.concat(ctx.identification_variable(), this::visit, TOKEN_SPACE)); + builder.appendInline(visit(ctx.identification_variable())); if (!CollectionUtils.isEmpty(ctx.numeric_literal())) { 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 e0e40cd415..03b87cdd34 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 @@ -985,8 +985,6 @@ 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_function() != null) { - return (visit(ctx.cast_function())); } return QueryTokenStream.empty(); @@ -1336,7 +1334,7 @@ public QueryTokenStream visitStringComparison(JpqlParser.StringComparisonContext QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendInline(visit(ctx.string_expression(0))); - builder.append(visit(ctx.comparison_operator())); + builder.appendInline(visit(ctx.comparison_operator())); if (ctx.string_expression(1) != null) { builder.append(visit(ctx.string_expression(1))); @@ -1392,7 +1390,7 @@ public QueryTokenStream visitDatetimeComparison(JpqlParser.DatetimeComparisonCon QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendInline(visit(ctx.datetime_expression(0))); - builder.append(QueryTokens.ventilated(ctx.comparison_operator().op)); + builder.appendInline(visit(ctx.comparison_operator())); if (ctx.datetime_expression(1) != null) { builder.append(visit(ctx.datetime_expression(1))); @@ -1425,8 +1423,8 @@ public QueryTokenStream visitArithmeticComparison(JpqlParser.ArithmeticCompariso QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.arithmetic_expression(0))); - builder.append(visit(ctx.comparison_operator())); + builder.appendInline(visit(ctx.arithmetic_expression(0))); + builder.appendInline(visit(ctx.comparison_operator())); if (ctx.arithmetic_expression(1) != null) { builder.append(visit(ctx.arithmetic_expression(1))); @@ -1469,19 +1467,17 @@ public QueryTokenStream visitComparison_operator(JpqlParser.Comparison_operatorC @Override public QueryTokenStream visitArithmetic_expression(JpqlParser.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 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 dbe4d45a9f..635c858e0f 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 @@ -25,12 +25,12 @@ * * @author Greg Turnquist */ -public class EqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { +class EqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { - assumeThat(query.isNative()).isFalse(); + assumeThat(query.isNative()).describedAs("EQL (non-native) only").isFalse(); return JpaQueryEnhancer.forEql(query.getQueryString()); } 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 f25e9fc2ee..3f32615e2b 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 @@ -25,12 +25,12 @@ * * @author Greg Turnquist */ -public class HqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { +class HqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { - assumeThat(query.isNative()).isFalse(); + assumeThat(query.isNative()).describedAs("HQL (non-native) only").isFalse(); return JpaQueryEnhancer.forHql(query.getQueryString()); } 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 44256fe4c9..2d86aa5761 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 @@ -25,12 +25,12 @@ * * @author Greg Turnquist */ -public class JpqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { +class JpqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @Override QueryEnhancer createQueryEnhancer(DeclaredQuery query) { - assumeThat(query.isNative()).isFalse(); + assumeThat(query.isNative()).describedAs("JPQL (non-native) only").isFalse(); return JpaQueryEnhancer.forJpql(query.getQueryString()); } From 17a59905a732780c07befaf7bbda5f1830c2e1dc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Apr 2025 14:44:56 +0200 Subject: [PATCH 088/224] Explore returning Search Results. Closes: #3868 --- pom.xml | 22 ++ spring-data-jpa/pom.xml | 36 ++ .../jpa/repository/aot/QueriesFactory.java | 3 +- .../repository/query/AbstractJpaQuery.java | 18 +- .../query/JpaCountQueryCreator.java | 2 +- .../query/JpaKeysetScrollQueryCreator.java | 4 +- .../query/JpaParametersParameterAccessor.java | 58 +++ .../jpa/repository/query/JpaQueryCreator.java | 244 +++++++++++-- .../repository/query/JpaQueryExecution.java | 82 +++++ .../repository/query/JpqlQueryBuilder.java | 136 +++++-- .../repository/query/ParameterBinding.java | 75 +++- .../query/ParameterMetadataProvider.java | 296 ++++++++------- .../repository/query/PartTreeJpaQuery.java | 17 +- .../query/QueryParameterSetterFactory.java | 4 + .../query/SimilarityNormalizer.java | 125 +++++++ .../AbstractVectorIntegrationTests.java | 342 ++++++++++++++++++ .../OracleVectorIntegrationTests.java | 95 +++++ .../repository/PgVectorIntegrationTests.java | 92 +++++ .../MySqlStoredProcedureIntegrationTests.java | 27 +- ...stgresStoredProcedureIntegrationTests.java | 27 +- ...ProcedureNullHandlingIntegrationTests.java | 28 +- .../query/AbstractJpaQueryTests.java | 2 +- .../query/JpaQueryCreatorTests.java | 21 +- .../query/JpqlQueryBuilderUnitTests.java | 10 + ...meterMetadataProviderIntegrationTests.java | 59 +++ .../ParameterMetadataProviderUnitTests.java | 21 -- .../query/SimilarityNormalizerUnitTests.java | 76 ++++ .../TestcontainerConfigSupport.java} | 36 +- .../scripts/oracle-vector-initialize.sql | 11 + .../test/resources/scripts/oracle-vector.sql | 16 + .../src/test/resources/scripts/pgvector.sql | 7 + src/main/antora/modules/ROOT/nav.adoc | 1 + .../pages/repositories/vector-search.adoc | 8 + .../partials/vector-search-intro-include.adoc | 32 ++ ...ector-search-method-annotated-include.adoc | 28 ++ .../vector-search-method-derived-include.adoc | 16 + .../partials/vector-search-model-include.adoc | 18 + .../vector-search-repository-include.adoc | 21 ++ .../vector-search-scoring-include.adoc | 38 ++ 39 files changed, 1931 insertions(+), 223 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimilarityNormalizer.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OracleVectorIntegrationTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/PgVectorIntegrationTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimilarityNormalizerUnitTests.java rename spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/{procedures/StoredProcedureConfigSupport.java => support/TestcontainerConfigSupport.java} (74%) create mode 100644 spring-data-jpa/src/test/resources/scripts/oracle-vector-initialize.sql create mode 100644 spring-data-jpa/src/test/resources/scripts/oracle-vector.sql create mode 100644 spring-data-jpa/src/test/resources/scripts/pgvector.sql create mode 100644 src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc diff --git a/pom.xml b/pom.xml index 9076272ffc..9bc676e471 100755 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ 5.2 9.2.0 42.7.5 + 23.7.0.25.01 4.0.0-SNAPSHOT 0.10.3 @@ -56,6 +57,14 @@ jmh + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + 0.5.0.RELEASE + test + + jitpack @@ -112,6 +121,19 @@ + + oracle-test + test + + test + + + + **/Oracle*IntegrationTests.java + + + + diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 1cc6674063..cdb738558f 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -88,6 +88,12 @@ true + + org.springframework + spring-test + test + + org.junit.platform junit-platform-launcher @@ -161,6 +167,28 @@ test + + + + com.oracle.database.jdbc + ojdbc17 + ${oracle} + test + + + + com.oracle.database.jdbc + ucp17 + ${oracle} + test + + + + org.testcontainers + oracle-free + test + + io.vavr vavr @@ -183,6 +211,13 @@ + + ${hibernate.groupId}.orm + hibernate-vector + ${hibernate} + true + + ${hibernate.groupId}.orm hibernate-jpamodelgen @@ -318,6 +353,7 @@ **/EclipseLink* **/MySql* **/Postgres* + **/Oracle* -Xmx4G 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 05c49f1144..ee26bf0d06 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 @@ -224,7 +224,8 @@ private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaPa ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, templates); - JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metamodel); + JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, false, returnedType, metadataProvider, templates, + metamodel); return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection()); 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 ef604e1f5b..ad0cafba95 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 @@ -101,7 +101,7 @@ public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) { return new StreamExecution(); } else if (method.isProcedureQuery()) { return new ProcedureExecution(method.isCollectionQuery()); - } else if (method.isCollectionQuery()) { + } else if (method.isCollectionQuery() || method.isSearchQuery()) { return new CollectionExecution(); } else if (method.isSliceQuery()) { return new SlicedExecution(); @@ -149,7 +149,9 @@ protected JpaMetamodel getMetamodel() { @Override public @Nullable Object execute(Object[] parameters) { - return doExecute(getExecution(), parameters); + + JpaParametersParameterAccessor accessor = obtainParameterAccessor(parameters); + return doExecute(getExecution(accessor), accessor); } /** @@ -157,9 +159,8 @@ protected JpaMetamodel getMetamodel() { * @param values * @return */ - private @Nullable Object doExecute(JpaQueryExecution execution, Object[] values) { + private @Nullable Object doExecute(JpaQueryExecution execution, JpaParametersParameterAccessor accessor) { - JpaParametersParameterAccessor accessor = obtainParameterAccessor(values); Object result = execution.execute(this, accessor); ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor); @@ -176,10 +177,17 @@ private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) return new JpaParametersParameterAccessor(method.getParameters(), values); } - protected JpaQueryExecution getExecution() { + protected JpaQueryExecution getExecution(JpaParametersParameterAccessor accessor) { JpaQueryExecution execution = this.execution.getNullable(); + if (method.isSearchQuery()) { + + ReturnedType returnedType = method.getResultProcessor().withDynamicProjection(accessor).getReturnedType(); + return new JpaQueryExecution.SearchResultExecution(execution == null ? new SingleEntityExecution() : execution, + returnedType, accessor.getScoringFunction(), accessor.normalizeSimilarity()); + } + if (execution != null) { return execution; } 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 c0f5c49d73..b95e272b1c 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 @@ -48,7 +48,7 @@ public class JpaCountQueryCreator extends JpaQueryCreator { public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider, JpqlQueryTemplates templates, EntityManager em) { - super(tree, returnedType, provider, templates, em); + super(tree, returnedType, provider, templates, em.getMetamodel()); this.distinct = tree.isDistinct(); this.returnedType = returnedType; 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 776657b2af..e7252b510a 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 @@ -23,6 +23,8 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; @@ -49,7 +51,7 @@ public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, ParameterMe JpqlQueryTemplates templates, JpaEntityInformation entityInformation, KeysetScrollPosition scrollPosition, EntityManager em) { - super(tree, type, provider, templates, em); + super(tree, type, provider, templates, em.getMetamodel()); this.entityInformation = entityInformation; this.scrollPosition = scrollPosition; 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 9d22c7bbb4..e77ab25c6e 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,8 +15,16 @@ */ package org.springframework.data.jpa.repository.query; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.Similarity; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -68,4 +76,54 @@ protected Object potentiallyUnwrap(Object parameterValue) { return parameterValue; } + /** + * Returns the {@link ScoringFunction}. + * + * @return + */ + public ScoringFunction getScoringFunction() { + return doWithScore(Score::getFunction, Score.class::isInstance, ScoringFunction::unspecified); + } + + /** + * Returns whether to normalize similarities (i.e. translate the database-specific score into {@link Similarity}). + * + * @return + */ + public boolean normalizeSimilarity() { + return doWithScore(it -> true, Similarity.class::isInstance, () -> false); + } + + /** + * Returns the {@link ScoringFunction}. + * + * @return + */ + public T doWithScore(Function function, Predicate scoreFilter, Supplier defaultValue) { + + Score score = getScore(); + if (score != null && scoreFilter.test(score)) { + return function.apply(score); + } + + JpaParameters parameters = getParameters(); + if (parameters.hasScoreRangeParameter()) { + + Range range = getScoreRange(); + + if (range != null && range.getLowerBound().isBounded() + && scoreFilter.test(range.getLowerBound().getValue().get())) { + return function.apply(range.getUpperBound().getValue().get()); + } + + if (range != null && range.getUpperBound().isBounded() + && scoreFilter.test(range.getUpperBound().getValue().get())) { + return function.apply(range.getUpperBound().getValue().get()); + } + + } + + return defaultValue.get(); + } + } 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 c49baf6ff9..f6cda83389 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 @@ -28,14 +28,21 @@ import jakarta.persistence.metamodel.SingularAttribute; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.VectorScoringFunctions; 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; @@ -63,8 +70,21 @@ * @author Christoph Strobl * @author Jinmyeong Kim */ -public class JpaQueryCreator extends AbstractQueryCreator implements JpqlQueryCreator { +public class JpaQueryCreator extends AbstractQueryCreator + implements JpqlQueryCreator { + private static final Map DISTANCE_FUNCTIONS = Map.of(VectorScoringFunctions.COSINE, + new DistanceFunction("cosine_distance", Sort.Direction.ASC), // + VectorScoringFunctions.EUCLIDEAN, new DistanceFunction("euclidean_distance", Sort.Direction.ASC), // + VectorScoringFunctions.TAXICAB, new DistanceFunction("taxicab_distance", Sort.Direction.ASC), // + VectorScoringFunctions.HAMMING, new DistanceFunction("hamming_distance", Sort.Direction.ASC), // + VectorScoringFunctions.DOT_PRODUCT, new DistanceFunction("negative_inner_product", Sort.Direction.ASC)); + + record DistanceFunction(String distanceFunction, Sort.Direction direction) { + + } + + private final boolean searchQuery; private final ReturnedType returnedType; private final ParameterMetadataProvider provider; private final JpqlQueryTemplates templates; @@ -73,6 +93,7 @@ public class JpaQueryCreator extends AbstractQueryCreator entityType; private final JpqlQueryBuilder.Entity entity; private final Metamodel metamodel; + private final SimilarityNormalizer similarityNormalizer; private final boolean useNamedParameters; /** @@ -80,20 +101,26 @@ public class JpaQueryCreator extends AbstractQueryCreator getFrom() { @@ -198,28 +226,41 @@ protected JpqlQueryBuilder.Select buildQuery(Sort sort) { return select; } - for (Sort.Order order : sort) { + if (sort.isSorted()) { + + for (Sort.Order order : sort) { - JpqlQueryBuilder.Expression expression; - QueryUtils.checkSortExpression(order); + JpqlQueryBuilder.Expression expression; + QueryUtils.checkSortExpression(order); - try { - expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, - PropertyPath.from(order.getProperty(), entityType.getJavaType())); - } catch (PropertyReferenceException e) { + try { + expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, + 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 instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) { + expression = JpqlQueryBuilder.expression(order.getProperty()); + } else { + throw e; + } } - } - if (order.isIgnoreCase()) { - expression = JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expression); + if (order.isIgnoreCase()) { + expression = JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expression); + } + + select.orderBy(JpqlQueryBuilder.orderBy(expression, order)); } + } else { + + if (searchQuery) { - select.orderBy(JpqlQueryBuilder.orderBy(expression, order)); + DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction()); + if (distanceFunction != null) { + select + .orderBy(JpqlQueryBuilder.orderBy(JpqlQueryBuilder.expression("distance"), distanceFunction.direction())); + } + } } return select; @@ -248,17 +289,46 @@ 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)); } + JpqlQueryBuilder.Expression distance = null; + if (searchQuery) { + distance = getDistanceExpression(); + } + if (useTupleQuery()) { + if (searchQuery) { + paths.add((distance != null ? distance : JpqlQueryBuilder.literal(0)).as("distance")); + } return selectStep.select(paths); } else { - return selectStep.instantiate(returnedType.getReturnedType(), paths); + + JpqlQueryBuilder.ConstructorExpression expression = new JpqlQueryBuilder.ConstructorExpression( + returnedType.getReturnedType().getName(), new JpqlQueryBuilder.Multiselect(entity, paths)); + + List selection = new ArrayList<>(2); + selection.add(expression); + + if (searchQuery) { + selection.add((distance != null ? distance : JpqlQueryBuilder.literal(0)).as("distance")); + } + + return selectStep.select(selection); + } + } + + if (searchQuery) { + + JpqlQueryBuilder.Expression distance = getDistanceExpression(); + + if (distance != null) { + return selectStep.select(new JpqlQueryBuilder.Multiselect(entity, + Arrays.asList(new JpqlQueryBuilder.EntitySelection(entity), distance.as("distance")))); } } @@ -287,6 +357,34 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { } } + @org.springframework.lang.Nullable + private JpqlQueryBuilder.Expression getDistanceExpression() { + + DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction()); + + if (distanceFunction != null) { + JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType, + getVectorPath(), true); + return JpqlQueryBuilder.function(distanceFunction.distanceFunction(), pas, + placeholder(provider.getVectorBinding())); + } + + return null; + } + + PropertyPath getVectorPath() { + + for (PartTree.OrPart parts : tree) { + for (Part part : parts) { + if (part.getType() == NEAR || part.getType() == WITHIN) { + return part.getProperty(); + } + } + } + + throw new IllegalStateException("No vector path found"); + } + Collection getRequiredSelection(Sort sort, ReturnedType returnedType) { return returnedType.getInputProperties(); } @@ -307,7 +405,7 @@ JpqlQueryBuilder.Expression placeholder(ParameterBinding binding) { * @return */ private JpqlQueryBuilder.Predicate toPredicate(Part part) { - return new PredicateBuilder(part).build(); + return new PredicateBuilder(part, similarityNormalizer).build(); } /** @@ -315,21 +413,23 @@ private JpqlQueryBuilder.Predicate toPredicate(Part part) { * * @author Phil Webb * @author Oliver Gierke + * @author Mark Paluch */ private class PredicateBuilder { private final Part part; + private final SimilarityNormalizer normalizer; /** * Creates a new {@link PredicateBuilder} for the given {@link Part}. * * @param part must not be {@literal null}. + * @param normalizer must not be {@literal null}. */ - public PredicateBuilder(Part part) { - - Assert.notNull(part, "Part must not be null"); + public PredicateBuilder(Part part, SimilarityNormalizer normalizer) { this.part = part; + this.normalizer = normalizer; } /** @@ -387,11 +487,10 @@ public JpqlQueryBuilder.Predicate build() { PartTreeParameterBinding parameter = provider.next(part, String.class); JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(), placeholder(parameter)); + // Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter()); String escapeChar = Character.toString(escape.getEscapeCharacter()); - return - - type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) + return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? whereIgnoreCase.notLike(parameterExpression, escapeChar) : whereIgnoreCase.like(parameterExpression, escapeChar); case TRUE: @@ -418,12 +517,99 @@ public JpqlQueryBuilder.Predicate build() { where = JpqlQueryBuilder.where(entity, property); return type.equals(IS_NOT_EMPTY) ? where.isNotEmpty() : where.isEmpty(); + case WITHIN: + case NEAR: + PartTreeParameterBinding vector = provider.next(part); + PartTreeParameterBinding within = provider.next(part); + + if (within.getValue() instanceof Range r) { + + Range range = (Range) r; + + if (range.getUpperBound().isBounded() || range.getUpperBound().isBounded()) { + + Range.Bound lower = range.getLowerBound(); + Range.Bound upper = range.getUpperBound(); + + String distanceFunction = getDistanceFunction(provider.getScoringFunction()); + JpqlQueryBuilder.Expression distance = JpqlQueryBuilder.function(distanceFunction, pas, + placeholder(vector)); + + JpqlQueryBuilder.Predicate lowerPredicate = null; + JpqlQueryBuilder.Predicate upperPredicate = null; + + // Score is a distance function, you typically want less when you specify a lower boundary, + // therefore lower and upper predicates are inverted. + if (lower.isBounded()) { + JpqlQueryBuilder.Expression distanceValue = placeholder(provider.lower(within, normalizer)); + lowerPredicate = getUpperPredicate(lower.isInclusive(), distance, distanceValue); + } + + if (upper.isBounded()) { + JpqlQueryBuilder.Expression distanceValue = placeholder(provider.upper(within, normalizer)); + upperPredicate = getLowerPredicate(upper.isInclusive(), distance, distanceValue); + } + + if (lowerPredicate != null && upperPredicate != null) { + return lowerPredicate.and(upperPredicate); + } else if (lowerPredicate != null) { + return lowerPredicate; + } else if (upperPredicate != null) { + return upperPredicate; + } + } + } + + if (within.getValue() instanceof Score score) { + + String distanceFunction = getDistanceFunction(score.getFunction()); + JpqlQueryBuilder.Expression distanceValue = placeholder(provider.normalize(within, normalizer)); + JpqlQueryBuilder.Expression distance = JpqlQueryBuilder.function(distanceFunction, pas, + placeholder(vector)); + return getUpperPredicate(true, distance, distanceValue); + } + + throw new InvalidDataAccessApiUsageException( + "Near/Within keywords must be used with a Score or Range type"); default: throw new IllegalArgumentException("Unsupported keyword " + type); } } + private JpqlQueryBuilder.Predicate getLowerPredicate(boolean inclusive, JpqlQueryBuilder.Expression lhs, + JpqlQueryBuilder.Expression distance) { + return doLower(inclusive, lhs, distance); + } + + private JpqlQueryBuilder.Predicate getUpperPredicate(boolean inclusive, JpqlQueryBuilder.Expression lhs, + JpqlQueryBuilder.Expression distance) { + return doUpper(inclusive, lhs, distance); + } + + private static JpqlQueryBuilder.Predicate doLower(boolean inclusive, JpqlQueryBuilder.Expression lhs, + JpqlQueryBuilder.Expression distance) { + return inclusive ? JpqlQueryBuilder.where(lhs).gte(distance) : JpqlQueryBuilder.where(lhs).gt(distance); + } + + private static JpqlQueryBuilder.Predicate doUpper(boolean inclusive, JpqlQueryBuilder.Expression lhs, + JpqlQueryBuilder.Expression distance) { + return inclusive ? JpqlQueryBuilder.where(lhs).lte(distance) : JpqlQueryBuilder.where(lhs).lt(distance); + } + + private static String getDistanceFunction(ScoringFunction scoringFunction) { + + DistanceFunction distanceFunction = JpaQueryCreator.DISTANCE_FUNCTIONS.get(scoringFunction); + + if (distanceFunction == null) { + throw new IllegalArgumentException( + "Unsupported ScoringFunction: %s. Make sure to declare a supported ScoringFunction when creating Score/Similarity instances." + .formatted(scoringFunction.getName())); + } + + return distanceFunction.distanceFunction(); + } + /** * Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part} * requires ignoring case. 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 be0a09bc4c..c157161683 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 @@ -18,8 +18,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import jakarta.persistence.StoredProcedureQuery; +import jakarta.persistence.Tuple; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -32,12 +34,18 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Similarity; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.StreamUtils; @@ -123,6 +131,80 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso } } + static class SearchResultExecution extends JpaQueryExecution { + + private final JpaQueryExecution delegate; + private final ReturnedType returnedType; + private final ScoringFunction function; + private final boolean normalizeSimilarity; + private final SimilarityNormalizer normalizer; + + SearchResultExecution(JpaQueryExecution delegate, ReturnedType returnedType, ScoringFunction function, + boolean normalizeSimilarity) { + + this.delegate = delegate; + this.returnedType = returnedType; + this.function = function; + this.normalizeSimilarity = normalizeSimilarity; + this.normalizer = normalizeSimilarity ? SimilarityNormalizer.get(function) : SimilarityNormalizer.IDENTITY; + } + + @Override + protected @Nullable Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) { + + Object result = delegate.execute(query, accessor); + + if (result instanceof Tuple || result instanceof Object[]) { + return map(result); + } + + if (result instanceof Collection c) { + + List> objects = new ArrayList<>(c.size()); + + for (Object o : c) { + objects.add(o instanceof Tuple || o instanceof Object[] ? map(o) : new SearchResult<>(o, 0)); + } + + return new SearchResults<>(objects); + } + + return result; + } + + private @Nullable SearchResult map(Object result) { + + if (result instanceof Tuple t) { + + Object value = returnedType.needsCustomConstruction() ? t : t.get(0); + try { + return new SearchResult<>(value, getScore(t.get("distance", Number.class).doubleValue())); + } catch (RuntimeException e) { + return new SearchResult<>(value, getScore(0)); + } + } + + if (result instanceof Object[] objects) { + + Object value = returnedType.needsCustomConstruction() ? objects : objects[0]; + try { + + return new SearchResult<>(value, getScore(((Number) (objects[objects.length - 1])).doubleValue())); + } catch (RuntimeException e) { + return new SearchResult<>(value, getScore(0)); + } + } + + return null; + } + + private Score getScore(double score) { + return normalizeSimilarity ? Similarity.raw(normalizer.getSimilarity(score), function) + : Score.of(score, function); + } + + } + /** * Executes the query to return a {@link org.springframework.data.domain.Window} of entities. * 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 45c804e124..e317528e8c 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 @@ -28,9 +28,9 @@ import java.util.Objects; import java.util.function.Supplier; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.Predicates; import org.springframework.lang.CheckReturnValue; @@ -124,15 +124,20 @@ 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); } + @Override + public Select select(Selection selection) { + return new Select(postProcess(selection), from); + } + Selection postProcess(Selection selection) { return distinct ? new DistinctSelection(selection) : selection; } @@ -239,6 +244,17 @@ public static Expression parameter(ParameterPlaceholder placeholder) { return new ParameterExpression(placeholder); } + /** + * Create a new ordering expression. + * + * @param sortExpression + * @return + * @since 4.0 + */ + public static Expression orderBy(Expression sortExpression) { + return new OrderExpression(sortExpression, null, Sort.NullHandling.NATIVE); + } + /** * Create a new ordering expression. * @@ -247,7 +263,19 @@ public static Expression parameter(ParameterPlaceholder placeholder) { * @return */ public static Expression orderBy(Expression sortExpression, Sort.Order order) { - return new OrderExpression(sortExpression, order); + return new OrderExpression(sortExpression, order.getDirection(), order.getNullHandling()); + } + + /** + * Create a new ordering expression. + * + * @param sortExpression + * @param direction + * @return + * @since 4.0 + */ + public static Expression orderBy(Expression sortExpression, Sort.Direction direction) { + return new OrderExpression(sortExpression, direction, Sort.NullHandling.NATIVE); } /** @@ -431,7 +459,7 @@ public interface SelectStep { * @return */ @CheckReturnValue - default Select instantiate(Class resultType, Collection paths) { + default Select instantiate(Class resultType, Collection paths) { return instantiate(resultType.getName(), paths); } @@ -440,10 +468,10 @@ default Select instantiate(Class resultType, Collection paths); + Select instantiate(String resultType, Collection paths); /** * Specify a multi-select. @@ -452,7 +480,7 @@ default Select instantiate(Class resultType, Collection paths); + Select select(Collection paths); /** * Select a single attribute. @@ -465,9 +493,18 @@ default Select select(JpqlQueryBuilder.PathExpression path) { return select(List.of(path)); } + /** + * Select a single attribute. + * + * @param selection + * @return + */ + @CheckReturnValue + Select select(Selection selection); + } - interface Selection { + public interface Selection { String render(RenderContext context); } @@ -530,7 +567,7 @@ static PathAndOrigin path(Origin origin, String path) { * * @param source */ - record EntitySelection(Entity source) implements Selection { + record EntitySelection(Entity source) implements Selection, Expression { @Override public String render(RenderContext context) { @@ -568,7 +605,7 @@ public String toString() { * @param resultType * @param multiselect */ - record ConstructorExpression(String resultType, Multiselect multiselect) implements Selection { + record ConstructorExpression(String resultType, Multiselect multiselect) implements Selection, Expression { @Override public String render(RenderContext context) { @@ -588,22 +625,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 (PathExpression path : paths) { + for (Expression path : paths) { if (!builder.isEmpty()) { builder.append(", "); } builder.append(path.render(context)); - if (!context.isConstructorContext()) { - builder.append(" ").append(path.getPropertyPath().getSegment()); + if (!context.isConstructorContext() && path instanceof AliasedExpression ae) { + builder.append(" ").append(ae.getAlias()); } } @@ -677,6 +714,47 @@ public interface Expression { * @return */ String render(RenderContext context); + + default AliasedExpression as(String alias) { + + if (this instanceof DefaultAliasedExpression de) { + return new DefaultAliasedExpression(de.delegate, alias); + } + + return new DefaultAliasedExpression(this, alias); + } + } + + /** + * Aliased expression. + * + * @since 4.0 + */ + public interface AliasedExpression extends Expression { + + /** + * @return the expression alias. + */ + String getAlias(); + + } + + record DefaultAliasedExpression(Expression delegate, String alias) implements AliasedExpression { + + @Override + public String render(RenderContext context) { + return delegate.render(context); + } + + @Override + public String getAlias() { + return alias(); + } + + @Override + public String toString() { + return render(RenderContext.EMPTY); + } } /** @@ -812,7 +890,8 @@ public String toString() { } } - record OrderExpression(Expression sortExpression, Sort.Order order) implements Expression { + record OrderExpression(Expression sortExpression, @org.springframework.lang.Nullable Sort.Direction direction, + Sort.NullHandling nullHandling) implements Expression { @Override public String render(RenderContext context) { @@ -820,14 +899,17 @@ 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 (direction != null) { - if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) { - builder.append(" NULLS FIRST"); - } else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) { - builder.append(" NULLS LAST"); + builder.append(" "); + builder.append(direction.isDescending() ? TOKEN_DESC : TOKEN_ASC); + + if (nullHandling == Sort.NullHandling.NULLS_FIRST) { + builder.append(" NULLS FIRST"); + } else if (nullHandling == Sort.NullHandling.NULLS_LAST) { + builder.append(" NULLS LAST"); + } } return builder.toString(); @@ -1395,7 +1477,8 @@ public String toString() { * @param origin * @param onTheJoin whether the path should target the join itself instead of matching {@link PropertyPath}. */ - record PathAndOrigin(PropertyPath path, Origin origin, boolean onTheJoin) implements PathExpression { + record PathAndOrigin(PropertyPath path, Origin origin, + boolean onTheJoin) implements PathExpression, AliasedExpression { @Override public PropertyPath getPropertyPath() { @@ -1411,6 +1494,11 @@ public String render(RenderContext context) { return context.getAlias(origin()); } } + + @Override + public String getAlias() { + return path().getSegment(); + } } /** 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 040e84a8ed..ac5462175b 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 @@ -23,10 +23,13 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.Vector; import org.springframework.data.expression.ValueExpression; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; @@ -160,6 +163,15 @@ public String toString() { * @param valueToBind value to prepare */ public @Nullable Object prepare(@Nullable Object valueToBind) { + + if (valueToBind instanceof Score score) { + return score.getValue(); + } + + if (valueToBind instanceof Vector v) { + return v.getType() == Float.TYPE ? v.toFloatArray() : v.toDoubleArray(); + } + return valueToBind; } @@ -216,6 +228,7 @@ static class PartTreeParameterBinding extends ParameterBinding { private final Type type; private final boolean ignoreCase; private final boolean noWildcards; + private final @Nullable Object value; public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class parameterType, Part part, @Nullable Object value, JpqlQueryTemplates templates, EscapeCharacter escape) { @@ -225,7 +238,7 @@ public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin or this.parameterType = parameterType; this.templates = templates; this.escape = escape; - + this.value = value; this.type = value == null && (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType())) ? Type.IS_NULL @@ -241,9 +254,14 @@ public boolean isIsNullParameter() { return Type.IS_NULL.equals(type); } + public @Nullable Object getValue() { + return value; + } + @Override public @Nullable Object prepare(@Nullable Object value) { + value = super.prepare(value); if (value == null || parameterType == null) { return value; } @@ -306,6 +324,9 @@ public boolean isIsNullParameter() { return Collections.singleton(value); } + public String lower() { + return null; + } } /** @@ -389,7 +410,7 @@ public Type getType() { @Override public @Nullable Object prepare(@Nullable Object value) { - Object unwrapped = PersistenceProvider.unwrapTypedParameterValue(value); + Object unwrapped = PersistenceProvider.unwrapTypedParameterValue(super.prepare(value)); if (unwrapped == null) { return null; } @@ -544,6 +565,26 @@ default String getName() { default int getPosition() { throw new IllegalStateException("No position associated"); } + + /** + * Map the name of the binding to a new name using the given {@link Function} if the binding has a name. If the + * binding is not associated with a name, then the binding is returned unchanged. + * + * @param nameMapper must not be {@literal null}. + * @return the transformed {@link BindingIdentifier} if the binding has a name, otherwise the binding itself. + * @since 4.0 + */ + BindingIdentifier mapName(Function nameMapper); + + /** + * Associate a position with the binding. + * + * @param position + * @return the new binding identifier with the position. + * @since 4.0 + */ + BindingIdentifier withPosition(int position); + } private record Named(String name) implements BindingIdentifier { @@ -562,6 +603,16 @@ public String getName() { public String toString() { return name(); } + + @Override + public BindingIdentifier mapName(Function nameMapper) { + return new Named(nameMapper.apply(name())); + } + + @Override + public BindingIdentifier withPosition(int position) { + return new NamedAndIndexed(name, position); + } } private record Indexed(int position) implements BindingIdentifier { @@ -576,6 +627,16 @@ public int getPosition() { return position(); } + @Override + public BindingIdentifier mapName(Function nameMapper) { + return this; + } + + @Override + public BindingIdentifier withPosition(int position) { + return new Indexed(position); + } + @Override public String toString() { return "[" + position() + "]"; @@ -604,6 +665,16 @@ public int getPosition() { return position(); } + @Override + public BindingIdentifier mapName(Function nameMapper) { + return new NamedAndIndexed(nameMapper.apply(name), position); + } + + @Override + public BindingIdentifier withPosition(int position) { + return new NamedAndIndexed(name, position); + } + @Override public String toString() { return "[" + name() + ", " + position() + "]"; 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 72d43ab5bd..b1c08fc58c 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 @@ -20,33 +20,29 @@ import jakarta.persistence.criteria.CriteriaBuilder; import java.util.ArrayList; -import java.util.Arrays; -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.jspecify.annotations.Nullable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.Vector; 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; import org.springframework.data.repository.query.parser.Part; -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.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; /** - * Helper class to allow easy creation of {@link ParameterMetadata}s. + * Helper class to allow easy creation of {@link PartTreeParameterBinding}s. * * @author Oliver Gierke * @author Thomas Darimont @@ -60,14 +56,19 @@ */ public class ParameterMetadataProvider { + static final Object PLACEHOLDER = new Object(); + private final Iterator parameters; + private final @Nullable JpaParametersParameterAccessor accessor; private final List bindings; private final Set syntheticParameterNames = new LinkedHashSet<>(); + private @Nullable ParameterBinding vector; private final @Nullable Iterator bindableParameterValues; private final EscapeCharacter escape; private final JpqlQueryTemplates templates; private final JpaParameters jpaParameters; private int position; + private int bindMarker; /** * Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and @@ -77,9 +78,9 @@ public class ParameterMetadataProvider { * @param escape must not be {@literal null}. * @param templates must not be {@literal null}. */ - public ParameterMetadataProvider(JpaParametersParameterAccessor accessor, - EscapeCharacter escape, JpqlQueryTemplates templates) { - this(accessor.iterator(), accessor.getParameters(), escape, templates); + public ParameterMetadataProvider(JpaParametersParameterAccessor accessor, EscapeCharacter escape, + JpqlQueryTemplates templates) { + this(accessor.iterator(), accessor, accessor.getParameters(), escape, templates); } /** @@ -90,9 +91,8 @@ public ParameterMetadataProvider(JpaParametersParameterAccessor accessor, * @param escape must not be {@literal null}. * @param templates must not be {@literal null}. */ - public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escape, - JpqlQueryTemplates templates) { - this(null, parameters, escape, templates); + public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escape, JpqlQueryTemplates templates) { + this(null, null, parameters, escape, templates); } /** @@ -104,14 +104,15 @@ public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escap * @param escape must not be {@literal null}. * @param templates must not be {@literal null}. */ - private ParameterMetadataProvider(@Nullable Iterator bindableParameterValues, JpaParameters parameters, - EscapeCharacter escape, JpqlQueryTemplates templates) { - + private ParameterMetadataProvider(@Nullable Iterator bindableParameterValues, + @Nullable JpaParametersParameterAccessor accessor, JpaParameters parameters, EscapeCharacter escape, + JpqlQueryTemplates templates) { 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.jpaParameters = parameters; + this.accessor = accessor; this.parameters = parameters.getBindableParameters().iterator(); this.bindings = new ArrayList<>(); this.bindableParameterValues = bindableParameterValues; @@ -119,6 +120,10 @@ private ParameterMetadataProvider(@Nullable Iterator bindableParameterVa this.templates = templates; } + public JpaParameters getParameters() { + return this.jpaParameters; + } + /** * Returns all {@link ParameterBinding}s built. * @@ -128,11 +133,23 @@ public List getBindings() { return bindings; } + /** + * @return the {@link SimilarityNormalizer}. + */ + SimilarityNormalizer getSimilarityNormalizer() { + + if (accessor != null && accessor.normalizeSimilarity()) { + return SimilarityNormalizer.get(accessor.getScoringFunction()); + } + + return SimilarityNormalizer.IDENTITY; + } + /** * Builds a new {@link PartTreeParameterBinding} for given {@link Part} and the next {@link Parameter}. */ @SuppressWarnings("unchecked") - public PartTreeParameterBinding next(Part part) { + PartTreeParameterBinding next(Part part) { Assert.isTrue(parameters.hasNext(), () -> String.format("No parameter available for part %s", part)); @@ -144,12 +161,11 @@ public PartTreeParameterBinding next(Part part) { * 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}. + * @param is the type parameter of the returned {@link PartTreeParameterBinding}. * @param type must not be {@literal null}. * @return ParameterMetadata for the next parameter. */ - @SuppressWarnings("unchecked") - public PartTreeParameterBinding next(Part part, Class type) { + PartTreeParameterBinding next(Part part, Class type) { Parameter parameter = parameters.next(); Class typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type; @@ -159,11 +175,11 @@ public PartTreeParameterBinding next(Part part, Class type) { /** * Builds a new {@link PartTreeParameterBinding} for the given type and name. * - * @param type parameter for the returned {@link ParameterMetadata}. + * @param type parameter for the returned {@link PartTreeParameterBinding}. * @param part must not be {@literal null}. * @param type must not be {@literal null}. - * @param parameter providing the name for the returned {@link ParameterMetadata}. - * @return a new {@link ParameterMetadata} for the given type and name. + * @param parameter providing the name for the returned {@link PartTreeParameterBinding}. + * @return a new {@link PartTreeParameterBinding} for the given type and name. */ private PartTreeParameterBinding next(Part part, Class type, Parameter parameter) { @@ -175,23 +191,63 @@ private PartTreeParameterBinding next(Part part, Class type, Parameter pa @SuppressWarnings("unchecked") Class reifiedType = Expression.class.equals(type) ? (Class) Object.class : type; - Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next(); - + Object value = bindableParameterValues == null ? PLACEHOLDER : bindableParameterValues.next(); int currentPosition = ++position; + int currentBindMarker = ++bindMarker; - BindingIdentifier bindingIdentifier = parameter.getName().map(it -> BindingIdentifier.of(it, currentPosition)) + BindingIdentifier bindingIdentifier = parameter.getName().map(it -> BindingIdentifier.of(it, currentBindMarker)) + .orElseGet(() -> BindingIdentifier.of(currentBindMarker)); + + BindingIdentifier origin = 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); - PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier, methodParameter, reifiedType, - part, value, templates, escape); + MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(origin); + PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier, + methodParameter, reifiedType, part, value, templates, escape); + // PartTreeParameterBinding is more expressive than a potential ParameterBinding for Vector. bindings.add(binding); + if (Vector.class.isAssignableFrom(parameter.getType())) { + this.vector = binding; + } + return binding; } + ScoringFunction getScoringFunction() { + + if (accessor != null) { + return accessor.getScoringFunction(); + } + + return ScoringFunction.unspecified(); + } + + ParameterBinding getVectorBinding() { + + if (!getParameters().hasVectorParameter()) { + throw new IllegalStateException("Vector parameter not available"); + } + + if (this.vector != null) { + return this.vector; + } + + int vectorIndex = getParameters().getVectorIndex(); + + BindingIdentifier bindingIdentifier = BindingIdentifier.of(vectorIndex + 1); + + /* identifier refers to bindable parameters, not _all_ parameters index */ + MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(bindingIdentifier); + ParameterBinding parameterBinding = new ParameterBinding(bindingIdentifier, methodParameter); + + this.bindings.add(parameterBinding); + + return parameterBinding; + } + EscapeCharacter getEscape() { return escape; } @@ -204,9 +260,9 @@ EscapeCharacter getEscape() { * @param source * @return a new {@link ParameterBinding} for the given value and source. */ - public ParameterBinding nextSynthetic(String nameHint, Object value, Object source) { + ParameterBinding nextSynthetic(String nameHint, Object value, Object source) { - int currentPosition = ++position; + int currentPosition = ++bindMarker; String bindingName = nameHint; if (!syntheticParameterNames.add(bindingName)) { @@ -219,126 +275,124 @@ public ParameterBinding nextSynthetic(String nameHint, Object value, Object sour ParameterOrigin.synthetic(value, source)); } - public JpaParameters getParameters() { - return this.jpaParameters; - } + RangeParameterBinding lower(PartTreeParameterBinding within, SimilarityNormalizer normalizer) { - /** - * @author Oliver Gierke - * @author Thomas Darimont - * @author Andrey Kovalev - */ - public static class ParameterMetadata { + int bindMarker = within.getRequiredPosition(); - static final Object PLACEHOLDER = new Object(); + if (!bindings.remove(within)) { + bindMarker = ++this.bindMarker; + } - private final Class parameterType; - private final Type type; - private final int position; - private final JpqlQueryTemplates templates; - private final EscapeCharacter escape; - private final boolean ignoreCase; - private final boolean noWildcards; + BindingIdentifier identifier = within.getIdentifier(); + RangeParameterBinding rangeBinding = new RangeParameterBinding( + identifier.mapName(name -> name + "_lower").withPosition(bindMarker), within.getOrigin(), true, normalizer); + bindings.add(rangeBinding); - /** - * Creates a new {@link ParameterMetadata}. - */ - public ParameterMetadata(Class parameterType, Part part, @Nullable Object value, EscapeCharacter escape, - int position, JpqlQueryTemplates templates) { - - 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 - : part.getType(); - this.ignoreCase = IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); - this.noWildcards = part.getProperty().getLeafProperty().isCollection(); - this.escape = escape; - } + return rangeBinding; + } - public int getPosition() { - return position; - } + RangeParameterBinding upper(PartTreeParameterBinding within, SimilarityNormalizer normalizer) { - public Class getParameterType() { - return parameterType; - } + int bindMarker = within.getRequiredPosition(); - /** - * Returns whether the parameter shall be considered an {@literal IS NULL} parameter. - */ - public boolean isIsNullParameter() { - return Type.IS_NULL.equals(type); + if (!bindings.remove(within)) { + bindMarker = ++this.bindMarker; } + BindingIdentifier identifier = within.getIdentifier(); + RangeParameterBinding rangeBinding = new RangeParameterBinding( + identifier.mapName(name -> name + "_upper").withPosition(bindMarker), within.getOrigin(), false, normalizer); + bindings.add(rangeBinding); + + return rangeBinding; + } + + ScoreParameterBinding normalize(PartTreeParameterBinding within, SimilarityNormalizer normalizer) { + + bindings.remove(within); + + ScoreParameterBinding rangeBinding = new ScoreParameterBinding(within.getIdentifier(), within.getOrigin(), + normalizer); + bindings.add(rangeBinding); + + return rangeBinding; + } + + static class ScoreParameterBinding extends ParameterBinding { + + private final SimilarityNormalizer normalizer; + /** - * Prepares the object before it's actually bound to the {@link jakarta.persistence.Query;}. + * Creates a new {@link ParameterBinding} for the parameter with the given identifier and origin. * - * @param value can be {@literal null}. + * @param identifier of the parameter, must not be {@literal null}. + * @param origin the origin of the parameter (expression or method argument) */ - public @Nullable Object prepare(@Nullable Object value) { + ScoreParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, SimilarityNormalizer normalizer) { + super(identifier, origin); + this.normalizer = normalizer; + } + + @Override + public @Nullable Object prepare(@Nullable Object valueToBind) { - if (value == null || parameterType == null) { - return value; + if (valueToBind instanceof Score score) { + return normalizer.getScore(score.getValue()); } - if (String.class.equals(parameterType) && !noWildcards) { + return super.prepare(valueToBind); + } - 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; - }; + @Override + public boolean isCompatibleWith(ParameterBinding binding) { + + if (super.isCompatibleWith(binding) && binding instanceof ScoreParameterBinding other) { + return normalizer == other.normalizer; } - return Collection.class.isAssignableFrom(parameterType) // - ? potentiallyIgnoreCase(ignoreCase, toCollection(value)) // - : value; + return false; } + } + + static class RangeParameterBinding extends ScoreParameterBinding { + + private final boolean lower; /** - * 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}. + * Creates a new {@link ParameterBinding} for the parameter with the given identifier and origin. * - * @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. + * @param identifier of the parameter, must not be {@literal null}. + * @param origin the origin of the parameter (expression or method argument) */ - private static @Nullable Collection toCollection(@Nullable Object value) { - - if (value == null) { - return null; - } - - if (value instanceof Collection collection) { - return collection.isEmpty() ? null : collection; - } + RangeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, boolean lower, + SimilarityNormalizer normalizer) { + super(identifier, origin, normalizer); + this.lower = lower; + } - if (ObjectUtils.isArray(value)) { + @Override + public @Nullable Object prepare(@Nullable Object valueToBind) { - List collection = Arrays.asList(ObjectUtils.toObjectArray(value)); - return collection.isEmpty() ? null : collection; + if (valueToBind instanceof Range r) { + if (lower) { + return super.prepare(r.getLowerBound().getValue().orElse(null)); + } else { + return super.prepare(r.getUpperBound().getValue().orElse(null)); + } } - return Collections.singleton(value); + return super.prepare(valueToBind); } - @SuppressWarnings("unchecked") - private @Nullable Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { + @Override + public boolean isCompatibleWith(ParameterBinding binding) { - if (!ignoreCase || CollectionUtils.isEmpty(collection)) { - return collection; + if (super.isCompatibleWith(binding) && binding instanceof RangeParameterBinding other) { + return lower == other.lower; } - return ((Collection) collection).stream() // - .map(it -> it == null // - ? null // - : templates.ignoreCase(it)) // - .collect(Collectors.toList()); + return false; } - } + } 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 bf254c46ba..06ee74cf02 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 @@ -129,7 +129,7 @@ public TypedQuery doCreateCountQuery(JpaParametersParameterAccessor access } @Override - protected JpaQueryExecution getExecution() { + protected JpaQueryExecution getExecution(JpaParametersParameterAccessor accessor) { if (this.getQueryMethod().isScrollQuery()) { return new ScrollExecution(this.tree.getSort(), new ScrollDelegate<>(entityInformation)); @@ -139,7 +139,7 @@ protected JpaQueryExecution getExecution() { return new ExistsExecution(); } - return super.getExecution(); + return super.getExecution(accessor); } private static void validate(PartTree tree, JpaParameters parameters, String methodName) { @@ -301,13 +301,16 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess entityManager); } - JpqlQueryCreator creator = new CacheableJpqlQueryCreator(sort, - new JpaQueryCreator(tree, returnedType, provider, templates, em)); - - if (accessor.getParameters().hasDynamicProjection()) { - return creator; + JpaParameters parameters = getQueryMethod().getParameters(); + if (accessor.getParameters().hasDynamicProjection() || getQueryMethod().isSearchQuery() + || parameters.hasScoreRangeParameter() || parameters.hasScoreParameter()) { + return new JpaQueryCreator(tree, getQueryMethod().isSearchQuery(), returnedType, provider, templates, + em.getMetamodel()); } + JpqlQueryCreator creator = new CacheableJpqlQueryCreator(sort, new JpaQueryCreator(tree, + getQueryMethod().isSearchQuery(), returnedType, provider, templates, em.getMetamodel())); + cache.put(sort, accessor, creator); return creator; 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 6d6196b8ef..b97c39da5b 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 @@ -305,6 +305,10 @@ private PartTreeQueryParameterSetterFactory(JpaParameters parameters) { return super.create(binding, query); } + if (binding instanceof ParameterMetadataProvider.ScoreParameterBinding) { + return super.create(binding, query); + } + return null; } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimilarityNormalizer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimilarityNormalizer.java new file mode 100644 index 0000000000..daead89db0 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimilarityNormalizer.java @@ -0,0 +1,125 @@ +/* + * 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.HashMap; +import java.util.Map; +import java.util.function.DoubleUnaryOperator; + +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.VectorScoringFunctions; + +/** + * Normalizes the score returned by a database to a similarity value and vice versa. + * + * @author Mark Paluch + * @since 4.0 + * @see org.springframework.data.domain.Similarity + */ +public class SimilarityNormalizer { + + /** + * Identity normalizer for {@link ScoringFunction#unspecified()} scoring function without altering the score. + */ + public static final SimilarityNormalizer IDENTITY = new SimilarityNormalizer(ScoringFunction.unspecified(), + DoubleUnaryOperator.identity(), DoubleUnaryOperator.identity()); + + /** + * Normalizer for Euclidean scores using {@code euclidean_distance(…)} as the scoring function. + */ + public static final SimilarityNormalizer EUCLIDEAN = new SimilarityNormalizer(VectorScoringFunctions.EUCLIDEAN, + it -> 1 / (1.0 + Math.pow(it, 2)), it -> it == 0 ? Float.MAX_VALUE : Math.sqrt((1 / it) - 1)); + + /** + * Normalizer for Cosine scores using {@code cosine_distance(…)} as the scoring function. + */ + public static final SimilarityNormalizer COSINE = new SimilarityNormalizer(VectorScoringFunctions.COSINE, + it -> (1.0 + (1 - it)) / 2.0, it -> 1 - ((it * 2) - 1)); + + /** + * Normalizer for Negative Inner Product (Dot) scores using {@code negative_inner_product(…)} as the scoring function. + */ + public static final SimilarityNormalizer DOT_PRODUCT = new SimilarityNormalizer(VectorScoringFunctions.DOT_PRODUCT, + it -> (1 - it) / 2, it -> 1 - (it * 2)); + + private static final Map NORMALIZERS = new HashMap<>(); + + static { + NORMALIZERS.put(EUCLIDEAN.scoringFunction, EUCLIDEAN); + NORMALIZERS.put(COSINE.scoringFunction, COSINE); + NORMALIZERS.put(DOT_PRODUCT.scoringFunction, DOT_PRODUCT); + } + + private final ScoringFunction scoringFunction; + private final DoubleUnaryOperator similarity; + private final DoubleUnaryOperator score; + + /** + * Constructor for {@link SimilarityNormalizer} using the given {@link DoubleUnaryOperator} for similarity and score + * computation. + * + * @param similarity compute the similarity from the underlying score returned by a database result. + * @param score compute the score value from a given {@link org.springframework.data.domain.Similarity} to compare + * against database results. + */ + SimilarityNormalizer(ScoringFunction scoringFunction, DoubleUnaryOperator similarity, DoubleUnaryOperator score) { + this.scoringFunction = scoringFunction; + this.score = score; + this.similarity = similarity; + } + + /** + * Lookup a {@link SimilarityNormalizer} for a given {@link ScoringFunction}. + * + * @param scoringFunction the scoring function to translate. + * @return the {@link SimilarityNormalizer} for the given {@link ScoringFunction}. + * @throws IllegalArgumentException if the {@link ScoringFunction} is not associated with a + * {@link SimilarityNormalizer}. + */ + public static SimilarityNormalizer get(ScoringFunction scoringFunction) { + + SimilarityNormalizer normalizer = NORMALIZERS.get(scoringFunction); + + if (normalizer == null) { + throw new IllegalArgumentException("No SimilarityNormalizer found for " + scoringFunction.getName()); + } + + return normalizer; + } + + /** + * @param score score value as returned by the database. + * @return the {@link org.springframework.data.domain.Similarity} value. + */ + public double getSimilarity(double score) { + return similarity.applyAsDouble(score); + } + + /** + * @param similarity similarity value as requested by the query mechanism. + * @return database score value. + */ + public double getScore(double similarity) { + return score.applyAsDouble(similarity); + } + + @Override + public String toString() { + return "%s Normalizer: Similarity[0 to 1] -> Score[%f to %f]".formatted(scoringFunction.getName(), getScore(0), + getScore(1)); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java new file mode 100644 index 0000000000..f4c334d39a --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java @@ -0,0 +1,342 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.annotations.Array; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Similarity; +import org.springframework.data.domain.Vector; +import org.springframework.data.domain.VectorScoringFunctions; +import org.springframework.test.annotation.Rollback; +import org.springframework.transaction.annotation.Transactional; + +/** + * Testcase to verify Vector Search work with Hibernate. + * + * @author Mark Paluch + */ +@Transactional +@Rollback(value = false) +abstract class AbstractVectorIntegrationTests { + + Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + @Autowired VectorSearchRepository repository; + + @BeforeEach + void setUp() { + + WithVector w1 = new WithVector("de", "one", new float[] { 0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f }); + WithVector w2 = new WithVector("de", "two", new float[] { 0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f }); + WithVector w3 = new WithVector("en", "three", new float[] { 0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f }); + WithVector w4 = new WithVector("de", "four", new float[] { 0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f }); + + repository.deleteAllInBatch(); + repository.saveAllAndFlush(Arrays.asList(w1, w2, w3, w4)); + } + + @ParameterizedTest + @MethodSource("scoringFunctions") + void shouldApplyVectorSearchWithDistance(VectorScoringFunctions functions) { + + SearchResults results = repository.searchTop5ByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.of(0, functions)); + + assertThat(results).hasSize(3).extracting(SearchResult::getContent).extracting(WithVector::getCountry) + .containsOnly("de", "de"); + + assertThat(results).extracting(SearchResult::getContent).extracting(WithVector::getDescription) + .containsExactlyInAnyOrder("two", "one", "four"); + } + + static Set scoringFunctions() { + return EnumSet.of(VectorScoringFunctions.COSINE, VectorScoringFunctions.DOT_PRODUCT, + VectorScoringFunctions.EUCLIDEAN); + } + + @Test + void shouldNormalizeEuclideanSimilarity() { + + SearchResults results = repository.searchTop5ByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.of(0.99, VectorScoringFunctions.EUCLIDEAN)); + + assertThat(results).hasSize(1); + + SearchResult two = results.getContent().get(0); + + assertThat(two.getContent().getDescription()).isEqualTo("two"); + assertThat(two.getScore()).isInstanceOf(Similarity.class); + assertThat(two.getScore().getValue()).isGreaterThan(0.99); + } + + @Test + void shouldNormalizeCosineSimilarity() { + + SearchResults results = repository.searchTop5ByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.of(0.999, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(1); + + SearchResult two = results.getContent().get(0); + + assertThat(two.getContent().getDescription()).isEqualTo("two"); + assertThat(two.getScore()).isInstanceOf(Similarity.class); + assertThat(two.getScore().getValue()).isGreaterThan(0.99); + } + + @Test + void shouldRunStringQuery() { + + List results = repository.findAnnotatedByCountryAndEmbeddingWithin("de", VECTOR, + Score.of(2, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(3).extracting(WithVector::getCountry).containsOnly("de", "de", "de"); + assertThat(results).extracting(WithVector::getDescription).containsSequence("two", "one", "four"); + } + + @Test + void shouldRunStringQueryWithDistance() { + + SearchResults results = repository.searchAnnotatedByCountryAndEmbeddingWithin("de", VECTOR, + Score.of(2, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(3).extracting(SearchResult::getContent).extracting(WithVector::getCountry) + .containsOnly("de", "de", "de"); + assertThat(results).extracting(SearchResult::getContent).extracting(WithVector::getDescription) + .containsSequence("two", "one", "four"); + + SearchResult result = results.getContent().get(0); + assertThat(result.getScore().getValue()).isGreaterThanOrEqualTo(0); + assertThat(result.getScore().getFunction()).isEqualTo(VectorScoringFunctions.COSINE); + } + + @Test + void shouldRunStringQueryWithFloatDistance() { + + SearchResults results = repository.searchAnnotatedByCountryAndEmbeddingWithin("de", VECTOR, 2); + + assertThat(results).hasSize(3).extracting(SearchResult::getContent).extracting(WithVector::getCountry) + .containsOnly("de", "de", "de"); + assertThat(results).extracting(SearchResult::getContent).extracting(WithVector::getDescription) + .containsSequence("two", "one", "four"); + + SearchResult result = results.getContent().get(0); + assertThat(result.getScore().getValue()).isGreaterThanOrEqualTo(0); + assertThat(result.getScore().getFunction()).isEqualTo(ScoringFunction.unspecified()); + } + + @Test + void shouldApplyVectorSearchWithRange() { + + SearchResults results = repository.searchAllByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.between(0, 1, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(3).extracting(SearchResult::getContent).extracting(WithVector::getCountry) + .containsOnly("de", "de", "de"); + assertThat(results).extracting(SearchResult::getContent).extracting(WithVector::getDescription) + .containsSequence("two", "one", "four"); + } + + @Test + void shouldApplyVectorSearchAndReturnList() { + + List results = repository.findAllByCountryAndEmbeddingWithin("de", VECTOR, + Score.of(10, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(3).extracting(WithVector::getCountry).containsOnly("de", "de", "de"); + assertThat(results).extracting(WithVector::getDescription).containsSequence("one", "two", "four"); + } + + @Test + void shouldProjectVectorSearchAsInterface() { + + SearchResults results = repository.searchInterfaceProjectionByCountryAndEmbeddingWithin("de", + VECTOR, Score.of(10, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(3).extracting(SearchResult::getContent).extracting(WithDescription::getDescription) + .containsSequence("two", "one", "four"); + } + + @Test + void shouldProjectVectorSearchAsDto() { + + SearchResults results = repository.searchDtoByCountryAndEmbeddingWithin("de", VECTOR, + Score.of(10, VectorScoringFunctions.COSINE)); + + assertThat(results).hasSize(3).extracting(SearchResult::getContent).extracting(DescriptionDto::getDescription) + .containsSequence("two", "one", "four"); + } + + @Test + void shouldProjectVectorSearchDynamically() { + + SearchResults dtos = repository.searchDynamicByCountryAndEmbeddingWithin("de", VECTOR, + Score.of(10, VectorScoringFunctions.COSINE), DescriptionDto.class); + + assertThat(dtos).hasSize(3).extracting(SearchResult::getContent).extracting(DescriptionDto::getDescription) + .containsSequence("two", "one", "four"); + + SearchResults proxies = repository.searchDynamicByCountryAndEmbeddingWithin("de", VECTOR, + Score.of(10, VectorScoringFunctions.COSINE), WithDescription.class); + + assertThat(proxies).hasSize(3).extracting(SearchResult::getContent).extracting(WithDescription::getDescription) + .containsSequence("two", "one", "four"); + } + + @Entity + @Table(name = "with_vector") + public static class WithVector { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // + private Integer id; + + private String country; + private String description; + + @Column(name = "the_embedding") + @JdbcTypeCode(SqlTypes.VECTOR) + @Array(length = 5) private float[] embedding; + + public WithVector() {} + + public WithVector(String country, String description, float[] embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getDescription() { + return description; + } + + public float[] getEmbedding() { + return embedding; + } + + public void setEmbedding(float[] embedding) { + this.embedding = embedding; + } + + @Override + public String toString() { + return "WithVector{" + "country='" + country + '\'' + ", description='" + description + '\'' + '}'; + } + } + + interface WithDescription { + String getDescription(); + } + + static class DescriptionDto { + + private final String description; + + public DescriptionDto(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + interface VectorSearchRepository extends JpaRepository { + + List findAllByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); + + @Query(""" + SELECT w FROM org.springframework.data.jpa.repository.AbstractVectorIntegrationTests$WithVector w + WHERE w.country = ?1 + AND cosine_distance(w.embedding, :embedding) <= :distance + ORDER BY cosine_distance(w.embedding, :embedding) asc""") + List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); + + @Query(""" + SELECT w, cosine_distance(w.embedding, :embedding) as distance FROM org.springframework.data.jpa.repository.AbstractVectorIntegrationTests$WithVector w + WHERE w.country = ?1 + AND cosine_distance(w.embedding, :embedding) <= :distance + ORDER BY distance asc""") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + Score distance); + + @Query(""" + SELECT w, cosine_distance(w.embedding, :embedding) as distance FROM org.springframework.data.jpa.repository.AbstractVectorIntegrationTests$WithVector w + WHERE w.country = ?1 + AND cosine_distance(w.embedding, :embedding) <= :distance + ORDER BY distance asc""") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + float distance); + + SearchResults searchAllByCountryAndEmbeddingWithin(String country, Vector embedding, + Range distance); + + SearchResults searchTop5ByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); + + SearchResults searchInterfaceProjectionByCountryAndEmbeddingWithin(String country, + Vector embedding, Score distance); + + SearchResults searchDtoByCountryAndEmbeddingWithin(String country, Vector embedding, + Score distance); + + SearchResults searchDynamicByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance, + Class projection); + + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OracleVectorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OracleVectorIntegrationTests.java new file mode 100644 index 0000000000..5b1d8779c6 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/OracleVectorIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * 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 java.net.URL; +import java.util.List; + +import org.hibernate.dialect.OracleDialect; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.support.TestcontainerConfigSupport; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import org.testcontainers.oracle.OracleContainer; +import org.testcontainers.utility.MountableFile; + +/** + * Testcase to verify Vector Search work with Oracle. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = OracleVectorIntegrationTests.Config.class) +class OracleVectorIntegrationTests extends AbstractVectorIntegrationTests { + + @EnableJpaRepositories(considerNestedRepositories = true, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = VectorSearchRepository.class)) + @EnableTransactionManagement + static class Config extends TestcontainerConfigSupport { + + public Config() { + super(OracleDialect.class, new ClassPathResource("scripts/oracle-vector.sql")); + } + + @Override + protected String getSchemaAction() { + return "none"; + } + + @Override + protected PersistenceManagedTypes getManagedTypes() { + return new PersistenceManagedTypes() { + @Override + public List getManagedClassNames() { + return List.of(WithVector.class.getName()); + } + + @Override + public List getManagedPackages() { + return List.of(); + } + + @Override + public @Nullable URL getPersistenceUnitRootUrl() { + return null; + } + + }; + } + + @SuppressWarnings("resource") + @Bean(initMethod = "start", destroyMethod = "start") + public OracleContainer container() { + + return new OracleContainer("gvenzl/oracle-free:23-slim") // + .withReuse(true) + .withCopyFileToContainer(MountableFile.forClasspathResource("/scripts/oracle-vector-initialize.sql"), + "/container-entrypoint-initdb.d/initialize.sql"); + } + + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/PgVectorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/PgVectorIntegrationTests.java new file mode 100644 index 0000000000..2427e1f930 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/PgVectorIntegrationTests.java @@ -0,0 +1,92 @@ +/* + * 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 java.net.URL; +import java.util.List; + +import org.hibernate.dialect.PostgreSQLDialect; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.support.TestcontainerConfigSupport; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import org.testcontainers.containers.PostgreSQLContainer; + +/** + * Testcase to verify Vector Search work with Postgres (PGvector). + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = PgVectorIntegrationTests.Config.class) +class PgVectorIntegrationTests extends AbstractVectorIntegrationTests { + + @EnableJpaRepositories(considerNestedRepositories = true, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = VectorSearchRepository.class)) + @EnableTransactionManagement + static class Config extends TestcontainerConfigSupport { + + public Config() { + super(PostgreSQLDialect.class, new ClassPathResource("scripts/pgvector.sql")); + } + + @Override + protected String getSchemaAction() { + return "none"; + } + + @Override + protected PersistenceManagedTypes getManagedTypes() { + return new PersistenceManagedTypes() { + @Override + public List getManagedClassNames() { + return List.of(WithVector.class.getName()); + } + + @Override + public List getManagedPackages() { + return List.of(); + } + + @Override + public @Nullable URL getPersistenceUnitRootUrl() { + return null; + } + }; + + } + + @SuppressWarnings("resource") + @Bean(initMethod = "start", destroyMethod = "start") + public PostgreSQLContainer container() { + + return new PostgreSQLContainer<>("pgvector/pgvector:pg17") // + .withUsername("postgres").withReuse(true); + } + + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java index 5366736fc9..64d52bc1d4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java @@ -23,10 +23,12 @@ import jakarta.persistence.Id; import jakarta.persistence.NamedStoredProcedureQuery; +import java.net.URL; import java.util.List; import java.util.Objects; import org.hibernate.dialect.MySQLDialect; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,6 +40,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.data.jpa.repository.support.TestcontainerConfigSupport; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -223,12 +227,33 @@ public interface EmployeeRepositoryWithNoCursor extends JpaRepository getManagedClassNames() { + return List.of(Employee.class.getName()); + } + + @Override + public List getManagedPackages() { + return List.of(); + } + + @Override + public @Nullable URL getPersistenceUnitRootUrl() { + return null; + } + }; + + } + @SuppressWarnings("resource") @Bean(initMethod = "start", destroyMethod = "stop") public MySQLContainer container() { 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 a88e23f9a6..77f46a5518 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 @@ -26,11 +26,13 @@ import jakarta.persistence.StoredProcedureParameter; import java.math.BigDecimal; +import java.net.URL; import java.util.List; import java.util.Map; import java.util.Objects; import org.hibernate.dialect.PostgreSQLDialect; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,7 +44,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.data.jpa.repository.support.TestcontainerConfigSupport; import org.springframework.data.jpa.util.DisabledOnHibernate; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -292,12 +296,33 @@ public interface EmployeeRepositoryWithRefCursor extends JpaRepository getManagedClassNames() { + return List.of(Employee.class.getName()); + } + + @Override + public List getManagedPackages() { + return List.of(); + } + + @Override + public @Nullable URL getPersistenceUnitRootUrl() { + return null; + } + }; + + } + @SuppressWarnings("resource") @Bean(initMethod = "start", destroyMethod = "stop") public PostgreSQLContainer container() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureNullHandlingIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureNullHandlingIntegrationTests.java index c5fba7eff7..125a2acbc7 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureNullHandlingIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureNullHandlingIntegrationTests.java @@ -20,10 +20,13 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.net.URL; import java.util.Date; +import java.util.List; import java.util.UUID; import org.hibernate.dialect.PostgreSQLDialect; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,7 +39,9 @@ import org.springframework.data.jpa.repository.Temporal; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.data.jpa.repository.support.TestcontainerConfigSupport; import org.springframework.data.jpa.util.DisabledOnHibernate; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -128,12 +133,33 @@ public interface TestModelRepository extends JpaRepository { @EnableJpaRepositories(considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = TestModelRepository.class)) @EnableTransactionManagement - static class Config extends StoredProcedureConfigSupport { + static class Config extends TestcontainerConfigSupport { public Config() { super(PostgreSQLDialect.class, new ClassPathResource("scripts/postgres-nullable-stored-procedures.sql")); } + @Override + protected PersistenceManagedTypes getManagedTypes() { + return new PersistenceManagedTypes() { + @Override + public List getManagedClassNames() { + return List.of(TestModel.class.getName()); + } + + @Override + public List getManagedPackages() { + return List.of(); + } + + @Override + public @Nullable URL getPersistenceUnitRootUrl() { + return null; + } + }; + + } + @SuppressWarnings("resource") @Bean(initMethod = "start", destroyMethod = "stop") public PostgreSQLContainer container() { 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 fdcbabf84b..3d9a616120 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 @@ -221,7 +221,7 @@ class DummyJpaQuery extends AbstractJpaQuery { } @Override - protected JpaQueryExecution getExecution() { + protected JpaQueryExecution getExecution(JpaParametersParameterAccessor accessor) { return execution; } 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 55e9f39122..582670223a 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 @@ -40,8 +40,11 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.jpa.util.TestMetaModel; import org.springframework.data.projection.ProjectionFactory; @@ -743,7 +746,8 @@ JpaQueryCreator queryCreator(PartTree tree, ReturnedType returnedType, Metamodel ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(parameterAccessor, EscapeCharacter.DEFAULT, templates); - return new JpaQueryCreator(tree, returnedType, parameterMetadataProvider, templates, entityManager); + return new JpaQueryCreator(tree, false, returnedType, parameterMetadataProvider, templates, + entityManager.getMetamodel()); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -979,6 +983,21 @@ public int bindingIndexFor(String placeholder) { public ParameterAccessor bindableParameters() { return new ParameterAccessor() { + @Override + public @Nullable Vector getVector() { + return null; + } + + @Override + public @Nullable Score getScore() { + return null; + } + + @Override + public @Nullable Range getScoreRange() { + return null; + } + @Override public @Nullable ScrollPosition getScrollPosition() { return null; 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 d2ac172373..46952dee71 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 @@ -33,6 +33,7 @@ * Unit tests for {@link JpqlQueryBuilder}. * * @author Christoph Strobl + * @author Mark Paluch */ class JpqlQueryBuilderUnitTests { @@ -77,6 +78,15 @@ void literalExpressionRendersAsIs() { assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))"); } + @Test // GH- + void aliasedExpression() { + + // aliasing is contextual and happens during selection rendering. E.g. constructor expressions don't use aliases. + Expression expression = expression("CONCAT(person.lastName, ‘, ’, person.firstName)").as("concatted"); + assertThat(expression.render(RenderContext.EMPTY)) + .isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName)"); + } + @Test // GH-3588 void xxx() { 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 81e454c799..963d742dd1 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 @@ -26,6 +26,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.Similarity; +import org.springframework.data.domain.Vector; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Param; @@ -41,6 +45,7 @@ * * @author Oliver Gierke * @author Jens Schauder + * @author Mark Paluch * @soundtrack Elephants Crossing - We are (Irrelephant) */ @ExtendWith(SpringExtension.class) @@ -78,6 +83,52 @@ void doesNotApplyLikeExpansionOnNonStringProperties() throws Exception { assertThat(binding.prepare(1)).isEqualTo(1); } + @Test // GH- + void appliesScoreValuePreparation() throws Exception { + + ParameterMetadataProvider provider = createProvider( + Sample.class.getMethod("findByVectorWithin", Vector.class, Score.class)); + ParameterBinding.PartTreeParameterBinding vector = provider.next(new Part("VectorWithin", WithVector.class)); + ParameterBinding.PartTreeParameterBinding score = provider.next(new Part("VectorWithin", WithVector.class)); + ParameterMetadataProvider.ScoreParameterBinding binding = provider.normalize(score, SimilarityNormalizer.EUCLIDEAN); + + assertThat(binding.prepare(Score.of(1))).isEqualTo(0.0); + assertThat(binding.prepare(Score.of(0.5))).isEqualTo(1.0); + assertThat(provider.getBindings()).hasSize(2).contains(binding).doesNotContain(score); + } + + @Test // GH- + void appliesLowerRangeValuePreparation() throws Exception { + + ParameterMetadataProvider provider = createProvider( + Sample.class.getMethod("findByVectorWithin", Vector.class, Range.class)); + ParameterBinding.PartTreeParameterBinding vector = provider.next(new Part("VectorWithin", WithVector.class)); + ParameterBinding.PartTreeParameterBinding score = provider.next(new Part("VectorWithin", WithVector.class)); + ParameterMetadataProvider.ScoreParameterBinding lower = provider.lower(score, SimilarityNormalizer.EUCLIDEAN); + + Range range = Similarity.between(0.5, 1); + + assertThat(lower.prepare(range)).isEqualTo(1.0); + assertThat(provider.getBindings()).hasSize(2).contains(lower).doesNotContain(score); + } + + @Test // GH- + void appliesRangeValuePreparation() throws Exception { + + ParameterMetadataProvider provider = createProvider( + Sample.class.getMethod("findByVectorWithin", Vector.class, Range.class)); + ParameterBinding.PartTreeParameterBinding vector = provider.next(new Part("VectorWithin", WithVector.class)); + ParameterBinding.PartTreeParameterBinding score = provider.next(new Part("VectorWithin", WithVector.class)); + ParameterMetadataProvider.ScoreParameterBinding lower = provider.lower(score, SimilarityNormalizer.EUCLIDEAN); + ParameterMetadataProvider.ScoreParameterBinding upper = provider.upper(score, SimilarityNormalizer.EUCLIDEAN); + + Range range = Similarity.between(0.5, 1); + + assertThat(lower.prepare(range)).isEqualTo(1.0); + assertThat(upper.prepare(range)).isEqualTo(0.0); + assertThat(provider.getBindings()).hasSize(3).contains(lower, upper).doesNotContain(score); + } + private ParameterMetadataProvider createProvider(Method method) { JpaParameters parameters = new JpaParameters(ParametersSource.of(method)); @@ -102,5 +153,13 @@ interface Sample { User findByLastname(String lastname); User findByAgeContaining(@Param("age") Integer age); + + User findByVectorWithin(Vector vector, Score score); + + User findByVectorWithin(Vector vector, Range score); + } + + static class WithVector { + Vector vector; } } 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 4ad41bfd14..30895c94b3 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 @@ -61,25 +61,4 @@ void errorMessageMentionsParametersWhenParametersAreExhausted() { .withMessageContaining("parameter"); } - @Test // GH-3137 - 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"); - assertThat(createParameterMetadata(Part.Type.CONTAINING).prepare("containing")).isEqualTo("%containing%"); - assertThat(createParameterMetadata(Part.Type.NOT_CONTAINING).prepare("not containing")) - .isEqualTo("%not containing%"); - assertThat(createParameterMetadata(Part.Type.LIKE).prepare("%like%")).isEqualTo("%like%"); - assertThat(createParameterMetadata(Part.Type.IS_NULL).prepare(null)).isEqualTo(null); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private ParameterMetadataProvider.ParameterMetadata createParameterMetadata(Part.Type partType) { - - when(part.getType()).thenReturn(partType); - 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/SimilarityNormalizerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimilarityNormalizerUnitTests.java new file mode 100644 index 0000000000..37f08ef12b --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimilarityNormalizerUnitTests.java @@ -0,0 +1,76 @@ +/* + * 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 org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SimilarityNormalizer}. + * + * @author Mark Paluch + */ +class SimilarityNormalizerUnitTests { + + @Test + void normalizesEuclidean() { + + assertThat(SimilarityNormalizer.EUCLIDEAN.getSimilarity(0)).isCloseTo(1.0, offset(0.01)); + assertThat(SimilarityNormalizer.EUCLIDEAN.getSimilarity(0.223606791085977)).isCloseTo(0.9523810148239136, + offset(0.01)); + assertThat(SimilarityNormalizer.EUCLIDEAN.getSimilarity(1.1618950141221271)).isCloseTo(0.42553189396858215, + offset(0.01)); + + assertThat(SimilarityNormalizer.EUCLIDEAN.getScore(1.0)).isCloseTo(0.0, offset(0.01)); + assertThat(SimilarityNormalizer.EUCLIDEAN.getScore(0.9523810148239136)).isCloseTo(0.223606791085977, offset(0.01)); + assertThat(SimilarityNormalizer.EUCLIDEAN.getScore(0.42553189396858215)).isCloseTo(1.1618950141221271, + offset(0.01)); + } + + @Test + void normalizesCosine() { + + assertThat(SimilarityNormalizer.COSINE.getSimilarity(0)).isCloseTo(1.0, offset(0.01)); + assertThat(SimilarityNormalizer.COSINE.getSimilarity(0.004470301418728173)).isCloseTo(0.9977648258209229, + offset(0.01)); + assertThat(SimilarityNormalizer.COSINE.getSimilarity(0.05568200370295473)).isCloseTo(0.9721590280532837, + offset(0.01)); + + assertThat(SimilarityNormalizer.COSINE.getScore(1.0)).isCloseTo(0.0, offset(0.01)); + assertThat(SimilarityNormalizer.COSINE.getScore(0.9977648258209229)).isCloseTo(0.004470301418728173, offset(0.01)); + assertThat(SimilarityNormalizer.COSINE.getScore(0.9721590280532837)).isCloseTo(0.05568200370295473, offset(0.01)); + } + + @Test + void normalizesNegativeInnerProduct() { + + assertThat(SimilarityNormalizer.DOT_PRODUCT.getSimilarity(-0.8465620279312134)).isCloseTo(0.9232810139656067, + offset(0.01)); + assertThat(SimilarityNormalizer.DOT_PRODUCT.getSimilarity(-1.0626180171966553)).isCloseTo(1.0313090085983276, + offset(0.01)); + assertThat(SimilarityNormalizer.DOT_PRODUCT.getSimilarity(-2.0293400287628174)).isCloseTo(1.5146700143814087, + offset(0.01)); + + assertThat(SimilarityNormalizer.DOT_PRODUCT.getScore(0.9232810139656067)).isCloseTo(-0.8465620279312134, + offset(0.01)); + assertThat(SimilarityNormalizer.DOT_PRODUCT.getScore(1.0313090085983276)).isCloseTo(-1.0626180171966553, + offset(0.01)); + assertThat(SimilarityNormalizer.DOT_PRODUCT.getScore(1.5146700143814087)).isCloseTo(-2.0293400287628174, + offset(0.01)); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/StoredProcedureConfigSupport.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/TestcontainerConfigSupport.java similarity index 74% rename from spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/StoredProcedureConfigSupport.java rename to spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/TestcontainerConfigSupport.java index 998245126b..505ad4b089 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/procedures/StoredProcedureConfigSupport.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/TestcontainerConfigSupport.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,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.procedures; +package org.springframework.data.jpa.repository.support; import jakarta.persistence.EntityManagerFactory; +import java.util.Collection; +import java.util.Collections; import java.util.Properties; import javax.sql.DataSource; @@ -29,6 +31,8 @@ import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; @@ -39,12 +43,12 @@ * * @author Mark Paluch */ -class StoredProcedureConfigSupport { +public class TestcontainerConfigSupport { private final Class dialect; private final Resource initScript; - StoredProcedureConfigSupport(Class dialect, Resource initScript) { + protected TestcontainerConfigSupport(Class dialect, Resource initScript) { this.dialect = dialect; this.initScript = initScript; } @@ -67,16 +71,36 @@ AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { factoryBean.setDataSource(dataSource); factoryBean.setPersistenceUnitRootLocation("simple-persistence"); factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); - factoryBean.setPackagesToScan(this.getClass().getPackage().getName()); + + factoryBean.setManagedTypes(getManagedTypes()); + factoryBean.setPackagesToScan(getPackagesToScan().toArray(new String[0])); + factoryBean.setManagedClassNameFilter(getManagedClassNameFilter()); Properties properties = new Properties(); - properties.setProperty("hibernate.hbm2ddl.auto", "create"); + properties.setProperty("hibernate.hbm2ddl.auto", getSchemaAction()); properties.setProperty("hibernate.dialect", dialect.getCanonicalName()); + factoryBean.setJpaProperties(properties); return factoryBean; } + protected String getSchemaAction() { + return "create"; + } + + protected PersistenceManagedTypes getManagedTypes() { + return null; + } + + protected Collection getPackagesToScan() { + return Collections.emptyList(); + } + + protected ManagedClassNameFilter getManagedClassNameFilter() { + return className -> true; + } + @Bean PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); diff --git a/spring-data-jpa/src/test/resources/scripts/oracle-vector-initialize.sql b/spring-data-jpa/src/test/resources/scripts/oracle-vector-initialize.sql new file mode 100644 index 0000000000..23a69dddb7 --- /dev/null +++ b/spring-data-jpa/src/test/resources/scripts/oracle-vector-initialize.sql @@ -0,0 +1,11 @@ +-- Exit on any errors +WHENEVER SQLERROR EXIT SQL.SQLCODE + +-- Configure the size of the Vector Pool to 1 GiB. +ALTER SYSTEM SET vector_memory_size = 1G SCOPE=SPFILE; + +SHUTDOWN +ABORT; +STARTUP; + +exit; diff --git a/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql b/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql new file mode 100644 index 0000000000..2d0bf06de4 --- /dev/null +++ b/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS with_vector;; + +CREATE TABLE IF NOT EXISTS with_vector +( + id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY, + country varchar2(10), + description varchar2(10), + the_embedding vector(5, FLOAT32) annotations(Distance 'COSINE', IndexType 'IVF') +);; + +create +vector index if not exists vector_index_1 on with_vector (the_embedding) + organization neighbor partitions + distance COSINE +with target accuracy 95 + parameters (type IVF, neighbor partitions 10);; diff --git a/spring-data-jpa/src/test/resources/scripts/pgvector.sql b/spring-data-jpa/src/test/resources/scripts/pgvector.sql new file mode 100644 index 0000000000..4057dd9528 --- /dev/null +++ b/spring-data-jpa/src/test/resources/scripts/pgvector.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION IF NOT EXISTS vector; + +DROP TABLE IF EXISTS with_vector; + +CREATE TABLE IF NOT EXISTS with_vector (id bigserial PRIMARY KEY,country varchar(10), description varchar(10),the_embedding vector(5)); + +CREATE INDEX ON with_vector USING hnsw (the_embedding vector_l2_ops); diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 351c162366..126f33c4af 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -14,6 +14,7 @@ ** xref:jpa/stored-procedures.adoc[] ** xref:jpa/specifications.adoc[] ** xref:repositories/query-by-example.adoc[] +** xref:repositories/vector-search.adoc[] ** xref:jpa/transactions.adoc[] ** xref:jpa/locking.adoc[] ** xref:auditing.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc new file mode 100644 index 0000000000..f33e2ad4d3 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc @@ -0,0 +1,8 @@ +:vector-search-intro-include: data-jpa::partial$vector-search-intro-include.adoc +:vector-search-model-include: data-jpa::partial$vector-search-model-include.adoc +:vector-search-repository-include: data-jpa::partial$vector-search-repository-include.adoc +:vector-search-scoring-include: data-jpa::partial$vector-search-scoring-include.adoc +:vector-search-method-derived-include: data-jpa::partial$vector-search-method-derived-include.adoc +:vector-search-method-annotated-include: data-jpa::partial$vector-search-method-annotated-include.adoc + +include::{commons}@data-commons::page$repositories/vector-search.adoc[] diff --git a/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc new file mode 100644 index 0000000000..2c255297f4 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc @@ -0,0 +1,32 @@ +To use Hibernate Vector Search, you need to add the following dependencies to your project. + +The following example shows how to set up dependencies in Maven and Gradle: + +[tabs] +====== +Maven:: ++ +[source,xml,indent=0,subs="verbatim,quotes",role="primary"] +---- + + + org.hibernate.orm + hibernate-vector + ${hibernate.version} + + +---- + +Gradle:: ++ +==== +[source,groovy,indent=0,subs="verbatim,quotes",role="secondary"] +---- +dependencies { + implementation 'org.hibernate.orm:hibernate-vector:${hibernateVersion}' +} +---- +==== +====== + +NOTE: While you can use `Vector` as type for queries, you cannot use it in your domain model as Hibernate requires float or double arrays as vector types. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc new file mode 100644 index 0000000000..8b27401c75 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc @@ -0,0 +1,28 @@ +Annotated search methods must define the entire JPQL query to run a Vector Search. + +.Using `@Query` Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @Query(""" + SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c + WHERE c.country = ?1 + AND cosine_distance(c.embedding, :embedding) <= :distance + ORDER BY distance asc""") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + Score distance); + + @Query(""" + SELECT c FROM Comment c + WHERE c.country = ?1 + AND cosine_distance(c.embedding, :embedding) <= :distance + ORDER BY cosine_distance(c.embedding, :embedding) asc""") + List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); +} +---- +==== + +Vector Search methods are not required to include a score or distance in their projection. +When using annotated search methods returning `SearchResults`, the execution mechanism assumes that if a second projection column is present that this one holds the score value. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc new file mode 100644 index 0000000000..9819837348 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc @@ -0,0 +1,16 @@ +.Using `Near` and `Within` Keywords in Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + SearchResults searchByEmbeddingNear(Vector vector, Score score); + + SearchResults searchByEmbeddingWithin(Vector vector, Range range); + + SearchResults searchByCountryAndEmbeddingWithin(String country, Vector vector, Range range); +} +---- +==== + +Derived search methods can declare predicates on domain model attributes and Vector parameters. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc new file mode 100644 index 0000000000..a6966630c2 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc @@ -0,0 +1,18 @@ +==== +[source,java] +---- +class Comment { + + @Id String id; + String country; + String comment; + + @Column(name = "the_embedding") + @JdbcTypeCode(SqlTypes.VECTOR) + @Array(length = 5) + Vector embedding; + + // getters, setters, … +} +---- +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc new file mode 100644 index 0000000000..716bf5a562 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc @@ -0,0 +1,21 @@ +.Using `SearchResult` in a Repository Search Method +==== +[source,java] +---- +interface CommentRepository extends Repository { + + SearchResults searchByCountryAndEmbeddingNear(String country, Vector vector, Score distance, + Limit limit); + + @Query(""" + SELECT c, cosine_distance(c.embedding, :embedding) as distance FROM Comment c + WHERE c.country = ?1 + AND cosine_distance(c.embedding, :embedding) <= :distance + ORDER BY distance asc""") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + Score distance); +} + +SearchResults results = repository.searchByCountryAndEmbeddingNear("en", Vector.of(…), Score.of(0.9), Limit.of(10)); +---- +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc new file mode 100644 index 0000000000..4cd793dc91 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc @@ -0,0 +1,38 @@ +Hibernate translates distance function calls to native database functions for PGvector and Oracle. +Their result is typically a distance. +When using `Similarity` instead of `Score`, Spring Data normalizes distance scores into a similarity score between 0 and 1. The higher the score, the more similar the two vectors are. +// END + +.Using `Score` and `Similarity` in a Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + SearchResults searchByEmbeddingNear(Vector vector, ScoringFunction function); + + SearchResults searchByEmbeddingNear(Vector vector, Score score); + + SearchResults searchByEmbeddingNear(Vector vector, Similarity similarity); + + SearchResults searchByEmbeddingNear(Vector vector, Range range); +} + +repository.searchByEmbeddingNear(Vector.of(…), ScoringFunction.cosine()); <1> + +repository.searchByEmbeddingNear(Vector.of(…), Score.of(0.9, ScoringFunction.cosine())); <2> + +repository.searchByEmbeddingNear(Vector.of(…), Similarity.of(0.9, ScoringFunction.cosine())); <3> + +repository.searchByEmbeddingNear(Vector.of(…), Similarity.between(0.5, 1, ScoringFunction.euclidean()));<4> +---- + +<1> Run a search and return results that are similar to the given `Vector` applying Cosine scoring. +<2> Run a search and return results with a score of `0.9` or smaller using the Cosine distance. +<3> Run a search and normalize the score into a similarity value. +Return results with a similarity of `0.9` or greater using Cosine scoring. +<4> Run a search and normalize the score into a similarity value. +Return results with a similarity of between `0.5` and `1.0` or greater using Euclidean scoring. +==== + +NOTE: JPA requires a `ScoringFunction` to be provided when creating `Score` or `Similarity` instances to select a scoring function. From e802143bafdc900d88edcde25546cea4e9055780 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 8 May 2025 12:11:16 +0200 Subject: [PATCH 089/224] Polishing. Original Pull Request: #3868 --- .../query/EmptyIntrospectedQuery.java | 3 +- .../query/JSqlParserQueryEnhancer.java | 9 ++- .../repository/query/JpaQueryExecution.java | 1 + .../repository/query/ParameterBinding.java | 4 -- .../query/ParameterMetadataProvider.java | 12 +++- .../AbstractVectorIntegrationTests.java | 65 ++++++++++++++----- .../test/resources/scripts/oracle-vector.sql | 1 + .../src/test/resources/scripts/pgvector.sql | 2 +- ...ector-search-method-annotated-include.adoc | 4 +- .../vector-search-repository-include.adoc | 2 +- 10 files changed, 71 insertions(+), 32 deletions(-) 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 188b0b8c23..a7336b98de 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 @@ -34,8 +34,6 @@ enum EmptyIntrospectedQuery implements EntityQuery { EmptyIntrospectedQuery() {} - - @Override public boolean hasParameterBindings() { return false; @@ -61,6 +59,7 @@ public List getParameterBindings() { } @Override + @SuppressWarnings("NullAway") public T doWithEnhancer(Function function) { return null; } 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 4b17555c55..b340d49ce4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -15,8 +15,9 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*; -import static org.springframework.data.jpa.repository.query.QueryUtils.*; +import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlCount; +import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlLower; +import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; @@ -52,7 +53,6 @@ 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; @@ -356,6 +356,8 @@ private String doApplySorting(Sort sort, @Nullable String alias) { private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullable String alias) { + Assert.notNull(selectStatement, "SelectStatement must not be null"); + if (selectStatement instanceof SetOperationList setOperationList) { return applySortingToSetOperationList(setOperationList, sort); } @@ -381,6 +383,7 @@ private String applySorting(@Nullable Select selectStatement, Sort sort, @Nullab } @Override + @SuppressWarnings("NullAway") public String createCountQueryFor(@Nullable String countProjection) { if (this.parsedType != ParsedType.SELECT) { 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 c157161683..b0e7c49a4f 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 @@ -295,6 +295,7 @@ private long count(Query resultQuery, AbstractJpaQuery repositoryQuery, JpaParam return provider.getResultCount(resultQuery, () -> doCount(repositoryQuery, accessor)); } + @SuppressWarnings("NullAway") long doCount(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) { List totals = repositoryQuery.createCountQuery(accessor).getResultList(); 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 ac5462175b..443b6ca3ce 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 @@ -323,10 +323,6 @@ public boolean isIsNullParameter() { return Collections.singleton(value); } - - public String lower() { - return null; - } } /** 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 b1c08fc58c..968751704e 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 @@ -120,7 +120,7 @@ private ParameterMetadataProvider(@Nullable Iterator bindableParameterVa this.templates = templates; } - public JpaParameters getParameters() { + JpaParameters getParameters() { return this.jpaParameters; } @@ -216,6 +216,10 @@ private PartTreeParameterBinding next(Part part, Class type, Parameter pa return binding; } + /** + * @return the scoring function if available {@link ScoringFunction#unspecified()} by default. + * @since 4.0 + */ ScoringFunction getScoringFunction() { if (accessor != null) { @@ -225,6 +229,12 @@ ScoringFunction getScoringFunction() { return ScoringFunction.unspecified(); } + /** + * + * @return the vector binding identifier. + * @throws IllegalStateException if parameters do not cotain + * @since 4.0 + */ ParameterBinding getVectorBinding() { if (!getParameters().hasVectorParameter()) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java index f4c334d39a..71538f9dff 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AbstractVectorIntegrationTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -36,7 +36,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Range; import org.springframework.data.domain.Score; @@ -53,6 +52,7 @@ * Testcase to verify Vector Search work with Hibernate. * * @author Mark Paluch + * @author Christoph Strobl */ @Transactional @Rollback(value = false) @@ -65,10 +65,11 @@ abstract class AbstractVectorIntegrationTests { @BeforeEach void setUp() { - WithVector w1 = new WithVector("de", "one", new float[] { 0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f }); - WithVector w2 = new WithVector("de", "two", new float[] { 0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f }); - WithVector w3 = new WithVector("en", "three", new float[] { 0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f }); - WithVector w4 = new WithVector("de", "four", new float[] { 0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f }); + WithVector w1 = new WithVector("de", "one", "d1", new float[] { 0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f }); + WithVector w2 = new WithVector("de", "two", "d2", new float[] { 0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f }); + WithVector w3 = new WithVector("en", "three", "d3", + new float[] { 0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f }); + WithVector w4 = new WithVector("de", "four", "d4", new float[] { 0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f }); repository.deleteAllInBatch(); repository.saveAllAndFlush(Arrays.asList(w1, w2, w3, w4)); @@ -93,7 +94,7 @@ static Set scoringFunctions() { VectorScoringFunctions.EUCLIDEAN); } - @Test + @Test // GH-3868 void shouldNormalizeEuclideanSimilarity() { SearchResults results = repository.searchTop5ByCountryAndEmbeddingWithin("de", VECTOR, @@ -108,7 +109,16 @@ void shouldNormalizeEuclideanSimilarity() { assertThat(two.getScore().getValue()).isGreaterThan(0.99); } - @Test + @Test // GH-3868 + void orderTargetsProperty() { + + SearchResults results = repository.searchTop5ByCountryAndEmbeddingWithinOrderByDistance("de", VECTOR, + Similarity.of(0, VectorScoringFunctions.EUCLIDEAN)); + + assertThat(results.getContent()).extracting(it -> it.getContent().getDistance()).containsExactly("d1", "d2", "d4"); + } + + @Test// GH-3868 void shouldNormalizeCosineSimilarity() { SearchResults results = repository.searchTop5ByCountryAndEmbeddingWithin("de", VECTOR, @@ -123,7 +133,7 @@ void shouldNormalizeCosineSimilarity() { assertThat(two.getScore().getValue()).isGreaterThan(0.99); } - @Test + @Test // GH-3868 void shouldRunStringQuery() { List results = repository.findAnnotatedByCountryAndEmbeddingWithin("de", VECTOR, @@ -133,7 +143,7 @@ void shouldRunStringQuery() { assertThat(results).extracting(WithVector::getDescription).containsSequence("two", "one", "four"); } - @Test + @Test // GH-3868 void shouldRunStringQueryWithDistance() { SearchResults results = repository.searchAnnotatedByCountryAndEmbeddingWithin("de", VECTOR, @@ -149,7 +159,7 @@ void shouldRunStringQueryWithDistance() { assertThat(result.getScore().getFunction()).isEqualTo(VectorScoringFunctions.COSINE); } - @Test + @Test // GH-3868 void shouldRunStringQueryWithFloatDistance() { SearchResults results = repository.searchAnnotatedByCountryAndEmbeddingWithin("de", VECTOR, 2); @@ -164,7 +174,7 @@ void shouldRunStringQueryWithFloatDistance() { assertThat(result.getScore().getFunction()).isEqualTo(ScoringFunction.unspecified()); } - @Test + @Test // GH-3868 void shouldApplyVectorSearchWithRange() { SearchResults results = repository.searchAllByCountryAndEmbeddingWithin("de", VECTOR, @@ -176,7 +186,7 @@ void shouldApplyVectorSearchWithRange() { .containsSequence("two", "one", "four"); } - @Test + @Test // GH-3868 void shouldApplyVectorSearchAndReturnList() { List results = repository.findAllByCountryAndEmbeddingWithin("de", VECTOR, @@ -186,7 +196,7 @@ void shouldApplyVectorSearchAndReturnList() { assertThat(results).extracting(WithVector::getDescription).containsSequence("one", "two", "four"); } - @Test + @Test // GH-3868 void shouldProjectVectorSearchAsInterface() { SearchResults results = repository.searchInterfaceProjectionByCountryAndEmbeddingWithin("de", @@ -196,7 +206,7 @@ void shouldProjectVectorSearchAsInterface() { .containsSequence("two", "one", "four"); } - @Test + @Test // GH-3868 void shouldProjectVectorSearchAsDto() { SearchResults results = repository.searchDtoByCountryAndEmbeddingWithin("de", VECTOR, @@ -206,7 +216,7 @@ void shouldProjectVectorSearchAsDto() { .containsSequence("two", "one", "four"); } - @Test + @Test // GH-3868 void shouldProjectVectorSearchDynamically() { SearchResults dtos = repository.searchDynamicByCountryAndEmbeddingWithin("de", VECTOR, @@ -233,16 +243,19 @@ public static class WithVector { private String country; private String description; + private String distance; + @Column(name = "the_embedding") @JdbcTypeCode(SqlTypes.VECTOR) @Array(length = 5) private float[] embedding; public WithVector() {} - public WithVector(String country, String description, float[] embedding) { + public WithVector(String country, String description, String distance, float[] embedding) { this.country = country; this.description = description; this.embedding = embedding; + this.distance = distance; } public Integer getId() { @@ -273,9 +286,22 @@ public void setEmbedding(float[] embedding) { this.embedding = embedding; } + public void setDescription(String description) { + this.description = description; + } + + public String getDistance() { + return distance; + } + + public void setDistance(String distance) { + this.distance = distance; + } + @Override public String toString() { - return "WithVector{" + "country='" + country + '\'' + ", description='" + description + '\'' + '}'; + return "WithVector{" + "id=" + id + ", country='" + country + '\'' + ", description='" + description + '\'' + + ", distance='" + distance + '\'' + ", embedding=" + Arrays.toString(embedding) + '}'; } } @@ -328,6 +354,9 @@ SearchResults searchAllByCountryAndEmbeddingWithin(String country, V SearchResults searchTop5ByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); + SearchResults searchTop5ByCountryAndEmbeddingWithinOrderByDistance(String country, Vector embedding, + Score distance); + SearchResults searchInterfaceProjectionByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); diff --git a/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql b/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql index 2d0bf06de4..f11fb13fc3 100644 --- a/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql +++ b/spring-data-jpa/src/test/resources/scripts/oracle-vector.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS with_vector id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY, country varchar2(10), description varchar2(10), + distance varchar2(10), the_embedding vector(5, FLOAT32) annotations(Distance 'COSINE', IndexType 'IVF') );; diff --git a/spring-data-jpa/src/test/resources/scripts/pgvector.sql b/spring-data-jpa/src/test/resources/scripts/pgvector.sql index 4057dd9528..b91725750d 100644 --- a/spring-data-jpa/src/test/resources/scripts/pgvector.sql +++ b/spring-data-jpa/src/test/resources/scripts/pgvector.sql @@ -2,6 +2,6 @@ CREATE EXTENSION IF NOT EXISTS vector; DROP TABLE IF EXISTS with_vector; -CREATE TABLE IF NOT EXISTS with_vector (id bigserial PRIMARY KEY,country varchar(10), description varchar(10),the_embedding vector(5)); +CREATE TABLE IF NOT EXISTS with_vector (id bigserial PRIMARY KEY,country varchar(10), description varchar(10), distance varchar(10), the_embedding vector(5)); CREATE INDEX ON with_vector USING hnsw (the_embedding vector_l2_ops); diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc index 8b27401c75..851457e68d 100644 --- a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc @@ -11,7 +11,7 @@ interface CommentRepository extends Repository { WHERE c.country = ?1 AND cosine_distance(c.embedding, :embedding) <= :distance ORDER BY distance asc""") - SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); @Query(""" @@ -19,7 +19,7 @@ interface CommentRepository extends Repository { WHERE c.country = ?1 AND cosine_distance(c.embedding, :embedding) <= :distance ORDER BY cosine_distance(c.embedding, :embedding) asc""") - List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); + List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); } ---- ==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc index 716bf5a562..8955bafe89 100644 --- a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc +++ b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc @@ -12,7 +12,7 @@ interface CommentRepository extends Repository { WHERE c.country = ?1 AND cosine_distance(c.embedding, :embedding) <= :distance ORDER BY distance asc""") - SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); } From 8556a7b96873ddd6d44b1508f0036f1483909696 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 08:54:44 +0200 Subject: [PATCH 090/224] Update CI Properties. See #3854 --- ci/pipeline.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 8dd2295acc..cde4a8e881 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,5 +1,5 @@ # Java versions -java.main.tag=17.0.15_6-jdk-focal +java.main.tag=24.0.1_9-jdk-noble java.next.tag=24.0.1_9-jdk-noble # Docker container images - standard @@ -14,7 +14,6 @@ docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis docker.redis.6.version=6.2.13 docker.redis.7.version=7.2.4 -docker.valkey.8.version=8.1.1 # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home From c96bf905b20f285b571d237b2303a31ef2cd52a8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 09:27:12 +0200 Subject: [PATCH 091/224] Upgrade to Oracle OJDBC 23.8.0.25.04. Closes #3881 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9bc676e471..47926932c4 100755 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 5.2 9.2.0 42.7.5 - 23.7.0.25.01 + 23.8.0.25.04 4.0.0-SNAPSHOT 0.10.3 From 50113038aa90585b8fb9879c567bf0fbcf01a66b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 09:54:51 +0200 Subject: [PATCH 092/224] Fix EQL and JPQL LIKE with ESCAPE clause parsing. Closes #3873 --- .../data/jpa/repository/query/HqlQueryRendererTests.java | 3 --- 1 file changed, 3 deletions(-) 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 ea44e5a326..7d30338ebe 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,9 +19,6 @@ 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; import org.junit.jupiter.params.provider.Arguments; From 6bffef2a58836592d68a28a9e8b3d0941b306b54 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 11:31:13 +0200 Subject: [PATCH 093/224] Polishing. See #3868 --- src/main/antora/antora-playbook.yml | 2 +- .../modules/ROOT/pages/jpa/query-methods.adoc | 2 +- .../pages/repositories/vector-search.adoc | 14 +- .../modules/ROOT/partials/vector-search.adoc | 167 ++++++++++++++++++ 4 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 src/main/antora/modules/ROOT/partials/vector-search.adoc diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml index 04dbefb29a..5465baed5d 100644 --- a/src/main/antora/antora-playbook.yml +++ b/src/main/antora/antora-playbook.yml @@ -17,7 +17,7 @@ content: - url: https://github.com/spring-projects/spring-data-commons # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: [ main, 3.4.x ] + branches: [ 4.0.x ] start_path: src/main/antora asciidoc: attributes: 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 b947fca73f..1d8ff7f9a2 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -428,7 +428,7 @@ This is a lighter variant than paging because it does not require the total resu 3. <>. This method avoids https://use-the-index-luke.com/no-offset[the shortcomings of offset-based result retrieval by leveraging database indexes]. -Read more on <> for your particular arrangement. +Read more on xref:repositories/query-methods-details.adoc#repositories.scrolling.guidance[which method to use best] for your particular arrangement. You can use the Scroll API with query methods, xref:repositories/query-by-example.adoc[Query-by-Example], and xref:repositories/core-extensions.adoc#core.extensions.querydsl[Querydsl]. diff --git a/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc index f33e2ad4d3..8821057b30 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc @@ -1,8 +1,8 @@ -:vector-search-intro-include: data-jpa::partial$vector-search-intro-include.adoc -:vector-search-model-include: data-jpa::partial$vector-search-model-include.adoc -:vector-search-repository-include: data-jpa::partial$vector-search-repository-include.adoc -:vector-search-scoring-include: data-jpa::partial$vector-search-scoring-include.adoc -:vector-search-method-derived-include: data-jpa::partial$vector-search-method-derived-include.adoc -:vector-search-method-annotated-include: data-jpa::partial$vector-search-method-annotated-include.adoc +:vector-search-intro-include: partial$vector-search-intro-include.adoc +:vector-search-model-include: partial$vector-search-model-include.adoc +:vector-search-repository-include: partial$vector-search-repository-include.adoc +:vector-search-scoring-include: partial$vector-search-scoring-include.adoc +:vector-search-method-derived-include: partial$vector-search-method-derived-include.adoc +:vector-search-method-annotated-include: partial$vector-search-method-annotated-include.adoc -include::{commons}@data-commons::page$repositories/vector-search.adoc[] +include::partial$vector-search.adoc[] diff --git a/src/main/antora/modules/ROOT/partials/vector-search.adoc b/src/main/antora/modules/ROOT/partials/vector-search.adoc new file mode 100644 index 0000000000..15e32dccee --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search.adoc @@ -0,0 +1,167 @@ +[[vector-search]] += Vector Search + +With the rise of Generative AI, Vector databases have gained strong traction in the world of databases. +These databases enable efficient storage and querying of high-dimensional vectors, making them well-suited for tasks such as semantic search, recommendation systems, and natural language understanding. + +Vector search is a technique that retrieves semantically similar data by comparing vector representations (also known as embeddings) rather than relying on traditional exact-match queries. +This approach enables intelligent, context-aware applications that go beyond keyword-based retrieval. + +In the context of Spring Data, vector search opens new possibilities for building intelligent, context-aware applications, particularly in domains like natural language processing, recommendation systems, and generative AI. +By modelling vector-based querying using familiar repository abstractions, Spring Data allows developers to seamlessly integrate similarity-based vector-capable databases with the simplicity and consistency of the Spring Data programming model. + +ifdef::vector-search-intro-include[] +include::{vector-search-intro-include}[] +endif::[] + +[[vector-search.model]] +== Vector Model + +To support vector search in a type-safe and idiomatic way, Spring Data introduces the following core abstractions: + +* <> +* <` and `SearchResult`>> +* <> + +[[vector-search.model.vector]] +=== `Vector` + +The `Vector` type represents an n-dimensional numerical embedding, typically produced by embedding models. +In Spring Data, it is defined as a lightweight wrapper around an array of floating-point numbers, ensuring immutability and consistency. +This type can be used as an input for search queries or as a property on a domain entity to store the associated vector representation. + +==== +[source,java] +---- +Vector vector = Vector.of(0.23f, 0.11f, 0.77f); +---- +==== + +Using `Vector` in your domain model removes the need to work with raw arrays or lists of numbers, providing a more type-safe and expressive way to handle vector data. +This abstraction also allows for easy integration with various vector databases and libraries. +It also allows for implementing vendor-specific optimizations such as binary or quantized vectors that do not map to a standard floating point (`float` and `double` as of https://en.wikipedia.org/wiki/IEEE_754[IEEE 754]) representation. +A domain object can have a vector property, which can be used for similarity searches. +Consider the following example: + +ifdef::vector-search-model-include[] +include::{vector-search-model-include}[] +endif::[] + +NOTE: Associating a vector with a domain object results in the vector being loaded and stored as part of the entity lifecycle, which may introduce additional overhead on retrieval and persistence operations. + +[[vector-search.model.search-result]] +=== Search Results + +The `SearchResult` type encapsulates the results of a vector similarity query. +It includes both the matched domain object and a relevance score that indicates how closely it matches the query vector. +This abstraction provides a structured way to handle result ranking and enables developers to easily work with both the data and its contextual relevance. + +ifdef::vector-search-repository-include[] +include::{vector-search-repository-include}[] +endif::[] + +In this example, the `searchByCountryAndEmbeddingNear` method returns a `SearchResults` object, which contains a list of `SearchResult` instances. +Each result includes the matched `Comment` entity and its relevance score. + +Relevance score is a numerical value that indicates how closely the matched vector aligns with the query vector. +Depending on whether a score represents distance or similarity a higher score can mean a closer match or a more distant one. + +The scoring function used to calculate this score can vary based on the underlying database, index or input parameters. + +[[vector-search.model.scoring]] +=== Score, Similarity, and Scoring Functions + +The `Score` type holds a numerical value indicating the relevance of a search result. +It can be used to rank results based on their similarity to the query vector. +The `Score` type is typically a floating-point number, and its interpretation (higher is better or lower is better) depends on the specific similarity function used. +Scores are a by-product of vector search and are not required for a successful search operation. +Score values are not part of a domain model and therefore represented best as out-of-band data. + +Generally, a Score is computed by a `ScoringFunction`. +The actual scoring function used to calculate this score can depends on the underlying database and can be obtained from a search index or input parameters. + +Spring Data support declares constants for commonly used functions such as: + +Euclidean Distance:: Calculates the straight-line distance in n-dimensional space involving the square root of the sum of squared differences. +Cosine Similarity:: Measures the angle between two vectors by calculating the Dot product first and then normalizing its result by dividing by the product of their lengths. +Dot Product:: Computes the sum of element-wise multiplications. + +The choice of similarity function can impact both the performance and semantics of the search and is often determined by the underlying database or index being used. +Spring Data adopts to the database's native scoring function capabilities and whether the score can be used to limit results. + +ifdef::vector-search-scoring-include[] +include::{vector-search-scoring-include}[] +endif::[] + +[[vector-search.methods]] +== Vector Search Methods + +Vector search methods are defined in repositories using the same conventions as standard Spring Data query methods. +These methods return `SearchResults` and require a `Vector` parameter to define the query vector. +The actual implementation depends on the actual internals of the underlying data store and its capabilities around vector search. + +NOTE: If you are new to Spring Data repositories, make sure to familiarize yourself with the xref:repositories/core-concepts.adoc[basics of repository definitions and query methods]. + +Generally, you have the choice of declaring a search method using two approaches: + +* Query Derivation +* Declaring a String-based Query + +Vector Search methods must declare a `Vector` parameter to define the query vector. + +[[vector-search.method.derivation]] +=== Derived Search Methods + +A derived search method uses the name of the method to derive the query. +Vector Search supports the following keywords to run a Vector search when declaring a search method: + +.Query predicate keywords +[options="header",cols="1,3"] +|=============== +|Logical keyword|Keyword expressions +|`NEAR`|`Near`, `IsNear` +|`WITHIN`|`Within`, `IsWithin` +|=============== + +ifdef::vector-search-method-derived-include[] +include::{vector-search-method-derived-include}[] +endif::[] + +Derived search methods are typically easier to read and maintain, as they rely on the method name to express the query intent. +However, a derived search method requires either to declare a `Score`, `Range` or `ScoreFunction` as second argument to the `Near`/`Within` keyword to limit search results by their score. + +[[vector-search.method.string]] +=== Annotated Search Methods + +Annotated methods provide full control over the query semantics and parameters. +Unlike derived methods, they do not rely on method name conventions. + +ifdef::vector-search-method-annotated-include[] +include::{vector-search-method-annotated-include}[] +endif::[] + +With more control over the actual query, Spring Data can make fewer assumptions about the query and its parameters. +For example, `Similarity` normalization uses the native score function within the query to normalize the given similarity into a score predicate value and vice versa. +If an annotated query does not define e.g. the score, then the score value in the returned `SearchResult` will be zero. + +[[vector-search.method.sorting]] +=== Sorting + +By default, search results are ordered according to their score. +You can override sorting by using the `Sort` parameter: + +.Using `Sort` in Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + SearchResults searchByEmbeddingNearOrderByCountry(Vector vector, Score score); + + SearchResults searchByEmbeddingWithin(Vector vector, Score score, Sort sort); +} +---- +==== + +Please note that custom sorting does not allow expressing the score as a sorting criteria. +You can only refer to domain properties. From 85fcbcd2fbb76bd64227fd3e9f8df47b634fb949 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 14:40:50 +0200 Subject: [PATCH 094/224] Support HQL `LIMIT`/`OFFSET` without ordering. Closes #3882 --- .../data/jpa/repository/query/HqlCountQueryTransformer.java | 4 ++-- .../data/jpa/repository/query/HqlSortedQueryTransformer.java | 4 ++-- 2 files changed, 4 insertions(+), 4 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 a5c034e5da..79dcfbc3a5 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 @@ -17,9 +17,9 @@ 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.HqlParser.SelectClauseContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; 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 ba95930d2c..675e3b394d 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 @@ -19,9 +19,9 @@ import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; From 2ebe4caba6f6f939cfb595bff7d1acd18a63bb44 Mon Sep 17 00:00:00 2001 From: oscarfanchin Date: Fri, 2 May 2025 15:38:12 +0200 Subject: [PATCH 095/224] Add support for Set-Returning Functions (SRF) to HQL parser and query rendering. Signed-off-by: oscarfanchin Closes: #3864 Original pull request: #3879 --- .../data/jpa/repository/query/Hql.g4 | 12 +- .../query/HibernateQueryInformation.java | 12 +- .../query/HqlCountQueryTransformer.java | 13 +- .../query/HqlQueryIntrospector.java | 10 +- .../repository/query/HqlQueryRenderer.java | 60 +++++ .../query/HqlSortedQueryTransformer.java | 14 + .../query/HqlQueryRendererTests.java | 251 ++++++++++++++++++ .../query/HqlQueryTransformerTests.java | 27 ++ 8 files changed, 395 insertions(+), 4 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 4ed7a44554..8ced23f280 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 @@ -114,8 +114,13 @@ joinSpecifier fromRoot : entityName variable? | LATERAL? '(' subquery ')' variable? + | functionCallAsFromSource variable? ; +functionCallAsFromSource + : identifier '(' (expression (',' expression)*)? ')' + ; + join : joinType JOIN FETCH? joinTarget joinRestriction? // Spec BNF says joinType isn't optional, but text says that it is. ; @@ -123,6 +128,11 @@ join joinTarget : path variable? # JoinPath | LATERAL? '(' subquery ')' variable? # JoinSubquery + | functionCallAsJoinTarget variable? # JoinFunctionCall + ; + +functionCallAsJoinTarget + : identifier '(' (expression (',' expression)*)? ')' ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-update @@ -1878,4 +1888,4 @@ ESCAPE_SEQUENCE QUOTED_IDENTIFIER : BACKTICK ( ESCAPE_SEQUENCE | '\\' BACKTICK | ~([`]) )* BACKTICK - ; + ; \ No newline at end of file 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 405fa08660..fd77f0ea90 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 @@ -23,19 +23,29 @@ * Hibernate-specific query details capturing common table expression details. * * @author Mark Paluch + * @author oscar.fanchin * @since 3.5 */ class HibernateQueryInformation extends QueryInformation { private final boolean hasCte; + + private final boolean hasFromFunction; + public HibernateQueryInformation(@Nullable String alias, List projection, - boolean hasConstructorExpression, boolean hasCte) { + boolean hasConstructorExpression, boolean hasCte,boolean hasFromFunction) { super(alias, projection, hasConstructorExpression); this.hasCte = hasCte; + this.hasFromFunction = hasFromFunction; } public boolean hasCte() { return hasCte; } + + public boolean hasFromFunction() { + return hasFromFunction; + } + } 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 79dcfbc3a5..3a85c59126 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 @@ -30,6 +30,7 @@ * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch + * @author oscar.fanchin * @since 3.1 */ @SuppressWarnings("ConstantValue") @@ -38,11 +39,13 @@ class HqlCountQueryTransformer extends HqlQueryRenderer { private final @Nullable String countProjection; private final @Nullable String primaryFromAlias; private final boolean containsCTE; + private final boolean containsFromFunction; HqlCountQueryTransformer(@Nullable String countProjection, HibernateQueryInformation queryInformation) { this.countProjection = countProjection; this.primaryFromAlias = queryInformation.getAlias(); this.containsCTE = queryInformation.hasCte(); + this.containsFromFunction = queryInformation.hasFromFunction(); } @Override @@ -156,11 +159,19 @@ public QueryRendererBuilder visitFromRoot(HqlParser.FromRootContext ctx) { builder.appendExpression(nested); + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); + } + } else if (ctx.functionCallAsFromSource() != null) { + + builder.appendExpression(visit(ctx.functionCallAsFromSource())); + if (ctx.variable() != null) { builder.appendExpression(visit(ctx.variable())); } } + return builder; } @@ -204,7 +215,7 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { } else { // with CTE primary alias fails with hibernate (WITH entities AS (…) SELECT count(c) FROM entities c) - if (containsCTE) { + if (containsCTE || containsFromFunction) { nested.append(QueryTokens.token("*")); } else { 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 d3ba055bb9..3c3f4b29ef 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 @@ -29,6 +29,7 @@ * {@link ParsedQueryIntrospector} for HQL queries. * * @author Mark Paluch + * @author oscar.fanchin */ @SuppressWarnings({ "UnreachableCode", "ConstantValue" }) class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIntrospector { @@ -40,11 +41,12 @@ class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIn private boolean projectionProcessed; private boolean hasConstructorExpression = false; private boolean hasCte = false; + private boolean hasFromFunction = false; @Override public HibernateQueryInformation getParsedQueryInformation() { return new HibernateQueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection, - hasConstructorExpression, hasCte); + hasConstructorExpression, hasCte, hasFromFunction); } @Override @@ -63,6 +65,12 @@ public Void visitCte(HqlParser.CteContext ctx) { this.hasCte = true; return super.visitCte(ctx); } + + @Override + public Void visitFunctionCallAsFromSource(HqlParser.FunctionCallAsFromSourceContext ctx) { + this.hasFromFunction = true; + return super.visitFunctionCallAsFromSource(ctx); + } @Override public Void visitFromRoot(HqlParser.FromRootContext ctx) { 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 1656d37082..a6b3dd4ca9 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 @@ -31,6 +31,7 @@ * * @author Greg Turnquist * @author Christoph Strobl + * @author Oscar Fanchin * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode", "UnreachableCode" }) @@ -63,6 +64,24 @@ public QueryTokenStream visitStart(HqlParser.StartContext ctx) { return visit(ctx.ql_statement()); } + @Override + public QueryTokenStream visitFunctionCallAsFromSource(HqlParser.FunctionCallAsFromSourceContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.identifier())); + + builder.append(TOKEN_OPEN_PAREN); + + if (!ctx.expression().isEmpty()) { + builder.append(QueryTokenStream.concatExpressions(ctx.expression(), this::visit, TOKEN_COMMA)); + } + + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + @Override public QueryTokenStream visitQl_statement(HqlParser.Ql_statementContext ctx) { @@ -376,6 +395,14 @@ public QueryTokenStream visitFromRoot(HqlParser.FromRootContext ctx) { builder.appendExpression(nested); + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); + } + + } else if (ctx.functionCallAsFromSource() != null) { + + builder.appendExpression(visit(ctx.functionCallAsFromSource())); + if (ctx.variable() != null) { builder.appendExpression(visit(ctx.variable())); } @@ -442,6 +469,39 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { return builder; } + @Override + public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.functionCallAsJoinTarget())); + + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); + } + + return builder; + + } + + @Override + public QueryTokenStream visitFunctionCallAsJoinTarget(HqlParser.FunctionCallAsJoinTargetContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.identifier())); + + builder.append(TOKEN_OPEN_PAREN); + + if (!ctx.expression().isEmpty()) { + builder.append(QueryTokenStream.concatExpressions(ctx.expression(), this::visit, TOKEN_COMMA)); + } + + builder.append(TOKEN_CLOSE_PAREN); + + return builder; + } + @Override public QueryTokenStream visitUpdateStatement(HqlParser.UpdateStatementContext ctx) { 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 675e3b394d..45074e5a47 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 @@ -32,6 +32,7 @@ * * @author Greg Turnquist * @author Christoph Strobl + * @author oscar.fanchin * @since 3.1 */ @SuppressWarnings("ConstantValue") @@ -122,6 +123,19 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { return tokens; } + + @Override + public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) { + + QueryTokenStream tokens = super.visitJoinFunctionCall(ctx); + + if (ctx.variable() != null && !tokens.isEmpty()) { + transformerSupport.registerAlias(tokens.getLast()); + } + + return tokens; + } + @Override public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) { 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 7d30338ebe..a557af1d40 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 @@ -36,6 +36,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Yannick Brandt + * @author oscar.fanchin * @since 3.1 */ class HqlQueryRendererTests { @@ -2375,4 +2376,254 @@ void reservedWordsShouldWork() { assertQuery("select ie from ItemExample ie left join ie.object io where io.object = :externalId"); assertQuery("select ie from ItemExample ie where ie.status = com.app.domain.object.Status.UP"); } + + @Test // GH-3864 - Added support for Set Return function (SRF) support H7 parsing and + // rendering + void fromSRFWithAlias() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue ) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date ) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function() d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue , :longValue ) d + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void fromSRFWithoutAlias() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue ) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date ) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function() + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue , :longValue ) + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinEntityToSRFWithFunctionAlias() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date , :integerValue ) d on (e.id = d.idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date ) d on (e.id = d.idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function() d on (e.id = d.idFunction) + """); + + assertQuery( + """ + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date , :integerValue , :longValue ) d on (e.id = d.idFunction) + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinEntityToSRFWithoutFunctionAlias() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date , :integerValue ) on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date ) on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function() on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date , :integerValue , :longValue ) on (e.id = idFunction) + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinSRFToEntityWithoutFunctionWithAlias() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue ) d join EntityClass e on (e.id = d.idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date ) d join EntityClass e on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function() d join EntityClass e on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue , :longValue ) d join EntityClass e on (e.id = d.idFunction) + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinSRFToEntityWithoutFunctionWithoutAlias() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue ) join EntityClass e on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date ) join EntityClass e on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function() join EntityClass e on (e.id = idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue , :longValue ) join EntityClass e on (e.id = idFunction) + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void selectSRFIntoSubquery() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date , :integerValue ) x) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date ) x) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function() x) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date , :integerValue , :longValue ) x) d + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinEntityToSRFIntoSubquery() { + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date , :integerValue ) x ) d on (k.id = d.idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date ) x ) d on (k.id = d.idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function() x ) d on (k.id = d.idFunction) + """); + + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date , :integerValue , :longValue ) x ) d on (k.id = d.idFunction) + """); + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinLateralEntityToSRF() { + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date , :integerValue ) x where x.idFunction = k.id ) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date ) x where x.idFunction = k.id ) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function() x where x.idFunction = k.id ) d + """); + + assertQuery(""" + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date , :integerValue , :longValue ) x where x.idFunction = k.id ) d + """); + + } + + @Test // GH-3864 - Added support for Set Return function support H7 parsing and + // rendering + void joinTwoFunctions() { + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date , :integerValue ) d + inner join some_function_single_param(:date ) k on (d.idFunction = k.idFunctionSP) + """); + + } + } 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 cd2c3987fc..71d2327f7e 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 @@ -1133,6 +1133,33 @@ void createsCountQueryUsingAliasCorrectly() { assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); } + + + @Test // GH-3864 + void testCountFromFunctionWithAlias() { + + // given + var original = "select x.id, x.value from some_function(:date , :integerValue ) x"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).contains("select count(*) from some_function(:date , :integerValue ) x"); + } + + @Test // GH-3864 + void testCountFromFunctionNoAlias() { + + // given + var original = "select id, value from some_function(:date , :integerValue )"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).contains("select count(*) from some_function(:date , :integerValue )"); + } @Test // GH-3427 void sortShouldBeAppendedWithSpacingInCaseOfSetOperator() { From 01eb2b833db8e8fdec75b1fc2e80e0f82b1e9ffb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 15:46:29 +0200 Subject: [PATCH 096/224] Polishing. Reuse setReturningFunction and genericFunctionArguments structure for easier extension. Split fromRoot into separate parser function fragments. Fix trailing spaces. See: #3864 Original pull request: #3879 --- .../data/jpa/repository/query/Hql.g4 | 26 +- .../query/HibernateQueryInformation.java | 2 +- .../query/HqlCountQueryTransformer.java | 46 +--- .../query/HqlQueryIntrospector.java | 43 ++- .../repository/query/HqlQueryRenderer.java | 116 ++++----- .../query/HqlSortedQueryTransformer.java | 2 +- .../query/HqlQueryRendererTests.java | 246 +++++++++--------- .../query/HqlQueryTransformerTests.java | 15 +- 8 files changed, 230 insertions(+), 266 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 8ced23f280..fab6d9a079 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 @@ -112,15 +112,11 @@ joinSpecifier ; fromRoot - : entityName variable? - | LATERAL? '(' subquery ')' variable? - | functionCallAsFromSource variable? + : entityName variable? # RootEntity + | LATERAL? '(' subquery ')' variable? # RootSubquery + | setReturningFunction variable? # RootFunction ; -functionCallAsFromSource - : identifier '(' (expression (',' expression)*)? ')' - ; - join : joinType JOIN FETCH? joinTarget joinRestriction? // Spec BNF says joinType isn't optional, but text says that it is. ; @@ -128,11 +124,7 @@ join joinTarget : path variable? # JoinPath | LATERAL? '(' subquery ')' variable? # JoinSubquery - | functionCallAsJoinTarget variable? # JoinFunctionCall - ; - -functionCallAsJoinTarget - : identifier '(' (expression (',' expression)*)? ')' + | setReturningFunction variable? # JoinFunctionCall ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-update @@ -768,6 +760,14 @@ function | genericFunction # GenericFunctionInvocation ; +setReturningFunction + : simpleSetReturningFunction + ; + +simpleSetReturningFunction + : identifier '(' genericFunctionArguments? ')' + ; + /** * Any function with an irregular syntax for the argument list * @@ -1888,4 +1888,4 @@ ESCAPE_SEQUENCE QUOTED_IDENTIFIER : BACKTICK ( ESCAPE_SEQUENCE | '\\' BACKTICK | ~([`]) )* BACKTICK - ; \ No newline at end of file + ; 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 fd77f0ea90..e1ea9173d1 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 @@ -23,7 +23,7 @@ * Hibernate-specific query details capturing common table expression details. * * @author Mark Paluch - * @author oscar.fanchin + * @author Oscar Fanchin * @since 3.5 */ class HibernateQueryInformation extends QueryInformation { 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 3a85c59126..e35b712589 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 @@ -30,7 +30,7 @@ * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch - * @author oscar.fanchin + * @author Oscar Fanchin * @since 3.1 */ @SuppressWarnings("ConstantValue") @@ -113,7 +113,6 @@ public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) { } } - if (ctx.whereClause() != null) { builder.appendExpression(visit(ctx.whereClause())); } @@ -133,48 +132,6 @@ public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) { return builder; } - @Override - public QueryRendererBuilder visitFromRoot(HqlParser.FromRootContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.entityName() != null) { - - builder.appendExpression(visit(ctx.entityName())); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } - } else if (ctx.subquery() != null) { - - if (ctx.LATERAL() != null) { - builder.append(QueryTokens.expression(ctx.LATERAL())); - } - - QueryRendererBuilder nested = QueryRenderer.builder(); - - nested.append(TOKEN_OPEN_PAREN); - nested.appendInline(visit(ctx.subquery())); - nested.append(TOKEN_CLOSE_PAREN); - - builder.appendExpression(nested); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } - } else if (ctx.functionCallAsFromSource() != null) { - - builder.appendExpression(visit(ctx.functionCallAsFromSource())); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } - } - - - return builder; - } - @Override public QueryRendererBuilder visitJoin(HqlParser.JoinContext ctx) { @@ -193,6 +150,7 @@ public QueryRendererBuilder visitJoin(HqlParser.JoinContext ctx) { return builder; } + @Override public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { 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 3c3f4b29ef..ba88ab2df1 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 @@ -21,15 +21,15 @@ import java.util.Collections; import java.util.List; -import org.springframework.data.jpa.repository.query.HqlParser.VariableContext; - import org.jspecify.annotations.Nullable; +import org.springframework.data.jpa.repository.query.HqlParser.VariableContext; + /** * {@link ParsedQueryIntrospector} for HQL queries. * * @author Mark Paluch - * @author oscar.fanchin + * @author Oscar Fanchin */ @SuppressWarnings({ "UnreachableCode", "ConstantValue" }) class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIntrospector { @@ -52,9 +52,9 @@ public HibernateQueryInformation getParsedQueryInformation() { @Override public Void visitSelectClause(HqlParser.SelectClauseContext ctx) { - if (!projectionProcessed) { - projection = captureSelectItems(ctx.selectionList().selection(), renderer); - projectionProcessed = true; + if (!this.projectionProcessed) { + this.projection = captureSelectItems(ctx.selectionList().selection(), renderer); + this.projectionProcessed = true; } return super.visitSelectClause(ctx); @@ -65,21 +65,36 @@ public Void visitCte(HqlParser.CteContext ctx) { this.hasCte = true; return super.visitCte(ctx); } - + + @Override + public Void visitRootEntity(HqlParser.RootEntityContext ctx) { + + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); + } + + return super.visitRootEntity(ctx); + } + @Override - public Void visitFunctionCallAsFromSource(HqlParser.FunctionCallAsFromSourceContext ctx) { - this.hasFromFunction = true; - return super.visitFunctionCallAsFromSource(ctx); + public Void visitRootSubquery(HqlParser.RootSubqueryContext ctx) { + + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); + } + + return super.visitRootSubquery(ctx); } @Override - public Void visitFromRoot(HqlParser.FromRootContext ctx) { + public Void visitRootFunction(HqlParser.RootFunctionContext ctx) { - if (primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { - primaryFromAlias = capturePrimaryAlias(ctx.variable()); + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); + this.hasFromFunction = true; } - return super.visitFromRoot(ctx); + return super.visitRootFunction(ctx); } @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 a6b3dd4ca9..01557b33d5 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 @@ -64,24 +64,6 @@ public QueryTokenStream visitStart(HqlParser.StartContext ctx) { return visit(ctx.ql_statement()); } - @Override - public QueryTokenStream visitFunctionCallAsFromSource(HqlParser.FunctionCallAsFromSourceContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.identifier())); - - builder.append(TOKEN_OPEN_PAREN); - - if (!ctx.expression().isEmpty()) { - builder.append(QueryTokenStream.concatExpressions(ctx.expression(), this::visit, TOKEN_COMMA)); - } - - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - @Override public QueryTokenStream visitQl_statement(HqlParser.Ql_statementContext ctx) { @@ -369,44 +351,74 @@ public QueryTokenStream visitJoinSpecifier(HqlParser.JoinSpecifierContext ctx) { } @Override - public QueryTokenStream visitFromRoot(HqlParser.FromRootContext ctx) { + public QueryTokenStream visitRootEntity(HqlParser.RootEntityContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.entityName() != null) { + builder.appendExpression(visit(ctx.entityName())); - builder.appendExpression(visit(ctx.entityName())); + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); + } - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } + return builder; + } - } else if (ctx.subquery() != null) { + @Override + public QueryTokenStream visitRootSubquery(HqlParser.RootSubqueryContext ctx) { - if (ctx.LATERAL() != null) { - builder.append(QueryTokens.expression(ctx.LATERAL())); - } + QueryRendererBuilder builder = QueryRenderer.builder(); - QueryRendererBuilder nested = QueryRenderer.builder(); + if (ctx.LATERAL() != null) { + builder.append(QueryTokens.expression(ctx.LATERAL())); + } - nested.append(TOKEN_OPEN_PAREN); - nested.appendInline(visit(ctx.subquery())); - nested.append(TOKEN_CLOSE_PAREN); + QueryRendererBuilder nested = QueryRenderer.builder(); - builder.appendExpression(nested); + nested.append(TOKEN_OPEN_PAREN); + nested.appendInline(visit(ctx.subquery())); + nested.append(TOKEN_CLOSE_PAREN); - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } + builder.appendExpression(nested); - } else if (ctx.functionCallAsFromSource() != null) { + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); + } - builder.appendExpression(visit(ctx.functionCallAsFromSource())); + return builder; + } - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } + @Override + public QueryTokenStream visitRootFunction(HqlParser.RootFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.setReturningFunction())); + + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); + } + + return builder; + } + + @Override + public QueryTokenStream visitSetReturningFunction(HqlParser.SetReturningFunctionContext ctx) { + return visit(ctx.simpleSetReturningFunction()); + } + + @Override + public QueryTokenStream visitSimpleSetReturningFunction(HqlParser.SimpleSetReturningFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.identifier())); + + builder.append(TOKEN_OPEN_PAREN); + if (ctx.genericFunctionArguments() != null) { + builder.append(visit(ctx.genericFunctionArguments())); } + builder.append(TOKEN_CLOSE_PAREN); return builder; } @@ -459,7 +471,7 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { } builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.subquery())); + builder.appendInline(visit(ctx.subquery())); builder.append(TOKEN_CLOSE_PAREN); if (ctx.variable() != null) { @@ -474,7 +486,7 @@ public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.functionCallAsJoinTarget())); + builder.append(visit(ctx.setReturningFunction())); if (ctx.variable() != null) { builder.appendExpression(visit(ctx.variable())); @@ -484,24 +496,6 @@ public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext } - @Override - public QueryTokenStream visitFunctionCallAsJoinTarget(HqlParser.FunctionCallAsJoinTargetContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.identifier())); - - builder.append(TOKEN_OPEN_PAREN); - - if (!ctx.expression().isEmpty()) { - builder.append(QueryTokenStream.concatExpressions(ctx.expression(), this::visit, TOKEN_COMMA)); - } - - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - @Override public QueryTokenStream visitUpdateStatement(HqlParser.UpdateStatementContext ctx) { 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 45074e5a47..175e918c80 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 @@ -32,7 +32,7 @@ * * @author Greg Turnquist * @author Christoph Strobl - * @author oscar.fanchin + * @author Oscar Fanchin * @since 3.1 */ @SuppressWarnings("ConstantValue") 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 a557af1d40..040c632dfb 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 @@ -5,7 +5,7 @@ * 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 + *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, @@ -36,7 +36,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Yannick Brandt - * @author oscar.fanchin + * @author Oscar Fanchin * @since 3.1 */ class HqlQueryRendererTests { @@ -608,8 +608,8 @@ void existsSubSelectExample1() { SELECT DISTINCT emp FROM Employee emp WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) """); } @@ -620,14 +620,14 @@ void everyAll() { SELECT DISTINCT emp FROM Employee emp WHERE EVERY (SELECT spouseEmp - FROM Employee spouseEmp) > 1 + FROM Employee spouseEmp) > 1 """); assertQuery(""" SELECT DISTINCT emp FROM Employee emp WHERE ALL (SELECT spouseEmp - FROM Employee spouseEmp) > 1 + FROM Employee spouseEmp) > 1 """); assertQuery(""" @@ -656,14 +656,14 @@ void anySome() { SELECT DISTINCT emp FROM Employee emp WHERE ANY (SELECT spouseEmp - FROM Employee spouseEmp) > 1 + FROM Employee spouseEmp) > 1 """); assertQuery(""" SELECT DISTINCT emp FROM Employee emp WHERE SOME (SELECT spouseEmp - FROM Employee spouseEmp) > 1 + FROM Employee spouseEmp) > 1 """); assertQuery(""" @@ -772,7 +772,7 @@ void leftJoinOnExample() { assertQuery(""" SELECT s.name, COUNT(p) FROM Suppliers s LEFT JOIN s.products p - ON p.status = 'inStock' + ON p.status = 'inStock' GROUP BY s.name """); } @@ -854,15 +854,15 @@ void fromClauseDowncastingExample3() { 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" + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE "cost overrun" """); 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' + OR TREAT(p AS SmallProject).name LIKE 'Persist%' + OR p.description LIKE 'cost overrun' """); } @@ -872,7 +872,7 @@ void fromClauseDowncastingExample4() { assertQuery(""" SELECT e FROM Employee e WHERE TREAT(e AS Exempt).vacationDays > 10 - OR TREAT(e AS Contractor).hours > 100 + OR TREAT(e AS Contractor).hours > 100 """); } @@ -883,8 +883,8 @@ void allExample() { SELECT emp FROM Employee emp WHERE emp.salary > ALL (SELECT m.salary - FROM Manager m - WHERE m.department = emp.department) + FROM Manager m + WHERE m.department = emp.department) """); } @@ -895,8 +895,8 @@ void existsSubSelectExample2() { SELECT DISTINCT emp FROM Employee emp WHERE EXISTS (SELECT spouseEmp - FROM Employee spouseEmp - WHERE spouseEmp = emp.spouse) + FROM Employee spouseEmp + WHERE spouseEmp = emp.spouse) """); } @@ -970,9 +970,9 @@ void updateCaseExample1() { 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 + WHEN e.rating = 2 THEN e.salary * 1.05 + ELSE e.salary * 1.01 + END """); } @@ -982,10 +982,10 @@ 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 + CASE e.rating WHEN 1 THEN e.salary * 1.1 + WHEN 2 THEN e.salary * 1.05 + ELSE e.salary * 1.01 + END """); } @@ -994,11 +994,11 @@ void selectCaseExample1() { assertQuery(""" SELECT e.name, - CASE TYPE(e) WHEN Exempt THEN 'Exempt' - WHEN Contractor THEN 'Contractor' - WHEN Intern THEN 'Intern' - ELSE 'NonExempt' - END + 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' """); @@ -1009,12 +1009,12 @@ 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') + 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 """); } @@ -1104,8 +1104,8 @@ void distinctFromPredicate(String distinctFrom) { SELECT c FROM Customer c WHERE EXISTS (SELECT c2 - FROM Customer c2 - WHERE c2.orders %s c.orders) + FROM Customer c2 + WHERE c2.orders %s c.orders) """.formatted(distinctFrom)); } @@ -1814,12 +1814,12 @@ void hqlQueries() { "from Person pr " + // "left join pr.phones ph " + // "where ph is null " + // - " or ph.type = :phoneType"); + "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"); + "or ph.type = :phoneType"); assertQuery("select pr.name, ph.number " + // "from Person pr " + // "left join pr.phones ph with ph.type = :phoneType "); @@ -1843,8 +1843,7 @@ void hqlQueries() { "(select c.duration as duration " + // " from p.calls c" + // " order by c.duration desc" + // - " limit 1 " + // - " ) longest " + // + " limit 1) longest " + // "where p.number = :phoneNumber"); assertQuery("select ph " + // "from Phone ph " + // @@ -2208,7 +2207,7 @@ void functionNamesShouldSupportSchemaScoping() { SELECT b FROM MyEntity b WHERE b.status = :status - AND utl_raw.cast_to_varchar2((nlssort(lower(b.name), 'nls_sort=binary_ai'))) LIKE lower(:name) + AND utl_raw.cast_to_varchar2((nlssort(lower(b.name), 'nls_sort=binary_ai'))) LIKE lower(:name) ORDER BY utl_raw.cast_to_varchar2((nlssort(lower(b.name), 'nls_sort=binary_ai'))) ASC """); @@ -2377,17 +2376,17 @@ void reservedWordsShouldWork() { assertQuery("select ie from ItemExample ie where ie.status = com.app.domain.object.Status.UP"); } - @Test // GH-3864 - Added support for Set Return function (SRF) support H7 parsing and - // rendering + @Test // GH-3864 void fromSRFWithAlias() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue ) d + from some_function(:date, :integerValue) d """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date ) d + from some_function(:date) d """); assertQuery(""" @@ -2397,21 +2396,21 @@ from some_function() d assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue , :longValue ) d + from some_function(:date, :integerValue, :longValue) d """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void fromSRFWithoutAlias() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue ) + from some_function(:date, :integerValue) """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date ) + from some_function(:date) """); assertQuery(""" @@ -2421,21 +2420,21 @@ from some_function() assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue , :longValue ) + from some_function(:date, :integerValue, :longValue) """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinEntityToSRFWithFunctionAlias() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from EntityClass e join some_function(:date , :integerValue ) d on (e.id = d.idFunction) + from EntityClass e join some_function(:date, :integerValue) d on (e.id = d.idFunction) """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from EntityClass e join some_function(:date ) d on (e.id = d.idFunction) + from EntityClass e join some_function(:date) d on (e.id = d.idFunction) """); assertQuery(""" @@ -2443,24 +2442,23 @@ from EntityClass e join some_function(:date ) d on (e.id = d.idFunction) from EntityClass e join some_function() d on (e.id = d.idFunction) """); - assertQuery( - """ - select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from EntityClass e join some_function(:date , :integerValue , :longValue ) d on (e.id = d.idFunction) - """); + assertQuery(""" + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from EntityClass e join some_function(:date, :integerValue, :longValue) d on (e.id = d.idFunction) + """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinEntityToSRFWithoutFunctionAlias() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from EntityClass e join some_function(:date , :integerValue ) on (e.id = idFunction) + from EntityClass e join some_function(:date, :integerValue) on (e.id = idFunction) """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from EntityClass e join some_function(:date ) on (e.id = idFunction) + from EntityClass e join some_function(:date) on (e.id = idFunction) """); assertQuery(""" @@ -2470,21 +2468,21 @@ from EntityClass e join some_function() on (e.id = idFunction) assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from EntityClass e join some_function(:date , :integerValue , :longValue ) on (e.id = idFunction) + from EntityClass e join some_function(:date, :integerValue, :longValue) on (e.id = idFunction) """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinSRFToEntityWithoutFunctionWithAlias() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue ) d join EntityClass e on (e.id = d.idFunction) + from some_function(:date, :integerValue) d join EntityClass e on (e.id = d.idFunction) """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date ) d join EntityClass e on (e.id = idFunction) + from some_function(:date) d join EntityClass e on (e.id = idFunction) """); assertQuery(""" @@ -2493,22 +2491,22 @@ from some_function() d join EntityClass e on (e.id = idFunction) """); assertQuery(""" - select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue , :longValue ) d join EntityClass e on (e.id = d.idFunction) + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date, :integerValue, :longValue) d join EntityClass e on (e.id = d.idFunction) """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinSRFToEntityWithoutFunctionWithoutAlias() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue ) join EntityClass e on (e.id = idFunction) + from some_function(:date, :integerValue) join EntityClass e on (e.id = idFunction) """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date ) join EntityClass e on (e.id = idFunction) + from some_function(:date) join EntityClass e on (e.id = idFunction) """); assertQuery(""" @@ -2518,23 +2516,23 @@ from some_function() join EntityClass e on (e.id = idFunction) assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue , :longValue ) join EntityClass e on (e.id = idFunction) + from some_function(:date, :integerValue, :longValue) join EntityClass e on (e.id = idFunction) """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void selectSRFIntoSubquery() { + assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) from (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date , :integerValue ) x) d + from some_function(:date, :integerValue) x) d """); assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) from (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date ) x) d + from some_function(:date) x) d """); assertQuery(""" @@ -2546,82 +2544,82 @@ from some_function() x) d assertQuery(""" select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) from (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date , :integerValue , :longValue ) x) d + from some_function(:date, :integerValue, :longValue) x) d """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinEntityToSRFIntoSubquery() { + assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - inner join (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date , :integerValue ) x ) d on (k.id = d.idFunction) + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date, :integerValue) x) d on (k.id = d.idFunction) """); assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - inner join (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date ) x ) d on (k.id = d.idFunction) + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date) x) d on (k.id = d.idFunction) """); assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - inner join (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function() x ) d on (k.id = d.idFunction) + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function() x) d on (k.id = d.idFunction) """); assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - inner join (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date , :integerValue , :longValue ) x ) d on (k.id = d.idFunction) + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + inner join (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date, :integerValue, :longValue) x) d on (k.id = d.idFunction) """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinLateralEntityToSRF() { + assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - join lateral (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date , :integerValue ) x where x.idFunction = k.id ) d + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date, :integerValue) x where x.idFunction = k.id) d """); assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - join lateral (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date ) x where x.idFunction = k.id ) d + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date) x where x.idFunction = k.id) d """); assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - join lateral (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function() x where x.idFunction = k.id ) d + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function() x where x.idFunction = k.id) d """); assertQuery(""" - select new com.example.dto.SampleDto(k.id, d.nameFunction) - from EntityClass k - join lateral (select x.idFunction idFunction, x.nameFunction nameFunction - from some_function(:date , :integerValue , :longValue ) x where x.idFunction = k.id ) d + select new com.example.dto.SampleDto(k.id, d.nameFunction) + from EntityClass k + join lateral (select x.idFunction idFunction, x.nameFunction nameFunction + from some_function(:date, :integerValue, :longValue) x where x.idFunction = k.id) d """); } - @Test // GH-3864 - Added support for Set Return function support H7 parsing and - // rendering + @Test // GH-3864 void joinTwoFunctions() { + assertQuery(""" - select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) - from some_function(:date , :integerValue ) d - inner join some_function_single_param(:date ) k on (d.idFunction = k.idFunctionSP) + select new com.example.dto.SampleDto(d.idFunction, d.nameFunction) + from some_function(:date, :integerValue) d + inner join some_function_single_param(:date) k on (d.idFunction = k.idFunctionSP) """); } 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 71d2327f7e..260a788d64 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 @@ -1133,32 +1133,31 @@ void createsCountQueryUsingAliasCorrectly() { assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); } - - + @Test // GH-3864 void testCountFromFunctionWithAlias() { // given - var original = "select x.id, x.value from some_function(:date , :integerValue ) x"; + var original = "select x.id, x.value from some_function(:date, :integerValue) x"; // when var results = createCountQueryFor(original); // then - assertThat(results).contains("select count(*) from some_function(:date , :integerValue ) x"); - } - + assertThat(results).contains("select count(*) from some_function(:date, :integerValue) x"); + } + @Test // GH-3864 void testCountFromFunctionNoAlias() { // given - var original = "select id, value from some_function(:date , :integerValue )"; + var original = "select id, value from some_function(:date, :integerValue)"; // when var results = createCountQueryFor(original); // then - assertThat(results).contains("select count(*) from some_function(:date , :integerValue )"); + assertThat(results).contains("select count(*) from some_function(:date, :integerValue)"); } @Test // GH-3427 From eeca4df36f98f94eeaa27aa12efd5ab5dfda2997 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 May 2025 15:59:41 +0200 Subject: [PATCH 097/224] Detect PersistenceUnitManager and PersistenceManaged types in JpaRepositoryContributor. See #3875 --- .../data/jpa/repository/aot/AotMetamodel.java | 127 +++++++++++------- .../aot/JpaRepositoryContributor.java | 34 +++-- .../config/JpaRepositoryConfigExtension.java | 55 ++++++-- ...toryRegistrationAotProcessorUnitTests.java | 75 ++++++++++- 4 files changed, 220 insertions(+), 71 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 2b3f49bb28..ee8ecd7f50 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 @@ -22,74 +22,131 @@ import jakarta.persistence.metamodel.ManagedType; import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.spi.ClassTransformer; +import jakarta.persistence.spi.PersistenceUnitInfo; +import java.net.URL; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; +import org.hibernate.cfg.JdbcSettings; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.engine.jdbc.connections.internal.UserSuppliedConnectionProviderImpl; import org.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; +import org.jspecify.annotations.Nullable; + +import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.util.Lazy; import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; /** + * AOT metamodel implementation that uses Hibernate to build the metamodel. + * * @author Christoph Strobl + * @author Mark Paluch * @since 4.0 */ 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()); + private final Lazy entityManagerFactory; + private final Lazy entityManager = Lazy.of(() -> getEntityManagerFactory().createEntityManager()); - public AotMetamodel(Set> managedTypes) { - this("AotMetamodel", managedTypes); + public AotMetamodel(AotRepositoryContext repositoryContext) { + this(repositoryContext.getResolvedTypes().stream().map(Class::getName) + .filter(name -> !name.startsWith("jakarta.persistence")).toList(), null); } - private AotMetamodel(String persistenceUnit, Set> managedTypes) { - this.persistenceUnit = persistenceUnit; - this.managedTypes = managedTypes; + public AotMetamodel(PersistenceManagedTypes managedTypes) { + this(managedTypes.getManagedClassNames(), managedTypes.getPersistenceUnitRootUrl()); } - public static AotMetamodel hibernateModel(Class... types) { - return new AotMetamodel(Set.of(types)); + public AotMetamodel(Collection managedTypes, @Nullable URL persistenceUnitRootUrl) { + + MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { + @Override + public ClassLoader getNewTempClassLoader() { + return new SimpleThrowawayClassLoader(this.getClass().getClassLoader()); + } + + @Override + public void addTransformer(ClassTransformer classTransformer) { + // just ignore it + } + }; + persistenceUnitInfo.setPersistenceUnitName("AotMetaModel"); + + this.entityManagerFactory = init(() -> { + + managedTypes.stream().forEach(persistenceUnitInfo::addManagedClassName); + + persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName()); + + return new PersistenceUnitInfoDescriptor(persistenceUnitInfo) { + + @Override + public List getManagedClassNames() { + return persistenceUnitInfo.getManagedClassNames(); + } + + @Override + public URL getPersistenceUnitRootUrl() { + return persistenceUnitRootUrl; + } + + }; + }); } - public static AotMetamodel hibernateModel(String persistenceUnit, Class... types) { - return new AotMetamodel(persistenceUnit, Set.of(types)); + public AotMetamodel(PersistenceUnitInfo unitInfo) { + this.entityManagerFactory = init(() -> new PersistenceUnitInfoDescriptor(unitInfo)); + } + + static Lazy init(Supplier unitInfo) { + + return Lazy.of(() -> new EntityManagerFactoryBuilderImpl(unitInfo.get(), + Map.of(JdbcSettings.DIALECT, H2Dialect.class.getName(), // + JdbcSettings.ALLOW_METADATA_ON_BOOT, "false", // + JdbcSettings.CONNECTION_PROVIDER, new UserSuppliedConnectionProviderImpl())) + .build()); + } + + private Metamodel getMetamodel() { + return getEntityManagerFactory().getMetamodel(); } public EntityType entity(Class cls) { - return metamodel.get().entity(cls); + return getMetamodel().entity(cls); } @Override public EntityType entity(String s) { - return metamodel.get().entity(s); + return getMetamodel().entity(s); } public ManagedType managedType(Class cls) { - return metamodel.get().managedType(cls); + return getMetamodel().managedType(cls); } public EmbeddableType embeddable(Class cls) { - return metamodel.get().embeddable(cls); + return getMetamodel().embeddable(cls); } public Set> getManagedTypes() { - return metamodel.get().getManagedTypes(); + return getMetamodel().getManagedTypes(); } public Set> getEntities() { - return metamodel.get().getEntities(); + return getMetamodel().getEntities(); } public Set> getEmbeddables() { - return metamodel.get().getEmbeddables(); + return getMetamodel().getEmbeddables(); } public EntityManager entityManager() { @@ -100,32 +157,4 @@ public EntityManagerFactory getEntityManagerFactory() { return entityManagerFactory.get(); } - EntityManagerFactory init() { - - MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { - @Override - public ClassLoader getNewTempClassLoader() { - return new SimpleThrowawayClassLoader(this.getClass().getClassLoader()); - } - - @Override - public void addTransformer(ClassTransformer classTransformer) { - // just ignore 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", "hibernate.boot.allow_jdbc_metadata_access", - "false")).build(); - } - } 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 1dcb10809b..41bf6ff6e0 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 @@ -17,10 +17,11 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.metamodel.Metamodel; +import jakarta.persistence.spi.PersistenceUnitInfo; import java.lang.reflect.Method; import java.util.Map; -import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -49,6 +50,7 @@ import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -64,31 +66,43 @@ */ public class JpaRepositoryContributor extends RepositoryContributor { + private final Metamodel metamodel; private final PersistenceProvider persistenceProvider; private final QueriesFactory queriesFactory; private final EntityGraphLookup entityGraphLookup; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { + this(repositoryContext, new AotMetamodel(repositoryContext)); + } - super(repositoryContext); - - AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes().stream() - .filter(it -> !it.getName().startsWith("jakarta.persistence")).collect(Collectors.toSet())); + public JpaRepositoryContributor(AotRepositoryContext repositoryContext, PersistenceUnitInfo unitInfo) { + this(repositoryContext, new AotMetamodel(unitInfo)); + } - this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory()); - this.queriesFactory = new QueriesFactory(amm.getEntityManagerFactory(), amm); - this.entityGraphLookup = new EntityGraphLookup(amm.getEntityManagerFactory()); + public JpaRepositoryContributor(AotRepositoryContext repositoryContext, PersistenceManagedTypes managedTypes) { + this(repositoryContext, new AotMetamodel(managedTypes)); } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { super(repositoryContext); + this.metamodel = entityManagerFactory.getMetamodel(); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); this.queriesFactory = new QueriesFactory(entityManagerFactory); this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory); } + private JpaRepositoryContributor(AotRepositoryContext repositoryContext, AotMetamodel metamodel) { + + super(repositoryContext); + + this.metamodel = metamodel; + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(metamodel.getEntityManagerFactory()); + this.queriesFactory = new QueriesFactory(metamodel.getEntityManagerFactory(), metamodel); + this.entityGraphLookup = new EntityGraphLookup(metamodel.getEntityManagerFactory()); + } + @Override protected void customizeClass(AotRepositoryClassBuilder classBuilder) { classBuilder.customize(builder -> builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class))); @@ -203,6 +217,10 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB }); } + public Metamodel getMetamodel() { + return metamodel; + } + record StoredProcedureMetadata(String procedure) implements QueryMetadata { @Override 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 ce3218593f..360b449be7 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 @@ -22,6 +22,7 @@ import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceUnit; +import jakarta.persistence.spi.PersistenceUnitInfo; import java.lang.annotation.Annotation; import java.util.Arrays; @@ -35,8 +36,11 @@ import java.util.Set; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -46,6 +50,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.dao.DataAccessException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; @@ -57,13 +62,14 @@ 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.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.orm.jpa.persistenceunit.PersistenceManagedTypes; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -90,6 +96,7 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi private static final String ENABLE_DEFAULT_TRANSACTIONS_ATTRIBUTE = "enableDefaultTransactions"; private static final String JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME = "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup"; private static final String ESCAPE_CHARACTER_PROPERTY = "escapeCharacter"; + private static final Logger log = LoggerFactory.getLogger(JpaRepositoryConfigExtension.class); private final Map entityManagerRefs = new LinkedHashMap<>(); @@ -327,28 +334,60 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi String GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER = "spring.aot.jpa.repositories.use-entitymanager"; - protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext, + protected @Nullable JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + Environment environment = repositoryContext.getEnvironment(); + boolean enabled = Boolean.parseBoolean( - repositoryContext.getEnvironment().getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); if (!enabled) { return null; } + ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); + boolean useEntityManager = Boolean.parseBoolean( - repositoryContext.getEnvironment().getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); + environment.getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); if (useEntityManager) { - ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); - EntityManagerFactory emf = beanFactory.getBeanProvider(EntityManagerFactory.class).getIfAvailable(); + ObjectProvider unitManagerProvider = beanFactory + .getBeanProvider(PersistenceUnitManager.class); + PersistenceUnitManager unitManager = unitManagerProvider.getIfAvailable(); - if (emf != null) { - return new JpaRepositoryContributor(repositoryContext, emf); + if (unitManager != null) { + + log.debug("Using PersistenceUnitManager for AOT repository generation"); + return new JpaRepositoryContributor(repositoryContext, unitManager.obtainDefaultPersistenceUnitInfo()); } + + log.debug("Using EntityManager for AOT repository generation"); + + EntityManagerFactory emf = beanFactory.getBean(EntityManagerFactory.class); + return new JpaRepositoryContributor(repositoryContext, emf); + } + + ObjectProvider managedTypesProvider = beanFactory + .getBeanProvider(PersistenceManagedTypes.class); + PersistenceManagedTypes managedTypes = managedTypesProvider.getIfAvailable(); + + if (managedTypes != null) { + + log.debug("Using PersistenceManagedTypes for AOT repository generation"); + return new JpaRepositoryContributor(repositoryContext, managedTypes); + } + + ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class); + PersistenceUnitInfo unitInfo = infoProvider.getIfAvailable(); + + if (unitInfo != null) { + + log.debug("Using PersistenceUnitInfo for AOT repository generation"); + return new JpaRepositoryContributor(repositoryContext, unitInfo); } + log.debug("Using scanned types for AOT repository generation"); return new JpaRepositoryContributor(repositoryContext); } } 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 44c260dcb5..d75531c507 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 @@ -18,24 +18,39 @@ import static org.assertj.core.api.Assertions.*; import jakarta.persistence.Entity; +import jakarta.persistence.Id; import java.lang.annotation.Annotation; +import java.net.URL; import java.util.Collections; +import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; + import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.InMemoryGeneratedFiles; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.aot.AotContext; +import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.AotRepositoryInformation; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.javapoet.ClassName; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; /** * @author Christoph Strobl @@ -49,7 +64,7 @@ void aotProcessorMustNotRegisterDomainTypes() { new InMemoryGeneratedFiles()); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext() { + .contribute(new DummyAotRepositoryContext(null) { @Override public Set> getResolvedTypes() { return Collections.singleton(Person.class); @@ -66,7 +81,7 @@ void aotProcessorMustNotRegisterAnnotations() { new InMemoryGeneratedFiles()); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext() { + .contribute(new DummyAotRepositoryContext(null) { @Override public Set> getResolvedAnnotations() { @@ -79,10 +94,57 @@ public Set> getResolvedAnnotations() { assertThat(RuntimeHintsPredicates.reflection().onType(Entity.class)).rejects(ctx.getRuntimeHints()); } - static class Person {} + @Test // GH-3838 + void repositoryProcessorShouldConsiderPersistenceManagedTypes() { + + GenerationContext ctx = new DefaultGenerationContext(new ClassNameGenerator(ClassName.OBJECT), + new InMemoryGeneratedFiles()); + + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean(PersistenceManagedTypes.class, () -> { + + return new PersistenceManagedTypes() { + @Override + public List getManagedClassNames() { + return List.of(Person.class.getName()); + } + + @Override + public List getManagedPackages() { + return List.of(); + } + + @Override + public @Nullable URL getPersistenceUnitRootUrl() { + return null; + } + }; + }); + + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + + JpaRepositoryContributor contributor = new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() + .contribute(new DummyAotRepositoryContext(context), ctx); + + assertThat(contributor.getMetamodel().managedType(Person.class)).isNotNull(); + } + + @Entity + static class Person { + @Id Long id; + } + + interface PersonRepository extends Repository {} static class DummyAotRepositoryContext implements AotRepositoryContext { + private final @Nullable AbstractApplicationContext applicationContext; + + DummyAotRepositoryContext(@Nullable AbstractApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + @Override public String getBeanName() { return "jpaRepository"; @@ -105,7 +167,8 @@ public Set> getIdentifyingAnnotations() { @Override public RepositoryInformation getRepositoryInformation() { - return null; + return new AotRepositoryInformation(AbstractRepositoryMetadata.getMetadata(PersonRepository.class), + SimpleJpaRepository.class, List.of()); } @Override @@ -120,12 +183,12 @@ public Set> getResolvedTypes() { @Override public ConfigurableListableBeanFactory getBeanFactory() { - return null; + return applicationContext != null ? applicationContext.getBeanFactory() : null; } @Override public Environment getEnvironment() { - return new StandardEnvironment(); + return applicationContext == null ? new StandardEnvironment() : applicationContext.getEnvironment(); } @Override From 438de0c2e0b732648791dab91d1c273e5b886f9f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 May 2025 11:50:04 +0200 Subject: [PATCH 098/224] Upgrade to Hibernate 7.0 CR2. Closes #3887 --- pom.xml | 2 +- .../HibernateJpaParametersParameterAccessor.java | 2 +- .../query/JpaParametersParameterAccessorTests.java | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 47926932c4..fc3327e671 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.CR1 + 7.0.0.CR2 7.0.0-SNAPSHOT 2.7.4

        2.3.232

        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 af1c4fa0ec..555c5d31d1 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 @@ -93,7 +93,7 @@ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAcce protected Object potentiallyUnwrap(Object parameterValue) { return (parameterValue instanceof TypedParameterValue typedParameterValue) // - ? typedParameterValue.getValue() // + ? typedParameterValue.value() // : parameterValue; } } 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 0c2727ece4..22b8fcaa6c 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 @@ -51,6 +51,7 @@ void createsJpaParametersParameterAccessor() throws Exception { } @Test // GH-2370 + @SuppressWarnings({ "rawtypes", "unchecked" }) void createsHibernateParametersParameterAccessor() throws Exception { Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class); @@ -63,18 +64,17 @@ void createsHibernateParametersParameterAccessor() throws Exception { ArgumentCaptor> captor = ArgumentCaptor.forClass(TypedParameterValue.class); verify(query).setParameter(eq(1), captor.capture()); TypedParameterValue captorValue = captor.getValue(); - assertThat(captorValue.getType().getBindableJavaType()).isEqualTo(Integer.class); + assertThat(captorValue.type().getJavaType()).isEqualTo(Integer.class); assertThat(captorValue.getValue()).isNull(); } private void bind(JpaParameters parameters, JpaParametersParameterAccessor accessor) { - ParameterBinderFactory.createBinder(parameters, true) - .bind( // - QueryParameterSetter.BindableQuery.from(query), // - accessor, // - QueryParameterSetter.ErrorHandling.LENIENT // - ); + ParameterBinderFactory.createBinder(parameters, true).bind( // + QueryParameterSetter.BindableQuery.from(query), // + accessor, // + QueryParameterSetter.ErrorHandling.LENIENT // + ); } interface SampleRepository { From 88da07e5556972699e7a95c54010cdf349e24d0c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 May 2025 11:50:21 +0200 Subject: [PATCH 099/224] Polishing. Move off deprecated API. See #3887 --- .../java/org/springframework/data/envers/Config.java | 3 +-- .../data/jpa/repository/query/JpaQueryCreator.java | 1 - .../jpa/repository/support/JpaRepositoryFactory.java | 9 +++++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/Config.java b/spring-data-envers/src/test/java/org/springframework/data/envers/Config.java index c8c1aa963e..be69b3251b 100755 --- a/spring-data-envers/src/test/java/org/springframework/data/envers/Config.java +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/Config.java @@ -21,7 +21,7 @@ import javax.sql.DataSource; -import org.hibernate.envers.strategy.ValidityAuditStrategy; +import org.hibernate.envers.strategy.internal.ValidityAuditStrategy; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,7 +31,6 @@ import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; 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 f6cda83389..27a146e14b 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 @@ -357,7 +357,6 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { } } - @org.springframework.lang.Nullable private JpqlQueryBuilder.Expression getDistanceExpression() { DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction()); 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 bbccb5b979..2729f085e7 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 @@ -38,6 +38,7 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.QueryCreationListener; @@ -260,8 +261,7 @@ protected Optional getQueryLookupStrategy(@Nullable Key key JpaQueryConfiguration queryConfiguration = new JpaQueryConfiguration(queryRewriterProvider, queryEnhancerSelector, new CachingValueExpressionDelegate(valueExpressionDelegate), escapeCharacter); - return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, - queryConfiguration)); + return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, queryConfiguration)); } @Override @@ -270,6 +270,11 @@ public JpaEntityInformation getEntityInformation(Class domainC return (JpaEntityInformation) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager); } + @Override + public EntityInformation getEntityInformation(RepositoryMetadata metadata) { + return JpaEntityInformationSupport.getEntityInformation(metadata.getDomainType(), entityManager); + } + @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { return getRepositoryFragments(metadata, entityManager, entityPathResolver, this.crudMethodMetadata); From 3f17014b9ffb3e690b76d603a925329ca4143b5c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 May 2025 12:04:06 +0200 Subject: [PATCH 100/224] Capture `@EnableJpaRepositories` configuration for AOT processing. Closes #3838 --- .../AotRepositoryQueryMethodBenchmarks.java | 14 +++- .../data/jpa/repository/aot/AotMetamodel.java | 12 ++- .../aot/JpaRepositoryContributor.java | 34 ++++++--- .../jpa/repository/aot/QueriesFactory.java | 27 ++++--- .../config/JpaRepositoryConfigExtension.java | 31 ++++---- .../AotFragmentTestConfigurationSupport.java | 18 ++++- ...positoryContributorConfigurationTests.java | 74 +++++++++++++++++++ .../aot/TestJpaAotRepositoryContext.java | 11 ++- ...toryRegistrationAotProcessorUnitTests.java | 7 ++ .../antora/modules/ROOT/pages/jpa/aot.adoc | 10 ++- 10 files changed, 191 insertions(+), 47 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java 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 index a8682d32c3..1ec94e1602 100644 --- 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 @@ -25,6 +25,7 @@ import org.hibernate.jpa.HibernatePersistenceProvider; import org.junit.platform.commons.annotation.Testable; +import org.mockito.Mockito; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; @@ -37,16 +38,24 @@ import org.openjdk.jmh.annotations.Warmup; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.core.type.AnnotationMetadata; 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.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.sample.SampleConfig; 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.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -74,7 +83,10 @@ public static class BenchmarkParameters { public static Class aot; public static TestJpaAotRepositoryContext repositoryContext = new TestJpaAotRepositoryContext<>( - PersonRepository.class, null); + PersonRepository.class, null, + new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(SampleConfig.class), + EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), + Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); EntityManager entityManager; RepositoryComposition.RepositoryFragments fragments; 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 ee8ecd7f50..a19191eb1c 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 @@ -58,8 +58,16 @@ class AotMetamodel implements Metamodel { private final Lazy entityManager = Lazy.of(() -> getEntityManagerFactory().createEntityManager()); public AotMetamodel(AotRepositoryContext repositoryContext) { - this(repositoryContext.getResolvedTypes().stream().map(Class::getName) - .filter(name -> !name.startsWith("jakarta.persistence")).toList(), null); + this(repositoryContext.getResolvedTypes().stream().filter(AotMetamodel::isJakartaAnnotated).map(Class::getName) + .toList(), null); + } + + private static boolean isJakartaAnnotated(Class cls) { + + return cls.isAnnotationPresent(jakarta.persistence.Entity.class) + || cls.isAnnotationPresent(jakarta.persistence.Embeddable.class) + || cls.isAnnotationPresent(jakarta.persistence.MappedSuperclass.class) + || cls.isAnnotationPresent(jakarta.persistence.Converter.class); } public AotMetamodel(PersistenceManagedTypes 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 41bf6ff6e0..564b5a8093 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 @@ -22,9 +22,11 @@ import java.lang.reflect.Method; import java.util.Map; +import java.util.Optional; import org.jspecify.annotations.Nullable; +import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -70,6 +72,7 @@ public class JpaRepositoryContributor extends RepositoryContributor { private final PersistenceProvider persistenceProvider; private final QueriesFactory queriesFactory; private final EntityGraphLookup entityGraphLookup; + private final AotRepositoryContext context; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { this(repositoryContext, new AotMetamodel(repositoryContext)); @@ -87,9 +90,10 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityMa super(repositoryContext); + this.context = repositoryContext; this.metamodel = entityManagerFactory.getMetamodel(); this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); - this.queriesFactory = new QueriesFactory(entityManagerFactory); + this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory); this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory); } @@ -97,9 +101,11 @@ private JpaRepositoryContributor(AotRepositoryContext repositoryContext, AotMeta super(repositoryContext); + this.context = repositoryContext; this.metamodel = metamodel; this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(metamodel.getEntityManagerFactory()); - this.queriesFactory = new QueriesFactory(metamodel.getEntityManagerFactory(), metamodel); + this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), + metamodel.getEntityManagerFactory(), metamodel); this.entityGraphLookup = new EntityGraphLookup(metamodel.getEntityManagerFactory()); } @@ -111,25 +117,36 @@ protected void customizeClass(AotRepositoryClassBuilder classBuilder) { @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); - // TODO: Pick up the configured QueryEnhancerSelector + Optional> queryEnhancerSelector = getQueryEnhancerSelectorClass(); + constructorBuilder.customize(builder -> { - builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class); + + if (queryEnhancerSelector.isPresent()) { + builder.addStatement("super(new T$(), context)", queryEnhancerSelector.get()); + } else { + builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class); + } }); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Optional> getQueryEnhancerSelectorClass() { + return (Optional) context.getConfigurationSource().getAttribute("queryEnhancerSelector", Class.class) + .filter(it -> !it.equals(QueryEnhancerSelector.DefaultQueryEnhancerSelector.class)); + } + @Override protected @Nullable MethodContributor contributeQueryMethod(Method method) { JpaQueryMethod queryMethod = new JpaQueryMethod(method, getRepositoryInformation(), getProjectionFactory(), persistenceProvider); - // meh! - QueryEnhancerSelector selector = QueryEnhancerSelector.DEFAULT_SELECTOR; + Optional> queryEnhancerSelectorClass = getQueryEnhancerSelectorClass(); + QueryEnhancerSelector selector = queryEnhancerSelectorClass.map(BeanUtils::instantiateClass) + .orElse(QueryEnhancerSelector.DEFAULT_SELECTOR); // no stored procedures for now. if (queryMethod.isProcedureQuery()) { @@ -183,7 +200,6 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB TypeInformation returnType = getRepositoryInformation().getReturnType(method); boolean returnsCount = JpaCodeBlocks.QueryExecutionBlockBuilder.returnsModifying(returnType.getType()); - boolean isVoid = ClassUtils.isVoidType(returnType.getType()); if (!returnsCount && !isVoid) { 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 ee26bf0d06..299dc47412 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 @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -37,6 +38,7 @@ 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.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; @@ -53,14 +55,21 @@ class QueriesFactory { private final EntityManagerFactory entityManagerFactory; private final Metamodel metamodel; + private final EscapeCharacter escapeCharacter; + private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; - public QueriesFactory(EntityManagerFactory entityManagerFactory) { - this(entityManagerFactory, entityManagerFactory.getMetamodel()); + public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory) { + this(configurationSource, entityManagerFactory, entityManagerFactory.getMetamodel()); } - public QueriesFactory(EntityManagerFactory entityManagerFactory, Metamodel metamodel) { + public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory, + Metamodel metamodel) { + this.metamodel = metamodel; this.entityManagerFactory = entityManagerFactory; + + Optional escapeCharacter = configurationSource.getAttribute("escapeCharacter", Character.class); + this.escapeCharacter = escapeCharacter.map(EscapeCharacter::of).orElse(EscapeCharacter.DEFAULT); } /** @@ -77,8 +86,7 @@ public AotQueries createQueries(RepositoryInformation repositoryInformation, Mer QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { - return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, - queryMethod); + return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, queryMethod); } TypedQueryReference namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName()); @@ -201,9 +209,6 @@ private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInfor MergedAnnotation query, JpaQueryMethod queryMethod) { PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); - // TODO make configurable - JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; - AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates); if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { @@ -222,8 +227,7 @@ private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInfor private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, JpqlQueryTemplates templates) { - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - templates); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, escapeCharacter, templates); JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, false, returnedType, metadataProvider, templates, metamodel); @@ -234,8 +238,7 @@ private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaPa private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, JpqlQueryTemplates templates) { - ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT, - templates); + ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, escapeCharacter, templates); JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, metamodel); 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 360b449be7..95495accb4 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 @@ -69,7 +69,6 @@ import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; -import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -339,38 +338,34 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi Environment environment = repositoryContext.getEnvironment(); - boolean enabled = Boolean.parseBoolean( - environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + boolean enabled = Boolean + .parseBoolean(environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); if (!enabled) { return null; } ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); - boolean useEntityManager = Boolean.parseBoolean( - environment.getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); + boolean useEntityManager = Boolean + .parseBoolean(environment.getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); if (useEntityManager) { - ObjectProvider unitManagerProvider = beanFactory - .getBeanProvider(PersistenceUnitManager.class); - PersistenceUnitManager unitManager = unitManagerProvider.getIfAvailable(); + Optional entityManagerFactoryRef = repositoryContext.getConfigurationSource() + .getAttribute("entityManagerFactoryRef"); - if (unitManager != null) { + log.debug( + "Using EntityManager '%s' for AOT repository generation".formatted(entityManagerFactoryRef.orElse(""))); - log.debug("Using PersistenceUnitManager for AOT repository generation"); - return new JpaRepositoryContributor(repositoryContext, unitManager.obtainDefaultPersistenceUnitInfo()); - } - - log.debug("Using EntityManager for AOT repository generation"); - - EntityManagerFactory emf = beanFactory.getBean(EntityManagerFactory.class); + EntityManagerFactory emf = entityManagerFactoryRef + .map(it -> beanFactory.getBean(it, EntityManagerFactory.class)) + .orElseGet(() -> beanFactory.getBean(EntityManagerFactory.class)); return new JpaRepositoryContributor(repositoryContext, emf); } ObjectProvider managedTypesProvider = beanFactory .getBeanProvider(PersistenceManagedTypes.class); - PersistenceManagedTypes managedTypes = managedTypesProvider.getIfAvailable(); + PersistenceManagedTypes managedTypes = managedTypesProvider.getIfUnique(); if (managedTypes != null) { @@ -379,7 +374,7 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi } ObjectProvider infoProvider = beanFactory.getBeanProvider(PersistenceUnitInfo.class); - PersistenceUnitInfo unitInfo = infoProvider.getIfAvailable(); + PersistenceUnitInfo unitInfo = infoProvider.getIfUnique(); if (unitInfo != null) { 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 b73f9cc0d8..87243096db 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 @@ -21,6 +21,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import org.mockito.Mockito; + import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; @@ -29,11 +31,18 @@ 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.DefaultBeanNameGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportResource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.sample.SampleConfig; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -56,8 +65,15 @@ class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { private final TestJpaAotRepositoryContext repositoryContext; public AotFragmentTestConfigurationSupport(Class repositoryInterface) { + this(repositoryInterface, SampleConfig.class); + } + + public AotFragmentTestConfigurationSupport(Class repositoryInterface, Class configClass) { this.repositoryInterface = repositoryInterface; - this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, null); + this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, null, + new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(configClass), + EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), + Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java new file mode 100644 index 0000000000..97334b00cb --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java @@ -0,0 +1,74 @@ +/* + * 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.aot.hint.RuntimeHints; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.UrlResource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +/** + * Integration tests for the {@link UserRepository} AOT fragment. + * + * @author Mark Paluch + */ +class JpaRepositoryContributorConfigurationTests { + + @Configuration + static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + public JpaRepositoryContributorConfiguration() { + super(UserRepository.class, MyConfiguration.class); + } + + @EnableJpaRepositories(escapeCharacter = 'ö', /* avoid creating repository instances */ includeFilters = { + @ComponentScan.Filter(value = EnableJpaRepositories.class) }) + static class MyConfiguration { + + } + } + + @Test // GH-3838 + void shouldConsiderConfiguration() throws IOException { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(JpaRepositoryContributorConfiguration.class); + context.refreshForAotProcessing(new RuntimeHints()); + + String location = UserRepository.class.getPackageName().replace('.', '/') + "/" + + UserRepository.class.getSimpleName() + ".json"; + UrlResource resource = new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location)); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'streamByLastnameLike')].query").isArray().first().isObject() + .containsEntry("query", + "SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname LIKE :lastname ESCAPE 'ö'"); + } + +} 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 6fc63defab..b46528c4a8 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 @@ -32,6 +32,7 @@ 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.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.lang.Nullable; @@ -45,9 +46,12 @@ public class TestJpaAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; private final Class repositoryInterface; + private final RepositoryConfigurationSource configurationSource; - public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition, + RepositoryConfigurationSource configurationSource) { this.repositoryInterface = repositoryInterface; + this.configurationSource = configurationSource; this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } @@ -85,6 +89,11 @@ public String getModuleName() { return "JPA"; } + @Override + public RepositoryConfigurationSource getConfigurationSource() { + return configurationSource; + } + @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 d75531c507..82c06f9687 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 @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.config; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -46,6 +47,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.AotRepositoryInformation; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.javapoet.ClassName; @@ -155,6 +157,11 @@ public String getModuleName() { return "JPA"; } + @Override + public RepositoryConfigurationSource getConfigurationSource() { + return mock(RepositoryConfigurationSource.class); + } + @Override public Set getBasePackages() { return Collections.singleton(this.getClass().getPackageName()); diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc index 031a75f527..e8d2d4a1a8 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -53,6 +53,12 @@ For instance, profiles that have been enabled at build-time are automatically en Also, the Spring Data module implementing a repository is fixed. Changing the implementation requires AOT re-processing. +NOTE: AOT processing avoids database access. +Therefore, it initializes an in-memory Hibernate instance for metadata collection. +Types for the Hibernate configuration are determined by our AOT metadata collector. +We prefer using a `PersistentEntityTypes` bean if available and fall back to `PersistenceUnitInfo` or our own discovered types. +If our type scanning is not sufficient for your arrangement, you can enable direct `EntityManagerFactory` usage by configuring the `spring.aot.jpa.repositories.use-entitymanager=true` property. + === Eligible Methods AOT repositories filter methods that are eligible for AOT processing. @@ -69,7 +75,6 @@ These are typically all query methods that are not backed by an xref:repositorie * 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. @@ -79,8 +84,7 @@ Mind that using Value Expressions requires expression parsing and contextual inf **Excluded methods** -* `CrudRepository` and other base interface methods -* Querydsl and Query by Example methods +* `CrudRepository`, Querydsl, Query by Example, and other base interface methods as their implementation is provided by the base class respective fragments * Methods whose implementation would be overly complex ** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) ** Stored procedure query methods annotated with `@Procedure` From e063f1691eae755b46633815754a455e2b217cfa Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:15:56 +0200 Subject: [PATCH 101/224] Prepare 4.0 M3 (2025.1.0). See #3854 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index fc3327e671..49b89d26db 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-M3 @@ -39,7 +39,7 @@ 9.2.0 42.7.5 23.8.0.25.04 - 4.0.0-SNAPSHOT + 4.0.0-M3 0.10.3 org.hibernate @@ -284,20 +284,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 5df7b98122c1c7cec84d36063fe518778d4f79c9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:16:17 +0200 Subject: [PATCH 102/224] Release version 4.0 M3 (2025.1.0). See #3854 --- 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 49b89d26db..432431b4a2 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-M3 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 43c08369f6..9217586dda 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-M3 org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M3 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..36792376b9 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-M3 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index cdb738558f..93eb2f9f83 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-M3 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-M3 ../pom.xml From 7bfb52717977bde8a5be0db2a39954e15fbbb668 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:18:46 +0200 Subject: [PATCH 103/224] Prepare next development iteration. See #3854 --- 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 432431b4a2..49b89d26db 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M3 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 9217586dda..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-M3 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-M3 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 36792376b9..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-M3 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 93eb2f9f83..cdb738558f 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-M3 + 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-M3 + 4.0.0-SNAPSHOT ../pom.xml From c35e30ed5284a364e5330cb02bdc78470b4396d2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:18:48 +0200 Subject: [PATCH 104/224] After release cleanups. See #3854 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 49b89d26db..b9e288fdfe 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M3 + 4.0.0-SNAPSHOT @@ -39,7 +39,7 @@ 9.2.0 42.7.5 23.8.0.25.04 - 4.0.0-M3 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -284,8 +284,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From 936bb31f3a3b0e2506b017c1b04eee3e7d00443a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 21 May 2025 13:47:07 +0200 Subject: [PATCH 105/224] Upgrade to Hibernate 7.0 Final. Closes: #3896 --- pom.xml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b9e288fdfe..b82816e812 100755 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,9 @@ 4.13.2 5.0.0-B07 5.0.0-SNAPSHOT - 7.0.0.CR2 - 7.0.0-SNAPSHOT + 7.0.0.Final + 7.0.1-SNAPSHOT + 7.1.0-SNAPSHOT 2.7.4

        2.3.232

        3.2.0 @@ -88,6 +89,22 @@ + + hibernate-71-snapshots + + ${hibernate-71-snapshots} + 3.2.0 + + + + sonatype-oss + https://oss.sonatype.org/content/repositories/snapshots + + false + + + + all-dbs From d3e088f5b93f49c98b0bf1864294e803b086cc17 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 2 Jun 2025 10:29:00 +0200 Subject: [PATCH 106/224] Add missing reflection hint for DefaultQueryEnhancerSelector. Closes: #3905 --- .../data/jpa/repository/aot/JpaRuntimeHints.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 3b00237d29..b7b7b15cc1 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 @@ -34,6 +34,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector.DefaultQueryEnhancerSelector; import org.springframework.data.jpa.repository.support.QuerydslJpaPredicateExecutor; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; @@ -69,6 +70,11 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) MemberCategory.INVOKE_DECLARED_METHODS)); } + // via JpaRepositoryFactoryBean creating the bean if not defined + hints.reflection().registerType(TypeReference.of(DefaultQueryEnhancerSelector.class), + hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS)); + hints.reflection().registerType(TypeReference.of(SimpleJpaRepository.class), hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)); From 1cad9074fddae00e435ba2c9f6f7b8ba7b3947c4 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 2 Jun 2025 10:30:34 +0200 Subject: [PATCH 107/224] Resolve deprecation warnings in JpaRuntimeHints. See: #3905 --- .../data/jpa/repository/aot/JpaRuntimeHints.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 b7b7b15cc1..3acd19fd00 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 @@ -21,9 +21,8 @@ import java.util.Collections; import java.util.List; -import org.springframework.aot.hint.ExecutableMode; - import org.jspecify.annotations.Nullable; +import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -84,7 +83,7 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) // make sure annotations on the fields are visible and allow reflection on protected methods hints.reflection().registerTypes( List.of(TypeReference.of(AbstractPersistable.class), TypeReference.of(AbstractAuditable.class)), - hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS)); + hint -> hint.withMembers(MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS)); if (QuerydslUtils.QUERY_DSL_PRESENT) { @@ -94,9 +93,8 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) } // streaming results requires reflective access to jakarta.persistence.Query#getResultAsStream - hints.reflection().registerType(jakarta.persistence.Query.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - hints.reflection().registerType(jakarta.persistence.Query.class, hint -> - hint.withMethod("getResultStream", Collections.emptyList(), ExecutableMode.INVOKE)); + hints.reflection().registerType(jakarta.persistence.Query.class, + hint -> hint.withMethod("getResultStream", Collections.emptyList(), ExecutableMode.INVOKE)); hints.reflection().registerType(NamedEntityGraph.class, hint -> hint.onReachableType(EntityGraph.class).withMembers(MemberCategory.INVOKE_PUBLIC_METHODS)); From b37a334549411e3f6449d4bbd053c40ff89deb3e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Jun 2025 16:53:04 +0200 Subject: [PATCH 108/224] Avoid capturing `?&` and `?|` as bind parameter markers. We now exclude `?&` and `?|` from being matched as JDBC-style parameter bind marker. Closes #3907 --- .../repository/query/PreprocessedQuery.java | 6 ++-- .../query/DefaultEntityQueryUnitTests.java | 30 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) 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 6f36ac80a3..b32a2b1ae3 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 @@ -176,7 +176,7 @@ enum ParameterBindingParser { INSTANCE; private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; - public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; + 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. @@ -264,7 +264,9 @@ PreprocessedQuery parse(String query, Function declaredQu Integer parameterIndex = getParameterIndex(parameterIndexString); String match = matcher.group(0); - if (JDBC_STYLE_PARAM.matcher(match).find()) { + Matcher jdbcStyleMatcher = JDBC_STYLE_PARAM.matcher(match); + + if (jdbcStyleMatcher.find()) { jdbcStyle = true; } 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 599fb05aa0..a4fd297beb 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 @@ -236,6 +236,21 @@ void rewritesPositionalLikeToUniqueParametersIfNecessary() { assertThat(bindings).hasSize(3); } + @Test // GH-3907 + void rewritesPositionalLikeToUniqueParametersIfNecessaryUsingPostgresJsonbOperator() { + + DefaultEntityQuery query = new TestEntityQuery( + "select '[\"x\", \"c\"]'::jsonb ?| '[\"x\", \"c\"]'::jsonb from User u where u.firstname like %?1 or u.firstname like ?1% or u.firstname = ?1", + true); + + assertThat(query.hasParameterBindings()).isTrue(); + assertThat(query.getQueryString()).isEqualTo( + "select '[\"x\", \"c\"]'::jsonb ?| '[\"x\", \"c\"]'::jsonb from User u where u.firstname like ?1 or u.firstname like ?2 or u.firstname = ?3"); + + List bindings = query.getParameterBindings(); + assertThat(bindings).hasSize(3); + } + @Test // GH-3041 void reusesNamedLikeBindingsWherePossible() { @@ -539,7 +554,6 @@ void treatsGreaterThanBindingAsSimpleBinding() { assertThat(bindings).hasSize(1); assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0)); - } @Test // DATAJPA-473 @@ -638,6 +652,20 @@ void shouldReplaceExpressionWithLikeParameters() { .isEqualTo("select a from A a where a.b LIKE :__$synthetic$__1 and a.c LIKE :__$synthetic$__2"); } + @Test // GH-3907 + void considersOnlyDedicatedPositionalBindMarkersAsSuch() { + + DefaultEntityQuery query = new TestEntityQuery( + "select '[\"x\", \"c\"]'::jsonb ?| array[?1]::text[] FROM foo WHERE foo BETWEEN ?1 and ?2", true); + + assertThat(query.getParameterBindings()).hasSize(2); + + query = new TestEntityQuery("select '[\"x\", \"c\"]'::jsonb ?& array[:foo]::text[] FROM foo WHERE foo = :bar", + true); + + assertThat(query.getParameterBindings()).hasSize(2); + } + @Test // DATAJPA-712, GH-3619 void shouldReplaceAllPositionExpressionParametersWithInClause() { From 6cb02db8c6f8cbd04b199cb8ab19c3d977e5cd73 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 2 Jun 2025 10:54:06 +0200 Subject: [PATCH 109/224] Refrain from rewriting queries without input properties. We now no longer attempt to rewrite the query if the target type doesn't define input properties (no-args constructor or multiple constructors). Closes #3895 --- .../DtoProjectionTransformerDelegate.java | 2 +- .../AbstractDtoQueryTransformerUnitTests.java | 133 ++++++++++++++++++ .../EqlDtoQueryTransformerUnitTests.java | 92 ++---------- .../HqlDtoQueryTransformerUnitTests.java | 90 ++---------- .../JpqlDtoQueryTransformerUnitTests.java | 90 ++---------- 5 files changed, 158 insertions(+), 249 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java 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 d57a83ab99..fd362c1e4d 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 @@ -42,7 +42,7 @@ public DtoProjectionTransformerDelegate(ReturnedType returnedType) { public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface() - || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { + || !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { return selectionList; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java new file mode 100644 index 0000000000..e72ded4fc7 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java @@ -0,0 +1,133 @@ +/* + * 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 java.lang.reflect.Method; + +import org.antlr.v4.runtime.tree.ParseTreeVisitor; +import org.junit.jupiter.api.Test; + +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; + +/** + * Support class for unit tests for {@link DtoProjectionTransformerDelegate}. + * + * @author Mark Paluch + */ +abstract class AbstractDtoQueryTransformerUnitTests

        > { + + JpaQueryMethod method = getMethod("dtoProjection"); + + @Test // GH-3076 + void shouldTranslateSingleProjectionToDto() { + + P parser = parse("SELECT p from Person p"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + } + + @Test // GH-3076 + void shouldRewriteQueriesWithSubselect() { + + P parser = parse("select u from User u left outer join u.roles r where r in (select r from Role r)"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "select new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3076 + void shouldNotRewriteQueriesWithoutProperties() { + + JpaQueryMethod method = getMethod("noProjection"); + P parser = parse("select u from User u"); + + QueryTokenStream visit = getTransformer(parser, method).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("select u from User u"); + } + + @Test // GH-3076 + void shouldNotTranslateConstructorExpressionQuery() { + + P parser = parse("SELECT NEW com.foo(p) from Person p"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW com.foo(p) from Person p"); + } + + @Test + void shouldTranslatePropertySelectionToDto() { + + P parser = parse("SELECT p.foo, p.bar, sum(p.age) from Person p"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + } + + private JpaQueryMethod getMethod(String name, Class... parameterTypes) { + + try { + Method method = MyRepo.class.getMethod(name, parameterTypes); + PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; + + return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), + new SpelAwareProxyProjectionFactory(), persistenceProvider); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + abstract P parse(String query); + + private ParseTreeVisitor getTransformer(P parser) { + return getTransformer(parser, method); + } + + abstract ParseTreeVisitor getTransformer(P parser, QueryMethod method); + + interface MyRepo extends Repository { + + MyRecord dtoProjection(); + + EmptyClass noProjection(); + } + + record Person(String id) { + + } + + record MyRecord(String foo, String bar) { + + } + + static class EmptyClass { + + } +} 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 b8ac4f35a3..967af726e6 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 @@ -15,102 +15,26 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; /** * Unit tests for {@link DtoProjectionTransformerDelegate}. * * @author Mark Paluch */ -class EqlDtoQueryTransformerUnitTests { - - JpaQueryMethod method = getMethod("dtoProjection"); - - @Test // GH-3076 - void shouldTranslateSingleProjectionToDto() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); - } - - @Test // GH-3076 - void shouldRewriteQueriesWithSubselect() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "select new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); - } - - @Test // GH-3076 - void shouldNotTranslateConstructorExpressionQuery() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("SELECT NEW Foo(p) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p"); - } - - @Test - void shouldTranslatePropertySelectionToDto() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); +class EqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests { - EqlSortedQueryTransformer transformer = getTransformer(parser); - QueryTokenStream visit = transformer.visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + @Override + JpaQueryEnhancer.EqlQueryParser parse(String query) { + return JpaQueryEnhancer.EqlQueryParser.parseQuery(query); } - private JpaQueryMethod getMethod(String name, Class... parameterTypes) { - - try { - Method method = MyRepo.class.getMethod(name, parameterTypes); - PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; - - return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), - new SpelAwareProxyProjectionFactory(), persistenceProvider); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private EqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.EqlQueryParser parser) { + @Override + ParseTreeVisitor getTransformer(JpaQueryEnhancer.EqlQueryParser parser, QueryMethod method) { return new EqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), method.getResultProcessor().getReturnedType()); } - - interface MyRepo extends Repository { - - MyRecord dtoProjection(); - } - - record Person(String id) { - - } - - record MyRecord(String foo, String bar) { - - } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java index 9afdcd9764..4ac9f6a9c5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java @@ -15,101 +15,27 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; /** * Unit tests for {@link DtoProjectionTransformerDelegate}. * * @author Mark Paluch */ -class HqlDtoQueryTransformerUnitTests { - - JpaQueryMethod method = getMethod("dtoProjection"); - - @Test // GH-3076 - void shouldTranslateSingleProjectionToDto() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p"); +class HqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests { - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + @Override + JpaQueryEnhancer.HqlQueryParser parse(String query) { + return JpaQueryEnhancer.HqlQueryParser.parseQuery(query); } - @Test // GH-3076 - void shouldRewriteQueriesWithSubselect() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser - .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "select new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); - } - - @Test // GH-3076 - void shouldNotTranslateConstructorExpressionQuery() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser - .parseQuery("SELECT NEW String(p) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); - } - - @Test - void shouldTranslatePropertySelectionToDto() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser - .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); - } - - private JpaQueryMethod getMethod(String name, Class... parameterTypes) { - - try { - Method method = MyRepo.class.getMethod(name, parameterTypes); - PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; - - return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), - new SpelAwareProxyProjectionFactory(), persistenceProvider); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private HqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.HqlQueryParser parser) { + @Override + ParseTreeVisitor getTransformer(JpaQueryEnhancer.HqlQueryParser parser, QueryMethod method) { return new HqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), method.getResultProcessor().getReturnedType()); } - interface MyRepo extends Repository { - - MyRecord dtoProjection(); - } - - record Person(String id) { - - } - - record MyRecord(String foo, String bar) { - - } } 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 d0c8fa1305..a82aaf7581 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 @@ -15,101 +15,27 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; /** * Unit tests for {@link DtoProjectionTransformerDelegate}. * * @author Mark Paluch */ -class JpqlDtoQueryTransformerUnitTests { - - JpaQueryMethod method = getMethod("dtoProjection"); - - @Test // GH-3076 - void shouldTranslateSingleProjectionToDto() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("SELECT p from Person p"); +class JpqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests { - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + @Override + JpaQueryEnhancer.JpqlQueryParser parse(String query) { + return JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); } - @Test // GH-3076 - void shouldRewriteQueriesWithSubselect() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "select new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); - } - - @Test // GH-3076 - void shouldNotTranslateConstructorExpressionQuery() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("SELECT NEW Foo(p) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p"); - } - - @Test - void shouldTranslatePropertySelectionToDto() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); - } - - private JpaQueryMethod getMethod(String name, Class... parameterTypes) { - - try { - Method method = MyRepo.class.getMethod(name, parameterTypes); - PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; - - return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), - new SpelAwareProxyProjectionFactory(), persistenceProvider); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private JpqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.JpqlQueryParser parser) { + @Override + ParseTreeVisitor getTransformer(JpaQueryEnhancer.JpqlQueryParser parser, QueryMethod method) { return new JpqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), method.getResultProcessor().getReturnedType()); } - interface MyRepo extends Repository { - - MyRecord dtoProjection(); - } - - record Person(String id) { - - } - - record MyRecord(String foo, String bar) { - - } } From 2f620400d5b62a9d6b8337fa3074c42213da65a0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 2 Jun 2025 11:24:04 +0200 Subject: [PATCH 110/224] Do not consider JPA-managed types projections. We now back off from rewriting queries to constructor expressions if a returned type is a JPA-managed one. See #3895 --- .../query/AbstractStringBasedJpaQuery.java | 10 +- .../query/SimpleJpaQueryUnitTests.java | 101 ++++++++++++++++-- 2 files changed, 97 insertions(+), 14 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 c288d4a350..75173a438a 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 @@ -158,13 +158,12 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { * @param processor * @return */ - private ReturnedType getReturnedType(ResultProcessor processor) { + ReturnedType getReturnedType(ResultProcessor processor) { ReturnedType returnedType = processor.getReturnedType(); Class returnedJavaType = processor.getReturnedType().getReturnedType(); - if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface() - || query.isNative()) { + if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNative()) { return returnedType; } @@ -174,13 +173,16 @@ private ReturnedType getReturnedType(ResultProcessor processor) { return returnedType; } - if ((known != null && !known) || returnedJavaType.isArray()) { + if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) { if (known == null) { knownProjections.put(returnedJavaType, false); } return new NonProjectingReturnedType(returnedType); } + if (query.isDefaultProjection()) { + return returnedType; + } String projectionToUse = query.<@Nullable String> doWithEnhancer(queryEnhancer -> { String alias = queryEnhancer.detectAlias(); 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 188166d3bd..43022e6bbb 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 @@ -22,12 +22,14 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; +import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.Metamodel; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; @@ -43,6 +45,7 @@ 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.jpa.domain.sample.User; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.NativeQuery; @@ -50,9 +53,12 @@ import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +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.query.Param; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; @@ -86,7 +92,7 @@ class SimpleJpaQueryUnitTests { @Mock QueryExtractor extractor; @Mock jakarta.persistence.Query query; @Mock TypedQuery typedQuery; - @Mock RepositoryMetadata metadata; + RepositoryMetadata metadata; @Mock ParameterBinder binder; @Mock Metamodel metamodel; @@ -102,12 +108,8 @@ void setUp() throws SecurityException, NoSuchMethodException { when(em.getEntityManagerFactory()).thenReturn(emf); when(em.getDelegate()).thenReturn(em); when(emf.createEntityManager()).thenReturn(em); - when(metadata.getRepositoryInterface()).thenReturn((Class) SampleRepository.class); - when(metadata.getDomainType()).thenReturn((Class) User.class); - when(metadata.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(User.class)); - when(metadata.getReturnedDomainClass(Mockito.any(Method.class))).thenReturn((Class) User.class); - when(metadata.getReturnType(Mockito.any(Method.class))) - .thenAnswer(invocation -> TypeInformation.fromReturnTypeOf(invocation.getArgument(0))); + + metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); Method setUp = UserRepository.class.getMethod("findByLastname", String.class); method = new JpaQueryMethod(setUp, metadata, factory, extractor); @@ -157,7 +159,6 @@ void discoversNativeQuery() throws Exception { assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); when(em.createNativeQuery(anyString(), eq(User.class))).thenReturn(query); - when(metadata.getReturnedDomainClass(method)).thenReturn((Class) User.class); jpaQuery.createQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { "Matthews" })); @@ -176,7 +177,6 @@ void discoversNativeQueryFromNativeQueryInterface() throws Exception { assertThat(jpaQuery).isInstanceOf(NativeJpaQuery.class); when(em.createNativeQuery(anyString(), eq(User.class))).thenReturn(query); - when(metadata.getReturnedDomainClass(method)).thenReturn((Class) User.class); jpaQuery.createQuery(new JpaParametersParameterAccessor(queryMethod.getParameters(), new Object[] { "Matthews" })); @@ -264,6 +264,67 @@ void projectsWithManuallyDeclaredQuery() throws Exception { verify(em, times(2)).createQuery(anyString()); } + @Test // GH-3895 + void doesNotRewriteQueryReturningEntity() throws Exception { + + EntityType entityType = mock(EntityType.class); + when(entityType.getJavaType()).thenReturn((Class) UnrelatedType.class); + when(metamodel.getManagedTypes()).thenReturn(Set.of(entityType)); + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("selectWithJoin")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd"); + } + + @Test // GH-3895 + void rewriteQueryReturningDto() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("selectWithJoin")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith( + "SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)"); + } + + @Test // GH-3895 + void doesNotRewriteQueryForUnknownProperty() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("projectWithUnknownPaths")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith("select u.unknown from User u"); + } + + @Test // GH-3895 + void doesNotRewriteQueryForJoinPath() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("projectWithJoinPaths")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r"); + } + @Test // DATAJPA-1307 void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception { @@ -311,7 +372,7 @@ private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional { @Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true) List findNativeByLastname(String lastname); @@ -337,6 +398,20 @@ interface SampleRepository { @Query("select u from User u") Collection projectWithExplicitQuery(); + @Query(""" + SELECT cd FROM CampaignDeal cd + LEFT JOIN FETCH cd.dealLibrary d + LEFT JOIN FETCH d.publisher p + WHERE cd.campaignId = :campaignId + """) + Collection selectWithJoin(); + + @Query("select u.unknown from User u") + Collection projectWithUnknownPaths(); + + @Query("select r.name from User u LEFT JOIN FETCH u.roles r") + Collection projectWithJoinPaths(); + @Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u") List findAllWithExpressionInCountQuery(Pageable pageable); @@ -350,4 +425,10 @@ interface SampleRepository { } interface UserProjection {} + + static class UnrelatedType { + + public UnrelatedType(String name) {} + + } } From 205912ccc27951fc2e4aaa2ca7e5a63fc656c36e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Jun 2025 11:15:01 +0200 Subject: [PATCH 111/224] Fix potential class-cast exception. See #3895 --- .../query/AbstractStringBasedJpaQuery.java | 2 -- .../data/jpa/repository/query/QueryRenderer.java | 14 ++++++++++++++ .../ROOT/pages/repositories/projections.adoc | 7 +++++-- 3 files changed, 19 insertions(+), 4 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 75173a438a..193a255bb8 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 @@ -37,7 +37,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. @@ -71,7 +70,6 @@ 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 countQuery can be {@literal null} if not defined. * @param queryConfiguration must not be {@literal null}. */ AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, 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 5c0969ea2b..5bd645a11b 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 @@ -175,6 +175,10 @@ public static QueryRenderer ofExpression(QueryTokenStream tokenStream) { return EmptyQueryRenderer.INSTANCE; } + if (!(tokenStream instanceof QueryRenderer)) { + tokenStream = QueryRenderer.from(tokenStream); + } + if (tokenStream.isExpression()) { return (QueryRenderer) tokenStream; } @@ -192,6 +196,10 @@ public static QueryRenderer inline(QueryTokenStream tokenStream) { return EmptyQueryRenderer.INSTANCE; } + if (!(tokenStream instanceof QueryRenderer)) { + tokenStream = QueryRenderer.from(tokenStream); + } + if (!tokenStream.isExpression()) { return (QueryRenderer) tokenStream; } @@ -323,6 +331,12 @@ public int size() { public boolean isExpression() { return !nested.isEmpty() && nested.get(nested.size() - 1).isExpression(); } + + public Stream renderers() { + return nested.stream() + .flatMap(renderer -> renderer instanceof CompositeRenderer ? ((CompositeRenderer) renderer).renderers() + : Stream.of(renderer)); + } } /** diff --git a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc index 0eb4682ff2..a9df80376a 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -32,9 +32,12 @@ Support for string-based queries covers both, JPQL queries(`@Query`) and native ==== JPQL Queries -When using <> with JPQL, you must use *constructor expressions* in your JPQL query, e.g. `SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u`. +JPA's mechanism to return <> using JPQL is *constructor expressions*. +Therefore, your query must define a constructor expression such as `SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u`. (Note the usage of a FQDN for the DTO type!) This JPQL expression can be used in `@Query` annotations as well where you define any named queries. -As a workaround you may use named queries with `ResultSetMapping` or the Hibernate-specific javadoc:{hibernatejavadocurl}org.hibernate.query.ResultListTransformer[] +As a workaround you may use named queries with `ResultSetMapping` or the Hibernate-specific javadoc:{hibernatejavadocurl}org.hibernate.query.ResultListTransformer[]. + +Spring Data JPA can aid with rewriting your query to a constructor expression if your query selects the primary entity or a list of select items. ===== DTO Projection JPQL Query Rewriting From ea9e6e15bbdce7faffedd22d9e5e6eefa87e52c0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Jun 2025 11:16:40 +0200 Subject: [PATCH 112/224] Refine DTO projection rewriting. We now consider dropping aliases (count(foo) as foo), support subselects and capture individual select items to avoid contextual information loss. Also, added a series of tests to cover edgecases. See #3895 --- .../repository/query/AbstractJpaQuery.java | 1 + .../query/AbstractStringBasedJpaQuery.java | 53 +--- .../DtoProjectionTransformerDelegate.java | 102 +++++-- .../query/EqlCountQueryTransformer.java | 4 +- .../query/EqlQueryIntrospector.java | 4 +- .../repository/query/EqlQueryRenderer.java | 11 +- .../query/EqlSortedQueryTransformer.java | 76 +++-- .../query/HqlCountQueryTransformer.java | 3 +- .../query/HqlOrderExpressionVisitor.java | 2 +- .../repository/query/HqlQueryRenderer.java | 2 +- .../query/HqlSortedQueryTransformer.java | 19 +- .../repository/query/JpqlQueryBuilder.java | 3 +- .../repository/query/JpqlQueryRenderer.java | 13 +- .../query/JpqlSortedQueryTransformer.java | 79 ++--- .../jpa/repository/query/QueryRenderer.java | 7 +- .../EclipseLinkUserRepositoryFinderTests.java | 4 - ...ipseLinkUserRepositoryProjectionTests.java | 32 ++ .../repository/UserRepositoryFinderTests.java | 165 ---------- .../UserRepositoryProjectionTests.java | 286 ++++++++++++++++++ .../AbstractDtoQueryTransformerUnitTests.java | 38 ++- .../query/SimpleJpaQueryUnitTests.java | 38 ++- .../jpa/repository/sample/UserRepository.java | 17 +- 22 files changed, 606 insertions(+), 353 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.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 ad0cafba95..f70210ff84 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 @@ -24,6 +24,7 @@ import jakarta.persistence.TypedQuery; import java.lang.reflect.Constructor; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.function.UnaryOperator; 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 193a255bb8..95965b2693 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 @@ -29,8 +29,6 @@ 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; @@ -178,56 +176,7 @@ ReturnedType getReturnedType(ResultProcessor processor) { return new NonProjectingReturnedType(returnedType); } - if (query.isDefaultProjection()) { - return 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); - } - } - + knownProjections.put(returnedJavaType, true); return returnedType; } 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 fd362c1e4d..28de9ba657 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 @@ -17,6 +17,11 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + import org.springframework.data.repository.query.ReturnedType; /** @@ -25,7 +30,8 @@ * Query rewriting from a plain property/object selection towards constructor expression only works if either: *

          *
        • The query selects its primary alias ({@code SELECT p FROM Person p})
        • - *
        • The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})
        • + *
        • The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p}, + * {@code SELECT COUNT(p.foo), p.bar AS bar FROM Person p})
        • *
        * * @author Mark Paluch @@ -34,42 +40,94 @@ class DtoProjectionTransformerDelegate { private final ReturnedType returnedType; + private final boolean applyRewriting; + private final List selectItems = new ArrayList<>(); public DtoProjectionTransformerDelegate(ReturnedType returnedType) { this.returnedType = returnedType; + this.applyRewriting = returnedType.isProjecting() && !returnedType.getReturnedType().isInterface() + && returnedType.needsCustomConstruction(); + } + + public boolean applyRewriting() { + return applyRewriting; + } + + public boolean canRewrite() { + return applyRewriting() && !selectItems.isEmpty(); + } + + public void appendSelectItem(QueryTokenStream selectItem) { + + if (applyRewriting()) { + selectItems.add(new DetachedStream(selectItem)); + } } - public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { + public QueryTokenStream getRewrittenSelectionList() { + + if (canRewrite()) { + + QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.TOKEN_NEW); + builder.append(QueryTokens.token(returnedType.getReturnedType().getName())); + builder.append(QueryTokens.TOKEN_OPEN_PAREN); + + if (selectItems.size() == 1 && selectItems.get(0).size() == 1) { - if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface() - || !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { - return selectionList; + builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> { + + QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder(); + prop.appendInline(selectItems.get(0)); + prop.append(QueryTokens.TOKEN_DOT); + prop.append(QueryTokens.token(property)); + + return prop.build(); + }, QueryTokens.TOKEN_COMMA)); + } else { + builder.append(QueryTokenStream.concat(selectItems, Function.identity(), TOKEN_COMMA)); + } + + builder.append(TOKEN_CLOSE_PAREN); + + return builder.build(); } - QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.TOKEN_NEW); - builder.append(QueryTokens.token(returnedType.getReturnedType().getName())); - builder.append(QueryTokens.TOKEN_OPEN_PAREN); + return QueryTokenStream.empty(); + } + + private static class DetachedStream extends QueryRenderer { - // assume the selection points to the document - if (selectionList.size() == 1) { + private final QueryTokenStream delegate; - builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> { + private DetachedStream(QueryTokenStream delegate) { + this.delegate = delegate; + } - QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder(); - prop.append(QueryTokens.token(selectionList.getRequiredFirst().value())); - prop.append(QueryTokens.TOKEN_DOT); - prop.append(QueryTokens.token(property)); + @Override + public boolean isExpression() { + return delegate.isExpression(); + } - return prop.build(); - }, QueryTokens.TOKEN_COMMA)); + @Override + public int size() { + return delegate.size(); + } - } else { - builder.appendInline(selectionList); + @Override + public boolean isEmpty() { + return delegate.isEmpty(); } - builder.append(QueryTokens.TOKEN_CLOSE_PAREN); + @Override + public Iterator iterator() { + return delegate.iterator(); + } - return builder.build(); + @Override + public String render() { + return delegate instanceof QueryRenderer ? ((QueryRenderer) delegate).render() : delegate.toString(); + } } + } 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 2d8e27c167..7b2aa13fda 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 @@ -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; /** 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 fa7fa5ec8e..5f261a16bb 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 @@ -21,10 +21,10 @@ import java.util.Collections; import java.util.List; -import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; - import org.jspecify.annotations.Nullable; +import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; + /** * {@link ParsedQueryIntrospector} for EQL queries. * 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 51767d9c90..cadc135b79 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 @@ -612,6 +612,15 @@ public QueryTokenStream visitDelete_clause(EqlParser.Delete_clauseContext ctx) { @Override public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = prepareSelectClause(ctx); + + builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); + + return builder; + } + + QueryRendererBuilder prepareSelectClause(EqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.SELECT())); @@ -620,8 +629,6 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); - return builder; } 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 30e9106d22..3c4bd92d72 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 @@ -19,9 +19,9 @@ import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; @@ -89,17 +89,53 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { return super.visitSelect_clause(ctx); } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = prepareSelectClause(ctx); + + QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + + if (dtoDelegate != null && dtoDelegate.canRewrite()) { + builder.append(dtoDelegate.getRewrittenSelectionList()); + } else { + builder.append(selectItems); + } + + return builder; + } + + @Override + public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) { - builder.append(QueryTokens.expression(ctx.SELECT())); + QueryTokenStream tokens = super.visitSelect_item(ctx); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (ctx.result_variable() != null && !tokens.isEmpty()) { + transformerSupport.registerAlias(ctx.result_variable().getText()); } - QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + return tokens; + } + + @Override + public QueryTokenStream visitSelect_expression(EqlParser.Select_expressionContext ctx) { + + QueryTokenStream selectItem = super.visitSelect_expression(ctx); + + if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + dtoDelegate.appendSelectItem(selectItem); + } - return builder.append(dtoDelegate.transformSelectionList(tokenStream)); + return selectItem; + } + + @Override + public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { + + QueryTokenStream tokens = super.visitJoin(ctx); + + if (ctx.identification_variable() != null) { + transformerSupport.registerAlias(ctx.identification_variable().getText()); + } + + return tokens; } private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) { @@ -129,28 +165,4 @@ private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_state } } - @Override - public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) { - - QueryTokenStream tokens = super.visitSelect_item(ctx); - - if (ctx.result_variable() != null && !tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - } 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 e35b712589..a4bd3af55e 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 @@ -107,7 +107,7 @@ public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) { if (ctx.fromClause() != null) { builder.appendExpression(visit(ctx.fromClause())); - if(primaryFromAlias == null) { + if (primaryFromAlias == null) { builder.append(TOKEN_AS); builder.append(TOKEN_DOUBLE_UNDERSCORE); } @@ -150,7 +150,6 @@ public QueryRendererBuilder visitJoin(HqlParser.JoinContext ctx) { return builder; } - @Override public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { 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 1d73d078f3..e1ed4997f8 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 @@ -44,8 +44,8 @@ import org.antlr.v4.runtime.tree.ParseTree; 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; 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 01557b33d5..61f3fcf215 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 @@ -885,7 +885,7 @@ public QueryTokenStream visitSelection(HqlParser.SelectionContext ctx) { builder.appendExpression(visit(ctx.variable())); } - return builder; + return builder.toInline(); } @Override 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 175e918c80..99a677a41f 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 @@ -93,13 +93,25 @@ public QueryTokenStream visitSelectionList(HqlParser.SelectionListContext ctx) { QueryTokenStream tokenStream = super.visitSelectionList(ctx); - if (dtoDelegate != null && !isSubquery(ctx)) { - return dtoDelegate.transformSelectionList(tokenStream); + if (dtoDelegate != null && dtoDelegate.canRewrite() && !isSubquery(ctx)) { + return dtoDelegate.getRewrittenSelectionList(); } return tokenStream; } + @Override + public QueryTokenStream visitSelectExpression(HqlParser.SelectExpressionContext ctx) { + + QueryTokenStream selectItem = super.visitSelectExpression(ctx); + + if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.instantiation() == null && !isSubquery(ctx)) { + dtoDelegate.appendSelectItem(QueryRenderer.ofExpression(selectItem)); + } + + return selectItem; + } + @Override public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { @@ -123,7 +135,7 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { return tokens; } - + @Override public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) { @@ -135,7 +147,6 @@ public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext return tokens; } - @Override public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) { 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 e317528e8c..124df50346 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 @@ -945,7 +945,8 @@ public String getAlias(Origin source) { */ public String getAlias(Origin source) { - return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(), s -> !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/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index 03b87cdd34..3167e09514 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 @@ -594,6 +594,15 @@ public QueryTokenStream visitDelete_clause(JpqlParser.Delete_clauseContext ctx) @Override public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = prepareSelectClause(ctx); + + builder.appendExpression(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); + + return builder; + } + + QueryRendererBuilder prepareSelectClause(JpqlParser.Select_clauseContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.SELECT())); @@ -602,8 +611,6 @@ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.append(QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA)); - return builder; } @@ -2219,7 +2226,7 @@ public QueryTokenStream visitIdentification_variable(JpqlParser.Identification_v } else if (ctx.type_literal() != null) { return visit(ctx.type_literal()); } else if (ctx.f != null) { - return QueryTokenStream.ofToken(ctx.f); + return QueryRenderer.from(QueryTokens.expression(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 654fb7df88..807140c5b3 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 @@ -19,9 +19,9 @@ import java.util.List; -import org.springframework.data.domain.Sort; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; @@ -72,7 +72,7 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext builder.appendExpression(visit(ctx.having_clause())); } - if(ctx.set_fuction() != null) { + if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } else { doVisitOrderBy(builder, ctx); @@ -88,17 +88,53 @@ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) return super.visitSelect_clause(ctx); } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = prepareSelectClause(ctx); + + QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + + if (dtoDelegate != null && dtoDelegate.canRewrite()) { + builder.append(dtoDelegate.getRewrittenSelectionList()); + } else { + builder.append(selectItems); + } + + return builder; + } + + @Override + public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { + + QueryTokenStream tokens = super.visitSelect_item(ctx); + + if (ctx.result_variable() != null && !tokens.isEmpty()) { + transformerSupport.registerAlias(ctx.result_variable().getText()); + } + + return tokens; + } + + @Override + public QueryTokenStream visitSelect_expression(JpqlParser.Select_expressionContext ctx) { - builder.append(QueryTokens.expression(ctx.SELECT())); + QueryTokenStream selectItem = super.visitSelect_expression(ctx); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + dtoDelegate.appendSelectItem(selectItem); } - QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + return selectItem; + } + + @Override + public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { + + QueryTokenStream tokens = super.visitJoin(ctx); - return builder.append(dtoDelegate.transformSelectionList(tokenStream)); + if (ctx.identification_variable() != null) { + transformerSupport.registerAlias(ctx.identification_variable().getText()); + } + + return tokens; } private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { @@ -127,29 +163,4 @@ private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_stat } } } - - @Override - public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { - - QueryTokenStream tokens = super.visitSelect_item(ctx); - - if (ctx.result_variable() != null && !tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getRequiredLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - 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 5bd645a11b..402fca8f7d 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,10 +22,10 @@ import java.util.List; import java.util.stream.Stream; -import org.springframework.util.CompositeIterator; - import org.jspecify.annotations.Nullable; +import org.springframework.util.CompositeIterator; + /** * Abstraction to encapsulate query expressions and render a query. *

        @@ -622,6 +622,9 @@ public QueryRenderer build() { return current; } + public QueryRenderer toInline() { + return new InlineRenderer(current); + } } private static class InlineRenderer extends QueryRenderer { 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 31d4a44d42..9ad2fe8f3c 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 @@ -40,8 +40,4 @@ void executesInKeywordForPageCorrectly() {} @Override void shouldProjectWithKeysetScrolling() {} - @Disabled - @Override - void rawMapProjectionWithEntityAndAggregatedValue() {} - } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java new file mode 100644 index 0000000000..d4d6e2a14f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * @author Oliver Gierke + * @author Greg Turnquist + */ +@ContextConfiguration("classpath:eclipselink-h2.xml") +class EclipseLinkUserRepositoryProjectionTests extends UserRepositoryProjectionTests { + + @Disabled + @Override + void rawMapProjectionWithEntityAndAggregatedValue() {} + +} 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 46721b1dfb..2c73a64803 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 @@ -22,15 +22,11 @@ import java.util.Arrays; import java.util.List; -import java.util.Map; -import org.assertj.core.data.Offset; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -42,18 +38,12 @@ 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; import org.springframework.data.jpa.repository.sample.RoleRepository; import org.springframework.data.jpa.repository.sample.UserRepository; -import org.springframework.data.jpa.repository.sample.UserRepository.IdOnly; import org.springframework.data.jpa.repository.sample.UserRepository.NameOnly; -import org.springframework.data.jpa.repository.sample.UserRepository.RolesAndFirstname; -import org.springframework.data.jpa.repository.sample.UserRepository.UserExcerpt; -import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountDtoProjection; -import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountInterfaceProjection; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -350,29 +340,6 @@ void translatesNotContainsToNotMemberOf() { .containsExactlyInAnyOrder(dave, oliver); } - @Test // DATAJPA-974, GH-2815 - void executesQueryWithProjectionContainingReferenceToPluralAttribute() { - - List rolesAndFirstnameBy = userRepository.findRolesAndFirstnameBy(); - - assertThat(rolesAndFirstnameBy).isNotNull(); - - for (RolesAndFirstname rolesAndFirstname : rolesAndFirstnameBy) { - assertThat(rolesAndFirstname.getFirstname()).isNotNull(); - assertThat(rolesAndFirstname.getRoles()).isNotNull(); - } - } - - @Test // GH-2815 - void executesQueryWithProjectionThroughStringQuery() { - - List ids = userRepository.findIdOnly(); - - assertThat(ids).isNotNull(); - - assertThat(ids).extracting(IdOnly::getId).doesNotContainNull(); - } - @Test // DATAJPA-1023, DATACMNS-959 @Transactional(propagation = Propagation.NOT_SUPPORTED) void rejectsStreamExecutionIfNoSurroundingTransactionActive() { @@ -381,22 +348,6 @@ void rejectsStreamExecutionIfNoSurroundingTransactionActive() { .isThrownBy(() -> userRepository.findAllByCustomQueryAndStream()); } - @Test // DATAJPA-1334 - void executesNamedQueryWithConstructorExpression() { - userRepository.findByNamedQueryWithConstructorExpression(); - } - - @Test // DATAJPA-1713, GH-2008 - void selectProjectionWithSubselect() { - - List dtos = userRepository.findProjectionBySubselect(); - - assertThat(dtos).flatExtracting(UserRepository.NameOnly::getFirstname) // - .containsExactly("Dave", "Carter", "Oliver August"); - assertThat(dtos).flatExtracting(UserRepository.NameOnly::getLastname) // - .containsExactly("Matthews", "Beauford", "Matthews"); - } - @Test // GH-3675 void findBySimplePropertyUsingMixedNullNonNullArgument() { @@ -415,114 +366,6 @@ void findByNegatingSimplePropertyUsingMixedNullNonNullArgument() { assertThat(result).containsExactly(carter); } - @Test // GH-3076 - void dtoProjectionShouldApplyConstructorExpressionRewriting() { - - List dtos = userRepository.findRecordProjection(); - - 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 - void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() { - - List dtos = userRepository.findMultiselectRecordProjection(); - - assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // - .contains("Dave", "Carter", "Oliver August"); - } - - @Test // GH-3076 - void dynamicDtoProjection() { - - List dtos = userRepository.findRecordProjection(UserExcerpt.class); - - assertThat(dtos).flatExtracting(UserRepository.UserExcerpt::firstname) // - .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() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat(userRepository.dtoProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { - assertThat(projection.user()).isIn(musicians.values()); - assertThat(projection.roleCount()).isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), - Offset.offset(0L)); - }); - } - - @Test // GH-3076 - void interfaceProjectionWithEntityAndAggregatedValue() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat(userRepository.interfaceProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { - assertThat(projection.getUser()).isIn(musicians.values()); - assertThat(projection.getRoleCount()) - .isCloseTo(musicians.get(projection.getUser().getFirstname()).getRoles().size(), Offset.offset(0L)); - }); - } - - @Test // GH-3076 - void rawMapProjectionWithEntityAndAggregatedValue() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat(userRepository.rawMapProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { - assertThat(projection.get("user")).isIn(musicians.values()); - assertThat(projection).containsKey("roleCount"); - }); - } - - @Test // GH-3076 - void dtoProjectionWithEntityAndAggregatedValueWithPageable() { - - Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, - oliver.getFirstname(), oliver); - - assertThat( - userRepository.dtoProjectionEntityAndAggregatedValue(PageRequest.of(0, 10).withSort(Sort.by("firstname")))) - .allSatisfy(projection -> { - assertThat(projection.user()).isIn(musicians.values()); - assertThat(projection.roleCount()) - .isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), Offset.offset(0L)); - }); - } - @Test // GH-3857 void shouldApplyParameterNames() { @@ -531,12 +374,4 @@ void shouldApplyParameterNames() { oliver.getLastname())).hasSize(2); } - @ParameterizedTest // GH-3076 - @ValueSource(classes = { UserRoleCountDtoProjection.class, UserRoleCountInterfaceProjection.class }) - void dynamicProjectionWithEntityAndAggregated(Class resultType) { - - assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3) - .hasOnlyElementsOfType(resultType); - } - } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java new file mode 100644 index 0000000000..8771939ac4 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java @@ -0,0 +1,286 @@ +/* + * 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.*; + +import jakarta.persistence.EntityManager; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +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; +import org.springframework.data.jpa.repository.sample.RoleRepository; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.data.jpa.repository.sample.UserRepository.IdOnly; +import org.springframework.data.jpa.repository.sample.UserRepository.NameOnly; +import org.springframework.data.jpa.repository.sample.UserRepository.RolesAndFirstname; +import org.springframework.data.jpa.repository.sample.UserRepository.UserExcerpt; +import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountDtoProjection; +import org.springframework.data.jpa.repository.sample.UserRepository.UserRoleCountInterfaceProjection; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration test for executing projecting query methods. + * + * @author Oliver Gierke + * @author Krzysztof Krason + * @author Greg Turnquist + * @author Mark Paluch + * @author Christoph Strobl + * @see QueryLookupStrategy + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(locations = "classpath:config/namespace-application-context-h2.xml") +@Transactional +class UserRepositoryProjectionTests { + + @Autowired UserRepository userRepository; + @Autowired RoleRepository roleRepository; + @Autowired EntityManager em; + + PersistenceProvider provider; + + private User dave; + private User carter; + private User oliver; + private Role drummer; + private Role guitarist; + private Role singer; + + @BeforeEach + void setUp() { + + drummer = roleRepository.save(new Role("DRUMMER")); + guitarist = roleRepository.save(new Role("GUITARIST")); + singer = roleRepository.save(new Role("SINGER")); + + dave = userRepository.save(new User("Dave", "Matthews", "dave@dmband.com", singer)); + carter = userRepository.save(new User("Carter", "Beauford", "carter@dmband.com", singer, drummer)); + oliver = userRepository.save(new User("Oliver August", "Matthews", "oliver@dmband.com")); + + provider = PersistenceProvider.fromEntityManager(em); + } + + @AfterEach + void clearUp() { + + userRepository.deleteAll(); + roleRepository.deleteAll(); + } + + @Test // DATAJPA-974, GH-2815 + void executesQueryWithProjectionContainingReferenceToPluralAttribute() { + + List rolesAndFirstnameBy = userRepository.findRolesAndFirstnameBy(); + + assertThat(rolesAndFirstnameBy).isNotNull(); + + for (RolesAndFirstname rolesAndFirstname : rolesAndFirstnameBy) { + assertThat(rolesAndFirstname.getFirstname()).isNotNull(); + assertThat(rolesAndFirstname.getRoles()).isNotNull(); + } + } + + @Test // GH-2815 + void executesQueryWithProjectionThroughStringQuery() { + + List ids = userRepository.findIdOnly(); + + assertThat(ids).isNotNull(); + + assertThat(ids).extracting(IdOnly::getId).doesNotContainNull(); + } + + @Test // DATAJPA-1334 + void executesNamedQueryWithConstructorExpression() { + userRepository.findByNamedQueryWithConstructorExpression(); + } + + @Test // DATAJPA-1713, GH-2008 + void selectProjectionWithSubselect() { + + List dtos = userRepository.findProjectionBySubselect(); + + assertThat(dtos).flatExtracting(NameOnly::getFirstname) // + .containsExactly("Dave", "Carter", "Oliver August"); + assertThat(dtos).flatExtracting(NameOnly::getLastname) // + .containsExactly("Matthews", "Beauford", "Matthews"); + } + + @Test // GH-3076 + void dtoProjectionShouldApplyConstructorExpressionRewriting() { + + List dtos = userRepository.findRecordProjection(); + + assertThat(dtos).flatExtracting(UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + + dtos = userRepository.findRecordProjectionWithFunctions(); + + assertThat(dtos).flatExtracting(UserExcerpt::lastname) // + .contains("matthews", "beauford"); + } + + @Test // GH-3895 + void stringProjectionShouldNotApplyConstructorExpressionRewriting() { + + List names = userRepository.findStringProjection(); + + assertThat(names) // + .contains("Dave", "Carter", "Oliver August"); + } + + @Test // GH-3895 + void objectArrayProjectionShouldNotApplyConstructorExpressionRewriting() { + + List names = userRepository.findObjectArrayProjectionWithFunctions(); + + assertThat(names) // + .contains(new String[] { "Dave", "matthews" }); + } + + @Test // GH-3076 + void dtoMultiselectProjectionShouldApplyConstructorExpressionRewriting() { + + List dtos = userRepository.findMultiselectRecordProjection(); + + assertThat(dtos).flatExtracting(UserExcerpt::firstname) // + .contains("Dave", "Carter", "Oliver August"); + } + + @Test // GH-3895 + void dtoMultiselectProjectionShouldApplyConstructorExpressionRewritingForJoin() { + + dave.setAddress(new Address("US", "Albuquerque", "some street", "12345")); + + List dtos = userRepository.findAddressProjection(); + + assertThat(dtos).flatExtracting(UserRepository.AddressDto::city) // + .contains("Albuquerque"); + } + + @Test // GH-3076 + void dynamicDtoProjection() { + + List dtos = userRepository.findRecordProjection(UserExcerpt.class); + + assertThat(dtos).flatExtracting(UserExcerpt::firstname) // + .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() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat(userRepository.dtoProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { + assertThat(projection.user()).isIn(musicians.values()); + assertThat(projection.roleCount()).isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), + Offset.offset(0L)); + }); + } + + @Test // GH-3076 + void interfaceProjectionWithEntityAndAggregatedValue() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat(userRepository.interfaceProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { + assertThat(projection.getUser()).isIn(musicians.values()); + assertThat(projection.getRoleCount()) + .isCloseTo(musicians.get(projection.getUser().getFirstname()).getRoles().size(), Offset.offset(0L)); + }); + } + + @Test // GH-3076 + void rawMapProjectionWithEntityAndAggregatedValue() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat(userRepository.rawMapProjectionEntityAndAggregatedValue()).allSatisfy(projection -> { + assertThat(projection.get("user")).isIn(musicians.values()); + assertThat(projection).containsKey("roleCount"); + }); + } + + @Test // GH-3076 + void dtoProjectionWithEntityAndAggregatedValueWithPageable() { + + Map musicians = Map.of(carter.getFirstname(), carter, dave.getFirstname(), dave, + oliver.getFirstname(), oliver); + + assertThat( + userRepository.dtoProjectionEntityAndAggregatedValue(PageRequest.of(0, 10).withSort(Sort.by("firstname")))) + .allSatisfy(projection -> { + assertThat(projection.user()).isIn(musicians.values()); + assertThat(projection.roleCount()) + .isCloseTo(musicians.get(projection.user().getFirstname()).getRoles().size(), Offset.offset(0L)); + }); + } + + @ParameterizedTest // GH-3076 + @ValueSource(classes = { UserRoleCountDtoProjection.class, UserRoleCountInterfaceProjection.class }) + void dynamicProjectionWithEntityAndAggregated(Class resultType) { + + assertThat(userRepository.findMultiselectRecordDynamicProjection(resultType)).hasSize(3) + .hasOnlyElementsOfType(resultType); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java index e72ded4fc7..fcede5da49 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java @@ -38,7 +38,7 @@ abstract class AbstractDtoQueryTransformerUnitTests

        ... parameterTypes) { try { 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 43022e6bbb..47949f1a6d 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 @@ -60,7 +60,6 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.data.util.TypeInformation; /** * Unit test for {@link SimpleJpaQuery}. @@ -274,10 +273,7 @@ void doesNotRewriteQueryReturningEntity() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("selectWithJoin")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd"); } @@ -288,41 +284,34 @@ void rewriteQueryReturningDto() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("selectWithJoin")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); assertThat(queryString).startsWith( "SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)"); } @Test // GH-3895 - void doesNotRewriteQueryForUnknownProperty() throws Exception { + void rewritesQueryForUnknownProperty() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("projectWithUnknownPaths")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); - assertThat(queryString).startsWith("select u.unknown from User u"); + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(u.unknown)"); } @Test // GH-3895 - void doesNotRewriteQueryForJoinPath() throws Exception { + void rewritesQueryForJoinPath() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("projectWithJoinPaths")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); - assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r"); + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(r.name) from User u LEFT JOIN FETCH u.roles r"); } @Test // DATAJPA-1307 @@ -372,6 +361,13 @@ private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional { @Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true) 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..2d2c46bb8c 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 @@ -718,9 +718,18 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter @Query("select u from User u") List findRecordProjection(); - @Query("select u.firstname, LOWER(u.lastname) from User u") + @Query("select u.firstname as fn, LOWER(u.lastname) as lastname from User u") List findRecordProjectionWithFunctions(); + @Query("select u.firstname from User u") + List findStringProjection(); + + @Query("select u.firstname, LOWER(u.lastname) from User u") + List findObjectArrayProjectionWithFunctions(); + + @Query("select u.address from User u") + List findAddressProjection(); + @Query("select u from User u") List findRecordProjection(Class projectionType); @@ -807,6 +816,12 @@ record UserExcerpt(String firstname, String lastname) { } + record AddressDto(String country, String city) { + public AddressDto(Address address) { + this(address != null ? address.getCountry() : null, address != null ? address.getCity() : null); + } + } + record UserRoleCountDtoProjection(User user, Long roleCount) { } From 83be337b2c1ccc3511b12a2a6e91537b2ca824d6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 4 Jun 2025 11:21:55 +0200 Subject: [PATCH 113/224] Consider only top-level properties for tuple query selection. We now only consider top-level properties for tuple query selection to avoid join products caused by selecting nested relationships. Closes #3908 --- .../repository/support/SimpleJpaRepository.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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..bebe618fe2 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 @@ -34,9 +34,11 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; @@ -820,11 +822,18 @@ private TypedQuery getQuery(ReturnedType returnedType, @Nullabl } List> selections = new ArrayList<>(); - + Set topLevelProperties = new HashSet<>(); for (String property : requiredSelection) { - PropertyPath path = PropertyPath.from(property, returnedType.getDomainType()); - selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(property)); + int separator = property.indexOf('.'); + String topLevelProperty = separator == -1 ? property : property.substring(0, separator); + + if (!topLevelProperties.add(topLevelProperty)) { + continue; + } + + PropertyPath path = PropertyPath.from(topLevelProperty, returnedType.getDomainType()); + selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(topLevelProperty)); } Class typeToRead = returnedType.getReturnedType(); From 1dcf91062271f98aa4a821834fa08cfa655ae0b6 Mon Sep 17 00:00:00 2001 From: Aref Date: Tue, 27 May 2025 09:37:13 +0330 Subject: [PATCH 114/224] Suppress warnings in tests. Signed-off-by: Aref Closes: #3901 --- .../jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java | 2 +- .../data/jpa/domain/DeleteSpecificationUnitTests.java | 4 ++-- .../data/jpa/domain/PredicateSpecificationUnitTests.java | 4 ++-- .../data/jpa/domain/UpdateSpecificationUnitTests.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java index a1daf39d27..41d24a1ed6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java @@ -31,7 +31,7 @@ */ class Jsr310JpaConvertersUnitTests { - static Iterable data() { + static Iterable data() { return Arrays.asList(new Jsr310JpaConverters.InstantConverter(), // new Jsr310JpaConverters.LocalDateConverter(), // 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 8dfcb33bad..ea41f7301a 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 @@ -109,7 +109,7 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "deprecation"}) DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( serialize(specification)); @@ -125,7 +125,7 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "deprecation"}) DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( serialize(specification)); 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 d11d61d0a2..d588fe2126 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 @@ -107,7 +107,7 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "deprecation"}) PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( serialize(specification)); @@ -123,7 +123,7 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "deprecation"}) PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( serialize(specification)); 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 61c788d143..907e302584 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 @@ -109,7 +109,7 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "deprecation"}) UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( serialize(specification)); @@ -125,7 +125,7 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "deprecation"}) UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( serialize(specification)); From 52a5e317a76cd03378b6b433163662495f59fdc2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 4 Jun 2025 14:43:54 +0200 Subject: [PATCH 115/224] Polishing. Move warning suppression to the class-level. See #3901 --- .../threeten/Jsr310JpaConvertersUnitTests.java | 5 ++--- .../jpa/domain/DeleteSpecificationUnitTests.java | 4 +--- .../domain/PredicateSpecificationUnitTests.java | 4 +--- .../data/jpa/domain/SpecificationUnitTests.java | 14 +------------- .../jpa/domain/UpdateSpecificationUnitTests.java | 4 +--- 5 files changed, 6 insertions(+), 25 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java index 41d24a1ed6..691d1a83d9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java @@ -17,10 +17,10 @@ import static org.assertj.core.api.Assertions.*; -import java.util.Arrays; - import jakarta.persistence.AttributeConverter; +import java.util.Arrays; + import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -40,7 +40,6 @@ static Iterable data() { new Jsr310JpaConverters.ZoneIdConverter()); } - @ParameterizedTest @MethodSource("data") void convertersHandleNullValuesCorrectly(AttributeConverter converter) { 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 ea41f7301a..99e6bb80ac 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 @@ -39,7 +39,7 @@ * * @author Mark Paluch */ -@SuppressWarnings("serial") +@SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class DeleteSpecificationUnitTests implements Serializable { @@ -109,7 +109,6 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({"unchecked", "deprecation"}) DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( serialize(specification)); @@ -125,7 +124,6 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({"unchecked", "deprecation"}) DeleteSpecification transferredSpecification = (DeleteSpecification) deserialize( serialize(specification)); 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 d588fe2126..0bcefc79ae 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 @@ -38,7 +38,7 @@ * * @author Mark Paluch */ -@SuppressWarnings("serial") +@SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class PredicateSpecificationUnitTests implements Serializable { @@ -107,7 +107,6 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({"unchecked", "deprecation"}) PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( serialize(specification)); @@ -123,7 +122,6 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({"unchecked", "deprecation"}) PredicateSpecification transferredSpecification = (PredicateSpecification) deserialize( serialize(specification)); 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 8380816d52..f819ed9a56 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 @@ -26,7 +26,6 @@ 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; @@ -44,24 +43,16 @@ * @author Mark Paluch * @author Daniel Shuy */ -@SuppressWarnings("removal") +@SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class SpecificationUnitTests { - private Specification spec; @Mock(serializable = true) Root root; @Mock(serializable = true) CriteriaQuery query; @Mock(serializable = true) CriteriaBuilder builder; - @Mock(serializable = true) Predicate predicate; - @BeforeEach - void setUp() { - - spec = (root, query, cb) -> predicate; - } - @Test // GH-1943 void emptyAllOfReturnsEmptySpecification() { @@ -88,7 +79,6 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation"}) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -103,7 +93,6 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation"}) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -116,7 +105,6 @@ void andCombinesSpecificationsInOrder() { Predicate secondPredicate = mock(Predicate.class); Specification first = ((root1, query1, criteriaBuilder) -> firstPredicate); - Specification second = ((root1, query1, criteriaBuilder) -> secondPredicate); first.and(second).toPredicate(root, query, builder); 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 907e302584..f65ab6ecaa 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 @@ -39,7 +39,7 @@ * * @author Mark Paluch */ -@SuppressWarnings("serial") +@SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class UpdateSpecificationUnitTests implements Serializable { @@ -109,7 +109,6 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({"unchecked", "deprecation"}) UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( serialize(specification)); @@ -125,7 +124,6 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({"unchecked", "deprecation"}) UpdateSpecification transferredSpecification = (UpdateSpecification) deserialize( serialize(specification)); From ab40236949f9c2dcf86b0456fc1236abe3af552a Mon Sep 17 00:00:00 2001 From: Ariel Morelli Andres Date: Thu, 22 May 2025 21:22:05 -0700 Subject: [PATCH 116/224] Prevent access to `EntityManager` when looking up `PersistenceProvider`. Signed-off-by: Ariel Morelli Andres Closes: #3425 Original pull request: #3885 --- .../jpa/provider/PersistenceProvider.java | 5 +- .../repository/query/AbstractJpaQuery.java | 1 - .../data/jpa/repository/support/Querydsl.java | 4 +- .../CrudMethodMetadataUnitTests.java | 6 +- ...ernateCurrentTenantIdentifierResolver.java | 49 +++++++++++ .../HibernateMultitenancyTests.java | 88 +++++++++++++++++++ .../AbstractStringBasedJpaQueryUnitTests.java | 4 + .../repository/query/NamedQueryUnitTests.java | 1 - .../query/NativeJpaQueryUnitTests.java | 8 +- ...positoryFragmentsContributorUnitTests.java | 4 + .../support/SimpleJpaRepositoryUnitTests.java | 6 +- .../src/test/resources/multitenancy-test.xml | 40 +++++++++ 12 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java create mode 100644 spring-data-jpa/src/test/resources/multitenancy-test.xml 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 9755f19f09..caabe955cc 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 @@ -56,6 +56,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yuriy Tsarkov + * @author Ariel Morelli Andres (Atlassian US, Inc.) */ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment { @@ -316,7 +317,7 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { } /** - * Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be + * Determines the {@link PersistenceProvider} from the given {@link EntityManagerFactory}. If no special one can be * determined {@link #GENERIC_JPA} will be returned. * * @param emf must not be {@literal null}. @@ -324,7 +325,7 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { */ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) { - Assert.notNull(emf, "EntityManager must not be null"); + Assert.notNull(emf, "EntityManagerFactory must not be null"); Class entityManagerType = emf.getPersistenceUnitUtil().getClass(); PersistenceProvider cachedProvider = CACHE.get(entityManagerType); 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 f70210ff84..ad0cafba95 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 @@ -24,7 +24,6 @@ import jakarta.persistence.TypedQuery; import java.lang.reflect.Constructor; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.function.UnaryOperator; 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 da00e05368..1e493eb905 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 @@ -40,7 +40,6 @@ 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,7 +86,8 @@ 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}, {@link JPQLTemplates#DEFAULT} by default. + * @return the {@link JPQLTemplates} for the configured {@link EntityManager}, {@link JPQLTemplates#DEFAULT} by + * default. * @since 3.5 */ public JPQLTemplates getTemplates() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java index 52e217bb71..69e7379d19 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java @@ -17,9 +17,6 @@ import static org.mockito.Mockito.*; -import java.util.Collections; -import java.util.Map; - import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.LockModeType; @@ -28,6 +25,9 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.metamodel.Metamodel; +import java.util.Collections; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java new file mode 100644 index 0000000000..436e99fb31 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java @@ -0,0 +1,49 @@ +/* + * 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 java.util.Optional; + +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.jspecify.annotations.Nullable; + +/** + * {@code CurrentTenantIdentifierResolver} instance for testing + * + * @author Ariel Morelli Andres (Atlassian US, Inc.) + */ +public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { + private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>(); + + public static void setTenantIdentifier(String tenantIdentifier) { + CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier); + } + + public static void removeTenantIdentifier() { + CURRENT_TENANT_IDENTIFIER.remove(); + } + + @Override + public String resolveCurrentTenantIdentifier() { + return Optional.ofNullable(CURRENT_TENANT_IDENTIFIER.get()) + .orElseThrow(() -> new IllegalArgumentException("Could not resolve current tenant identifier")); + } + + @Override + public boolean validateExistingCurrentSessions() { + return true; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java new file mode 100644 index 0000000000..3de19e90d8 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java @@ -0,0 +1,88 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +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.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.sample.RoleRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; + +/** + * Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected + * despite not having a tenant available at creation time + * + * @author Ariel Morelli Andres (Atlassian US, Inc.) + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration() +class HibernateMultitenancyTests { + + @Autowired RoleRepository roleRepository; + @Autowired EntityManager em; + + @AfterEach + void tearDown() { + HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier(); + } + + @Test + void testPersistenceProviderFromFactoryWithoutTenant() { + PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory()); + assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE); + } + + @Test + void testRepositoryWithTenant() { + HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id"); + assertThatNoException().isThrownBy(() -> roleRepository.findAll()); + } + + @Test + void testRepositoryWithoutTenantFails() { + assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class); + } + + @Transactional + List insertAndQuery() { + roleRepository.save(new Role("DRUMMER")); + roleRepository.flush(); + return roleRepository.findAll(); + } + + @ImportResource({ "classpath:multitenancy-test.xml" }) + @Configuration + @EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE)) + static class TestConfig {} +} 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 953203134f..b254c41cef 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 @@ -18,6 +18,7 @@ import static org.mockito.Mockito.*; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.metamodel.Metamodel; import java.lang.reflect.Method; @@ -52,6 +53,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Ariel Morelli Andres */ class AbstractStringBasedJpaQueryUnitTests { @@ -137,10 +139,12 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu public EntityManager get() { EntityManager em = Mockito.mock(EntityManager.class); + EntityManagerFactory emf = Mockito.mock(EntityManagerFactory.class); Metamodel meta = mock(Metamodel.class); when(em.getMetamodel()).thenReturn(meta); when(em.getDelegate()).thenReturn(new Object()); // some generic jpa + when(em.getEntityManagerFactory()).thenReturn(emf); return em; } 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 71bd266f05..b5a70c9099 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 @@ -36,7 +36,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.provider.QueryExtractor; -import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; 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 c17cc49f94..7768239163 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 @@ -71,11 +71,9 @@ void shouldApplySorting() { queryExtractor); NativeJpaQuery query = new NativeJpaQuery(queryMethod, em, queryMethod.getRequiredDeclaredQuery(), - queryMethod.getDeclaredCountQuery(), - new JpaQueryConfiguration(QueryRewriterProvider.simple(), QueryEnhancerSelector.DEFAULT_SELECTOR, - ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT)); - QueryProvider sql = query.getSortedQuery(Sort.by("foo", "bar"), - queryMethod.getResultProcessor().getReturnedType()); + queryMethod.getDeclaredCountQuery(), new JpaQueryConfiguration(QueryRewriterProvider.simple(), + QueryEnhancerSelector.DEFAULT_SELECTOR, ValueExpressionDelegate.create(), EscapeCharacter.DEFAULT)); + QueryProvider sql = query.getSortedQuery(Sort.by("foo", "bar"), queryMethod.getResultProcessor().getReturnedType()); assertThat(sql.getQueryString()).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/support/JpaRepositoryFragmentsContributorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFragmentsContributorUnitTests.java index 7825534a32..f3634a37eb 100644 --- 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 @@ -19,6 +19,7 @@ import static org.mockito.Mockito.*; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import java.util.Iterator; @@ -40,6 +41,7 @@ * Unit tests for {@link JpaRepositoryFragmentsContributor}. * * @author Mark Paluch + * @author Ariel Morelli Andres */ class JpaRepositoryFragmentsContributorUnitTests { @@ -53,7 +55,9 @@ void composedContributorShouldCreateFragments() { when(entityPathResolver.createPath(any())).thenReturn((EntityPath) QCustomer.customer); EntityManager entityManager = mock(EntityManager.class); + EntityManagerFactory emf = mock(EntityManagerFactory.class); when(entityManager.getDelegate()).thenReturn(entityManager); + when(entityManager.getEntityManagerFactory()).thenReturn(emf); RepositoryComposition.RepositoryFragments fragments = contributor.contribute( AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), 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 3d17c347c1..b720228bdc 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 @@ -31,7 +31,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; -import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -44,7 +43,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; 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; @@ -61,6 +59,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yanming Zhou + * @author Ariel Morelli Andres (Atlassian US, Inc.) */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -85,6 +84,9 @@ class SimpleJpaRepositoryUnitTests { void setUp() { when(em.getDelegate()).thenReturn(em); + when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory); + + when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); when(information.getJavaType()).thenReturn(User.class); when(em.getCriteriaBuilder()).thenReturn(builder); diff --git a/spring-data-jpa/src/test/resources/multitenancy-test.xml b/spring-data-jpa/src/test/resources/multitenancy-test.xml new file mode 100644 index 0000000000..d1ff786d12 --- /dev/null +++ b/spring-data-jpa/src/test/resources/multitenancy-test.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + org.springframework.data.jpa.repository.HibernateCurrentTenantIdentifierResolver + + + + + + + + + + + + + + + + + + + + + + From 98e801cfd5f56a933cc96c26144b7f3d0244627b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 5 Jun 2025 11:01:03 +0200 Subject: [PATCH 117/224] Polishing. Revise PersistenceProvider detection to a EntityManagerFactory-based variant, considering EntityManagerFactory proxying. See: #3425 Original pull request: #3885 --- .../jpa/provider/PersistenceProvider.java | 111 +++++++----------- .../PersistenceProviderUnitTests.java | 61 ++++++---- ...ernateCurrentTenantIdentifierResolver.java | 12 +- .../HibernateMultitenancyTests.java | 28 +++-- .../support/SimpleJpaRepositoryUnitTests.java | 6 +- 5 files changed, 108 insertions(+), 110 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 caabe955cc..5de0142a64 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 @@ -25,12 +25,14 @@ import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.SingularAttribute; +import java.lang.reflect.Proxy; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.LongSupplier; +import java.util.stream.Stream; import org.eclipse.persistence.config.QueryHints; import org.eclipse.persistence.jpa.JpaQuery; @@ -41,6 +43,8 @@ import org.hibernate.query.SelectionQuery; import org.jspecify.annotations.Nullable; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -56,22 +60,15 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yuriy Tsarkov - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment { /** * Hibernate persistence provider. - *

        - * Since Hibernate 4.3 the location of the HibernateEntityManager moved to the org.hibernate.jpa package. In order to - * support both locations we interpret both classnames as a Hibernate {@code PersistenceProvider}. - * - * @see DATAJPA-444 */ - HIBERNATE(// - Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // - Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), // - Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) { + HIBERNATE(List.of(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // + List.of(HIBERNATE_JPA_METAMODEL_TYPE)) { @Override public @Nullable String extractQueryString(Object query) { @@ -136,9 +133,7 @@ public long getResultCount(Query resultQuery, LongSupplier countSupplier) { /** * EclipseLink persistence provider. */ - 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)) { + ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE), List.of(ECLIPSELINK_JPA_METAMODEL_TYPE)) { @Override public String extractQueryString(Object query) { @@ -180,8 +175,7 @@ public String getCommentHintValue(String comment) { /** * Unknown special provider. Use standard JPA. */ - GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), - Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { + GENERIC_JPA(List.of(GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE), Collections.emptySet()) { @Override public @Nullable String extractQueryString(Object query) { @@ -231,8 +225,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; + final Iterable entityManagerFactoryClassNames; private final Iterable metamodelClassNames; private final boolean present; @@ -242,37 +235,15 @@ public boolean shouldUseAccessorFor(Object entity) { * * @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}. + * @param metamodelClassNames the names of the provider specific {@link Metamodel} implementations. Must not be + * {@literal null} or empty. */ - PersistenceProvider(Iterable entityManagerFactoryClassNames, Iterable entityManagerClassNames, - Iterable metamodelClassNames) { + PersistenceProvider(Collection entityManagerFactoryClassNames, Collection metamodelClassNames) { this.entityManagerFactoryClassNames = entityManagerFactoryClassNames; - this.entityManagerClassNames = entityManagerClassNames; this.metamodelClassNames = metamodelClassNames; - - boolean present = false; - for (String emfClassName : entityManagerFactoryClassNames) { - - 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; + this.present = Stream.concat(entityManagerFactoryClassNames.stream(), metamodelClassNames.stream()) + .anyMatch(it -> ClassUtils.isPresent(it, PersistenceProvider.class.getClassLoader())); } /** @@ -288,32 +259,23 @@ private static PersistenceProvider cacheAndReturn(Class type, PersistenceProv } /** - * Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be + * Determines the {@link PersistenceProvider} from the given {@link EntityManager} by introspecting + * {@link EntityManagerFactory} via {@link EntityManager#getEntityManagerFactory()}. If no special one can be * determined {@link #GENERIC_JPA} will be returned. + *

        + * This method avoids {@link EntityManager} initialization when using + * {@link org.springframework.orm.jpa.SharedEntityManagerCreator} by accessing + * {@link EntityManager#getEntityManagerFactory()}. * * @param em must not be {@literal null}. * @return will never be {@literal null}. + * @see org.springframework.orm.jpa.SharedEntityManagerCreator */ public static PersistenceProvider fromEntityManager(EntityManager em) { Assert.notNull(em, "EntityManager must not be null"); - Class entityManagerType = em.getDelegate().getClass(); - PersistenceProvider cachedProvider = CACHE.get(entityManagerType); - - if (cachedProvider != null) { - return cachedProvider; - } - - for (PersistenceProvider provider : ALL) { - for (String entityManagerClassName : provider.entityManagerClassNames) { - if (isEntityManagerOfType(em, entityManagerClassName)) { - return cacheAndReturn(entityManagerType, provider); - } - } - } - - return cacheAndReturn(entityManagerType, GENERIC_JPA); + return fromEntityManagerFactory(em.getEntityManagerFactory()); } /** @@ -322,12 +284,24 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { * * @param emf must not be {@literal null}. * @return will never be {@literal null}. + * @since 3.5.1 */ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) { Assert.notNull(emf, "EntityManagerFactory must not be null"); - Class entityManagerType = emf.getPersistenceUnitUtil().getClass(); + EntityManagerFactory unwrapped = emf; + + while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) { + + if (Proxy.isProxyClass(unwrapped.getClass())) { + unwrapped = unwrapped.unwrap(null); + } else if (AopUtils.isAopProxy(unwrapped)) { + unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped); + } + } + + Class entityManagerType = unwrapped.getClass(); PersistenceProvider cachedProvider = CACHE.get(entityManagerType); if (cachedProvider != null) { @@ -336,8 +310,7 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory for (PersistenceProvider provider : ALL) { for (String emfClassName : provider.entityManagerFactoryClassNames) { - if (isOfType(emf.getPersistenceUnitUtil(), emfClassName, - emf.getPersistenceUnitUtil().getClass().getClassLoader())) { + if (isOfType(unwrapped, emfClassName, unwrapped.getClass().getClassLoader())) { return cacheAndReturn(entityManagerType, provider); } } @@ -445,16 +418,14 @@ 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_FACTORY_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManagerFactory"; String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager"; + String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; // 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_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.SessionFactory"; + String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.Session"; String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel"; - String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java index ba7c3abed7..66d55e2397 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java @@ -16,18 +16,20 @@ package org.springframework.data.jpa.provider; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import static org.springframework.data.jpa.provider.PersistenceProvider.*; import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import java.util.Arrays; import java.util.Map; -import org.assertj.core.api.Assumptions; -import org.hibernate.Version; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import org.springframework.asm.ClassWriter; @@ -42,6 +44,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Jens Schauder + * @author Mark Paluch */ class PersistenceProviderUnitTests { @@ -56,12 +59,32 @@ void setup() { this.shadowingClassLoader = new ShadowingClassLoader(getClass().getClassLoader()); } + @ParameterizedTest // GH-3425 + @EnumSource(PersistenceProvider.class) + void entityManagerFactoryClassNamesAreInterfaces(PersistenceProvider provider) throws ClassNotFoundException { + + for (String className : provider.entityManagerFactoryClassNames) { + assertThat(ClassUtils.forName(className, PersistenceProvider.class.getClassLoader()).isInterface()).isTrue(); + } + } + + @ParameterizedTest // GH-3425 + @EnumSource(PersistenceProvider.class) + void metaModelNamesExist(PersistenceProvider provider) throws ClassNotFoundException { + + for (String className : provider.entityManagerFactoryClassNames) { + assertThat(ClassUtils.forName(className, PersistenceProvider.class.getClassLoader()).isInterface()).isNotNull(); + } + } + @Test void detectsEclipseLinkPersistenceProvider() throws Exception { shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa"); EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE); + when(em.getEntityManagerFactory()) + .thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE)); assertThat(fromEntityManager(em)).isEqualTo(ECLIPSELINK); } @@ -70,31 +93,19 @@ void detectsEclipseLinkPersistenceProvider() throws Exception { void fallbackToGenericJpaForUnknownPersistenceProvider() throws Exception { EntityManager em = mockProviderSpecificEntityManagerInterface("foo.bar.unknown.jpa.JpaEntityManager"); + when(em.getEntityManagerFactory()).thenReturn(mock(EntityManagerFactory.class)); assertThat(fromEntityManager(em)).isEqualTo(GENERIC_JPA); } - @Test // DATAJPA-1019 - void detectsHibernatePersistenceProviderForHibernateVersion52() throws Exception { - - Assumptions.assumeThat(Version.getVersionString()).startsWith("5.2"); - - shadowingClassLoader.excludePackage("org.hibernate"); - - EntityManager em = mockProviderSpecificEntityManagerInterface(HIBERNATE_ENTITY_MANAGER_INTERFACE); - - assertThat(fromEntityManager(em)).isEqualTo(HIBERNATE); - } - @Test // DATAJPA-1379 void detectsProviderFromProxiedEntityManager() throws Exception { shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa"); - EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE); - EntityManager emProxy = Mockito.mock(EntityManager.class); - Mockito.when(emProxy.getDelegate()).thenReturn(em); + when(emProxy.getEntityManagerFactory()) + .thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE)); assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK); } @@ -105,13 +116,23 @@ private EntityManager mockProviderSpecificEntityManagerInterface(String interfac EntityManager.class); EntityManager em = (EntityManager) Mockito.mock(providerSpecificEntityManagerInterface); - Mockito.when(em.getDelegate()).thenReturn(em); // delegate is used to determine the classloader of the provider - // specific interface, therefore we return the proxied - // EntityManager. + + // delegate is used to determine the classloader of the provider + // specific interface, therefore we return the proxied EntityManager + when(em.getDelegate()).thenReturn(em); return em; } + private EntityManagerFactory mockProviderSpecificEntityManagerFactoryInterface(String interfaceName) + throws ClassNotFoundException { + + Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, + EntityManager.class); + + return (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerInterface); + } + static class InterfaceGenerator implements Opcodes { static Class generate(final String interfaceName, ClassLoader parentClassLoader, final Class... interfaces) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java index 436e99fb31..c6670bcc95 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-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. @@ -21,18 +21,19 @@ import org.jspecify.annotations.Nullable; /** - * {@code CurrentTenantIdentifierResolver} instance for testing + * {@code CurrentTenantIdentifierResolver} instance for testing. * - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { + private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>(); - public static void setTenantIdentifier(String tenantIdentifier) { + static void setTenantIdentifier(String tenantIdentifier) { CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier); } - public static void removeTenantIdentifier() { + static void removeTenantIdentifier() { CURRENT_TENANT_IDENTIFIER.remove(); } @@ -46,4 +47,5 @@ public String resolveCurrentTenantIdentifier() { public boolean validateExistingCurrentSessions() { return true; } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java index 3de19e90d8..28ebcd1765 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-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. @@ -18,11 +18,14 @@ import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assumptions.*; +import jakarta.persistence.EntityManager; + import java.util.List; import org.junit.jupiter.api.AfterEach; 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.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -36,16 +39,14 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; -import jakarta.persistence.EntityManager; - /** * Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected - * despite not having a tenant available at creation time + * despite not having a tenant available at creation time. * - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ @ExtendWith(SpringExtension.class) -@ContextConfiguration() +@ContextConfiguration class HibernateMultitenancyTests { @Autowired RoleRepository roleRepository; @@ -56,19 +57,23 @@ void tearDown() { HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier(); } - @Test + @Test // GH-3425 void testPersistenceProviderFromFactoryWithoutTenant() { - PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory()); + + PersistenceProvider provider = PersistenceProvider.fromEntityManager(em); + assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE); } - @Test + @Test // GH-3425 void testRepositoryWithTenant() { + HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id"); + assertThatNoException().isThrownBy(() -> roleRepository.findAll()); } - @Test + @Test // GH-3425 void testRepositoryWithoutTenantFails() { assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class); } @@ -80,9 +85,10 @@ List insertAndQuery() { return roleRepository.findAll(); } - @ImportResource({ "classpath:multitenancy-test.xml" }) + @ImportResource("classpath:multitenancy-test.xml") @Configuration @EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE)) static class TestConfig {} + } 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 b720228bdc..0d1e8133f5 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 @@ -18,7 +18,6 @@ import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.data.jpa.domain.Specification.*; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; @@ -43,6 +42,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; 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; @@ -59,7 +59,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yanming Zhou - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -189,7 +189,6 @@ void doNothingWhenNewInstanceGetsDeleted() { newUser.setId(null); when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory); - when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); repo.delete(newUser); @@ -206,7 +205,6 @@ void doNothingWhenNonExistentInstanceGetsDeleted() { when(information.isNew(newUser)).thenReturn(false); when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory); - when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); when(persistenceUnitUtil.getIdentifier(any())).thenReturn(23); when(em.find(User.class, 23)).thenReturn(null); From 3d28143f22bfc3b29c29a42ea6eb70120581cdbd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 10 Jun 2025 10:27:13 +0200 Subject: [PATCH 118/224] Fix `QueryUtils` regex parsing field and function aliases. Remove leading space requirement, simplify group nesting and replace character class with non-capturing group to avoid a, s and | (pipe) matching. Closes #3911 --- .../data/jpa/repository/query/QueryUtils.java | 8 +-- .../repository/query/QueryUtilsUnitTests.java | 61 +++++++++++++------ 2 files changed, 47 insertions(+), 22 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 749853f36f..e01b5d224b 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 @@ -195,17 +195,15 @@ public abstract class QueryUtils { // any function call including parameters within the brackets builder.append("\\w+\\s*\\([\\w\\.,\\s'=:;\\\\?]+\\)"); // the potential alias - builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); + builder.append("\\s+(?:as|AS)+\\s+([\\w\\.]+)"); FUNCTION_PATTERN = compile(builder.toString()); builder = new StringBuilder(); - builder.append("\\s+"); // at least one space builder.append("[^\\s\\(\\)]+"); // No white char no bracket - builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); // the potential alias + builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); // the potential alias FIELD_ALIAS_PATTERN = compile(builder.toString()); - } /** @@ -391,7 +389,7 @@ static Set getOuterJoinAliases(String query) { * @param query a {@literal String} containing a query. Must not be {@literal null}. * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}. */ - private static Set getFieldAliases(String query) { + static Set getFieldAliases(String query) { Set result = new HashSet<>(); Matcher matcher = FIELD_ALIAS_PATTERN.matcher(query); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java index 647d4dfa2b..717791afec 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java @@ -51,6 +51,7 @@ * @author Erik Pellizzon * @author Pranav HS * @author Eduard Dudar + * @author Mark Paluch */ class QueryUtilsUnitTests { @@ -130,13 +131,13 @@ void detectsAliasCorrectly() { .isEqualTo("u"); assertThat(detectAlias( "from Foo f left join f.bar b with type(b) = BarChild where (f.id = (select max(f.id) from Foo f2 where type(f2) = FooChild) or 1 <> 1) and 1=1")) - .isEqualTo("f"); + .isEqualTo("f"); assertThat(detectAlias( "(from Foo f max(f) ((((select * from Foo f2 (from Foo f3) max(*)) (from Foo f4)) max(f5)) (f6)) (from Foo f7))")) - .isEqualTo("f"); + .isEqualTo("f"); assertThat(detectAlias( "SELECT e FROM DbEvent e WHERE (CAST(:modifiedFrom AS date) IS NULL OR e.modificationDate >= :modifiedFrom)")) - .isEqualTo("e"); + .isEqualTo("e"); assertThat(detectAlias("from User u where (cast(:effective as date) is null) OR :effective >= u.createdAt")) .isEqualTo("u"); assertThat(detectAlias("from User u where (cast(:effectiveDate as date) is null) OR :effectiveDate >= u.createdAt")) @@ -145,7 +146,7 @@ void detectsAliasCorrectly() { .isEqualTo("u"); assertThat( detectAlias("from User u where (cast(:e1f2f3ectiveFrom as date) is null) OR :effectiveFrom >= u.createdAt")) - .isEqualTo("u"); + .isEqualTo("u"); } @Test // GH-2260 @@ -175,13 +176,13 @@ void testRemoveSubqueries() throws Exception { .isEqualTo("(select u from User u where not exists )"); assertThat(normalizeWhitespace( removeSubqueries("select u from User u where not exists (from User u2 where not exists (from User u3))"))) - .isEqualTo("select u from User u where not exists"); + .isEqualTo("select u from User u where not exists"); assertThat(normalizeWhitespace( removeSubqueries("select u from User u where not exists ((from User u2 where not exists (from User u3)))"))) - .isEqualTo("select u from User u where not exists ( )"); + .isEqualTo("select u from User u where not exists ( )"); assertThat(normalizeWhitespace( removeSubqueries("(select u from User u where not exists ((from User u2 where not exists (from User u3))))"))) - .isEqualTo("(select u from User u where not exists ( ))"); + .isEqualTo("(select u from User u where not exists ( ))"); } @Test // GH-2581 @@ -543,6 +544,32 @@ void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpa assertThat(applySorting(query, sort, "m")).endsWith("order by avgPrice asc"); } + @Test // GH-3911 + void discoversFunctionAliasesCorrectly() { + + assertThat(getFunctionAliases("SELECT COUNT(1) a alias1,2 s alias2")).isEmpty(); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1,2 as alias2")).containsExactly("alias1"); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1,COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1, 2 as alias2")).containsExactly("alias1"); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1, COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("COUNT(1) as alias1,COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("COUNT(1) as alias1,COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("1 as alias1, COUNT(2) as alias2")).containsExactly("alias2"); + assertThat(getFunctionAliases("1 as alias1, COUNT(2) as alias2")).containsExactly("alias2"); + assertThat(getFunctionAliases("COUNT(1) as alias1,2 as alias2")).containsExactly("alias1"); + assertThat(getFunctionAliases("COUNT(1) as alias1, 2 as alias2")).containsExactly("alias1"); + } + + @Test // GH-3911 + void discoversFieldAliasesCorrectly() { + + assertThat(getFieldAliases("SELECT 1 a alias1,2 s alias2")).isEmpty(); + assertThat(getFieldAliases("SELECT 1 as alias1,2 as alias2")).contains("alias1", "alias2"); + assertThat(getFieldAliases("SELECT 1 as alias1,2 as alias2")).contains("alias1", "alias2"); + assertThat(getFieldAliases("1 as alias1,2 as alias2")).contains("alias1", "alias2"); + assertThat(getFieldAliases("1 as alias1, 2 as alias2")).contains("alias1", "alias2"); + } + @Test // DATAJPA-1000 void discoversCorrectAliasForJoinFetch() { @@ -564,7 +591,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 @@ -784,18 +811,19 @@ void applySortingAccountsForNativeWindowFunction() { // order by in over clause + at the end assertThat( QueryUtils.applySorting("select dense_rank() over (order by lastname) from user u order by u.lastname", sort)) - .isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc"); + .isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc"); // partition by + order by in over clause - assertThat(QueryUtils.applySorting( - "select dense_rank() over (partition by active, age order by lastname) from user u", sort)).isEqualTo( + assertThat(QueryUtils + .applySorting("select dense_rank() over (partition by active, age order by lastname) from user u", sort)) + .isEqualTo( "select dense_rank() over (partition by active, age order by lastname) from user u order by u.age desc"); // partition by + order by in over clause + order by at the end assertThat(QueryUtils.applySorting( "select dense_rank() over (partition by active, age order by lastname) from user u order by active", sort)) - .isEqualTo( - "select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc"); + .isEqualTo( + "select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc"); // partition by + order by in over clause + frame clause assertThat(QueryUtils.applySorting( @@ -812,8 +840,7 @@ void applySortingAccountsForNativeWindowFunction() { // order by in subselect (select expression) assertThat( QueryUtils.applySorting("select lastname, (select i.id from item i order by i.id limit 1) from user u", sort)) - .isEqualTo( - "select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc"); + .isEqualTo("select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc"); // order by in subselect (select expression) + at the end assertThat(QueryUtils.applySorting( @@ -949,7 +976,7 @@ select q.specialist_id, listagg(q.points, '%s') as points @Test // GH-3324 void createCountQueryForSimpleQuery() { - assertCountQuery("select * from User","select count(*) from User"); - assertCountQuery("select * from User u","select count(u) from User u"); + assertCountQuery("select * from User", "select count(*) from User"); + assertCountQuery("select * from User u", "select count(u) from User u"); } } From 12050a75d481a07ab12385c49696c95c54f7dbfb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 11 Jun 2025 14:17:24 +0200 Subject: [PATCH 119/224] Upgrade to PGJDBC Driver 42.7.7. Closes #3914 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b82816e812..fb2c4c3dea 100755 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 3.2.0 5.2 9.2.0 - 42.7.5 + 42.7.7 23.8.0.25.04 4.0.0-SNAPSHOT 0.10.3 From 68af434d8242766197ab24de95f822cda19b6ab0 Mon Sep 17 00:00:00 2001 From: hoyeon Jang Date: Sat, 7 Jun 2025 10:08:35 +0900 Subject: [PATCH 120/224] Fix typos in query-methods.adoc. Signed-off-by: hoyeon Jang Closes #3912 --- src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 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 1d8ff7f9a2..9f8f7562b1 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -74,7 +74,7 @@ NOTE: `In` and `NotIn` also take any subclass of `Collection` as a parameter as ==== `DISTINCT` can be tricky and not always producing the results you expect. For example, `select distinct u from User u` will produce a complete different result than `select distinct u.lastname from User u`. -In the first case, since you are including `User.id`, nothing will duplicated, hence you'll get the whole table, and it would be of `User` objects. +In the first case, since you are including `User.id`, nothing will be duplicated, hence you'll get the whole table, and it would be of `User` objects. However, that latter query would narrow the focus to just `User.lastname` and find all unique last names for that table. This would also yield a `List` result set instead of a `List` result set. @@ -83,7 +83,7 @@ This would also yield a `List` result set instead of a `List` resu `countDistinctByLastname(String lastname)` can also produce unexpected results. Spring Data JPA will derive `select count(distinct u.id) from User u where u.lastname = ?1`. Again, since `u.id` won't hit any duplicates, this query will count up all the users that had the binding last name. -Which would the same as `countByLastname(String lastname)`! +Which would be the same as `countByLastname(String lastname)`! What is the point of this query anyway? To find the number of people with a given last name? To find the number of _distinct_ people with that binding last name? To find the number of _distinct last names_? (That last one is an entirely different query!) @@ -425,7 +425,7 @@ You have multiple options to consume large query results: You have learned in the previous chapter about `Pageable` and `PageRequest`. 2. <>. This is a lighter variant than paging because it does not require the total result count. -3. <>. +3. <>. This method avoids https://use-the-index-luke.com/no-offset[the shortcomings of offset-based result retrieval by leveraging database indexes]. Read more on xref:repositories/query-methods-details.adoc#repositories.scrolling.guidance[which method to use best] for your particular arrangement. From 89d059f7b5e75425190b8bc83e3ef8cf60033d3e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 12 Jun 2025 08:31:35 +0200 Subject: [PATCH 121/224] Polishing. Simplify regex. See #3911 --- .../data/jpa/repository/query/QueryUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 e01b5d224b..d250c89d7a 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 @@ -195,15 +195,15 @@ public abstract class QueryUtils { // any function call including parameters within the brackets builder.append("\\w+\\s*\\([\\w\\.,\\s'=:;\\\\?]+\\)"); // the potential alias - builder.append("\\s+(?:as|AS)+\\s+([\\w\\.]+)"); + builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); - FUNCTION_PATTERN = compile(builder.toString()); + FUNCTION_PATTERN = compile(builder.toString(), CASE_INSENSITIVE); builder = new StringBuilder(); builder.append("[^\\s\\(\\)]+"); // No white char no bracket builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); // the potential alias - FIELD_ALIAS_PATTERN = compile(builder.toString()); + FIELD_ALIAS_PATTERN = compile(builder.toString(), CASE_INSENSITIVE); } /** From 46453bc9d41c2625e3b578db7cf1dd65e05f53e0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 17 Jun 2025 09:37:50 +0200 Subject: [PATCH 122/224] Polishing. Refine readme. See #3892 --- README.adoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index 3c5597973a..3bd2b4da00 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Data JPA image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-jpa%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-jpa/] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data JPA Parent"] += Spring Data JPA image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data JPA Parent"] Spring Data JPA, part of the larger https://projects.spring.io/spring-data[Spring Data] family, makes it easy to implement JPA-based repositories. This module deals with enhanced support for JPA-based data access layers. @@ -157,7 +157,9 @@ You also need JDK 17 or above. If you want to build with the regular `mvn` command, you will need https://maven.apache.org/run-maven/index.html[Maven v3.8.0 or above]. -_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular please sign the https://cla.pivotal.io/sign/spring[Contributor’s Agreement] before your first non-trivial change._ +_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests._ +All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. === Building reference documentation @@ -168,7 +170,7 @@ Building the documentation builds also the project without running tests. $ ./mvnw clean install -Pantora ---- -The generated documentation is available from `target/antora/site/index.html`. +The generated documentation is available from `target/antora/index.html`. == Guides From 1975f9669ab6b128ce6d6b55ed047df28c49b511 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 25 Jun 2025 14:29:06 +0200 Subject: [PATCH 123/224] Fix `PersistenceProvider` lookup using proxied `EntityManagerFactory`. We now distinguish between Spring-proxied and other JDK proxied EntityManagerFactory objects for proper unwrapping. Spring consistently uses a null value as class to get hold of the target object. Both, Hibernate and EclipseLink fail with a NullPointerException when calling unwrap(null) and therefore, we call all other JDK proxies with unwrap(EntityManagerFactory.class) to adhere to the JPA specification and avoid failures according to the implementations. Any other proxying mechanism that behaves differently will require additional refinement once such a case comes up. Closes #3923 --- .../jpa/provider/PersistenceProvider.java | 5 ++- .../PersistenceProviderUnitTests.java | 44 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 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 5de0142a64..f46567bb63 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 @@ -295,7 +295,10 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) { if (Proxy.isProxyClass(unwrapped.getClass())) { - unwrapped = unwrapped.unwrap(null); + + Class unwrapTo = Proxy.getInvocationHandler(unwrapped).getClass().getName() + .contains("org.springframework.orm.jpa.") ? null : EntityManagerFactory.class; + unwrapped = unwrapped.unwrap(unwrapTo); } else if (AopUtils.isAopProxy(unwrapped)) { unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java index 66d55e2397..bb3c0aa161 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java @@ -22,7 +22,9 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceException; +import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Map; @@ -35,6 +37,7 @@ import org.springframework.asm.ClassWriter; import org.springframework.asm.Opcodes; import org.springframework.instrument.classloading.ShadowingClassLoader; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; @@ -110,6 +113,25 @@ void detectsProviderFromProxiedEntityManager() throws Exception { assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK); } + @Test // GH-3923 + void detectsEntityManagerFromProxiedEntityManagerFactory() throws Exception { + + EntityManagerFactory emf = mockProviderSpecificEntityManagerFactoryInterface( + "foo.bar.unknown.jpa.JpaEntityManager"); + when(emf.unwrap(null)).thenThrow(new NullPointerException()); + when(emf.unwrap(EntityManagerFactory.class)).thenReturn(emf); + + MyEntityManagerFactoryBean factoryBean = new MyEntityManagerFactoryBean(EntityManagerFactory.class, emf); + EntityManagerFactory springProxy = factoryBean.createEntityManagerFactoryProxy(emf); + + Object externalProxy = Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { EntityManagerFactory.class }, (proxy, method, args) -> method.invoke(emf, args)); + + assertThat(PersistenceProvider.fromEntityManagerFactory(springProxy)).isEqualTo(GENERIC_JPA); + assertThat(PersistenceProvider.fromEntityManagerFactory((EntityManagerFactory) externalProxy)) + .isEqualTo(GENERIC_JPA); + } + private EntityManager mockProviderSpecificEntityManagerInterface(String interfaceName) throws ClassNotFoundException { Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, @@ -128,7 +150,7 @@ private EntityManagerFactory mockProviderSpecificEntityManagerFactoryInterface(S throws ClassNotFoundException { Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, - EntityManager.class); + EntityManagerFactory.class); return (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerInterface); } @@ -181,4 +203,24 @@ private static String[] toResourcePaths(Class... interfacesToImplement) { .toArray(String[]::new); } } + + static class MyEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean { + + public MyEntityManagerFactoryBean(Class entityManagerFactoryInterface, + EntityManagerFactory entityManagerFactory) { + setEntityManagerFactoryInterface(entityManagerFactoryInterface); + ReflectionTestUtils.setField(this, "nativeEntityManagerFactory", entityManagerFactory); + + } + + @Override + protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException { + return null; + } + + @Override + protected EntityManagerFactory createEntityManagerFactoryProxy(EntityManagerFactory emf) { + return super.createEntityManagerFactoryProxy(emf); + } + } } From 4fda7f8d8771ae0e02e8a0abbfbe24c74f8a7d35 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 25 Jun 2025 14:33:06 +0200 Subject: [PATCH 124/224] Polishing. Add missing since tag. See #3892 Related ticket: #3456 --- .../springframework/data/jpa/provider/PersistenceProvider.java | 1 + 1 file changed, 1 insertion(+) 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 f46567bb63..a0bc1606ae 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 @@ -405,6 +405,7 @@ public boolean isPresent() { * @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. + * @since 4.0 */ public long getResultCount(Query resultQuery, LongSupplier countSupplier) { return countSupplier.getAsLong(); From a1885d00af1257250e3c6861e468761da5e0a26d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 26 Jun 2025 08:48:45 +0200 Subject: [PATCH 125/224] Simplify build. Remove duplicate surefire config, remove additional Eclipselink tests as we don't use these. See #3892 --- Jenkinsfile | 22 ---------- pom.xml | 83 -------------------------------------- spring-data-envers/pom.xml | 6 --- spring-data-jpa/pom.xml | 6 --- 4 files changed, 117 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index de8ad4ec91..2a026b08fa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,28 +80,6 @@ pipeline { } } } - stage("test: eclipselink-next") { - 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.main.image']).inside(p['docker.java.inside.docker']) { - sh "PROFILE=all-dbs,eclipselink-next " + - "JENKINS_USER_NAME=${p['jenkins.user.name']} " + - "ci/test.sh" - } - } - } - } - } } } diff --git a/pom.xml b/pom.xml index fb2c4c3dea..a521fad379 100755 --- a/pom.xml +++ b/pom.xml @@ -193,92 +193,9 @@ ${spring} provided - - org.jboss.logging - jboss-logging - 3.6.1.Final - - - - - 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-envers/pom.xml b/spring-data-envers/pom.xml index 43c08369f6..0bdf2c8e7e 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -60,12 +60,6 @@ ${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 cdb738558f..6801bdd39e 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -293,12 +293,6 @@ true - - org.jboss.logging - jboss-logging - 3.6.1.Final - - From ba97c46f9b8e3e4faadb7a90c858fa8aa980133b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 26 Jun 2025 09:42:35 +0200 Subject: [PATCH 126/224] Remove usage of `o.s.orm.hibernate5`. Closes #3924 --- spring-data-jpa/pom.xml | 7 ++++ ...aParametersParameterAccessorUnitTests.java | 2 +- .../src/test/resources/hjppa-test.xml | 33 ------------------- 3 files changed, 8 insertions(+), 34 deletions(-) delete mode 100644 spring-data-jpa/src/test/resources/hjppa-test.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 6801bdd39e..b9c7df6084 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -211,6 +211,13 @@ + + org.jboss.logging + jboss-logging + 3.6.1.Final + provided + + ${hibernate.groupId}.orm hibernate-vector diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessorUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessorUnitTests.java index 6ec33ba71f..b43e164a37 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessorUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessorUnitTests.java @@ -22,7 +22,7 @@ * @author Cedomir Igaly */ @ExtendWith(SpringExtension.class) -@ContextConfiguration("classpath:hjppa-test.xml") +@ContextConfiguration("classpath:infrastructure.xml") class HibernateJpaParametersParameterAccessorUnitTests { @Autowired private EntityManager em; diff --git a/spring-data-jpa/src/test/resources/hjppa-test.xml b/spring-data-jpa/src/test/resources/hjppa-test.xml deleted file mode 100644 index cec01327c5..0000000000 --- a/spring-data-jpa/src/test/resources/hjppa-test.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - org.springframework.data.jpa.domain - org.springframework.data.jpa.domain.sample - - - - - - - - - - - - - - - - - - From 176b945d00097158b0241386610048c8f1f081d7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 26 Jun 2025 11:11:00 +0200 Subject: [PATCH 127/224] Deprecate `SharedEntityManager` bean registration in favor of JPA 3.2 qualified `EntityManager` injection. We now no longer register a SharedEntityManager bean if the EntityManagerFactors is created by AbstractEntityManagerFactoryBean. Closes #3926 --- .../config/JpaRepositoryConfigExtension.java | 56 ++++++++++++++++--- .../repository/support/DefaultJpaContext.java | 27 ++++++++- ...rBeanDefinitionRegistrarPostProcessor.java | 27 ++++++++- .../data/jpa/util/BeanDefinitionUtils.java | 30 ++++++++-- .../AotFragmentTestConfigurationSupport.java | 22 +++++--- ...JpaRepositoryConfigExtensionUnitTests.java | 16 +----- .../support/DefaultJpaContextUnitTests.java | 7 ++- ...egistrarPostProcessorIntegrationTests.java | 3 +- ...ydslRepositorySupportIntegrationTests.java | 5 -- .../test/resources/application-context.xml | 2 - 10 files changed, 146 insertions(+), 49 deletions(-) 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 95495accb4..e21e956383 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,11 +43,13 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; 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.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; @@ -58,7 +60,6 @@ import org.springframework.data.jpa.repository.JpaRepository; 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; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; @@ -68,9 +69,11 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -192,10 +195,6 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf Object source = config.getSource(); - registerLazyIfNotAlreadyRegistered( - () -> new RootBeanDefinition(EntityManagerBeanDefinitionRegistrarPostProcessor.class), registry, - EM_BEAN_DEFINITION_REGISTRAR_POST_PROCESSOR_BEAN_NAME, source); - registerLazyIfNotAlreadyRegistered(() -> new RootBeanDefinition(JpaMetamodelMappingContextFactoryBean.class), registry, JPA_MAPPING_CONTEXT_BEAN_NAME, source); @@ -230,10 +229,21 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf }, registry, JpaEvaluationContextExtension.class.getName(), source); } - private String registerSharedEntityMangerIfNotAlreadyRegistered(BeanDefinitionRegistry registry, + private void registerSharedEntityMangerIfNotAlreadyRegistered(BeanDefinitionRegistry registry, RepositoryConfigurationSource config) { String entityManagerBeanRef = getEntityManagerBeanRef(config); + String sharedEntityManagerBeanRef = lookupSharedEntityManagerBeanRef(entityManagerBeanRef, registry); + + // TODO: Remove once Cannot convert value of type 'jdk.proxy2.$Proxy129 implementing + // org.hibernate.SessionFactory,org.springframework.orm.jpa.EntityManagerFactoryInfo' to required type + // 'jakarta.persistence.EntityManager' for property 'entityManager': no matching editors or conversion strategy + // found is fixed. + /*if (sharedEntityManagerBeanRef != null) { + entityManagerRefs.put(config, sharedEntityManagerBeanRef); + return; + } */ + String entityManagerBeanName = "jpaSharedEM_" + entityManagerBeanRef; if (!registry.containsBeanDefinition(entityManagerBeanName)) { @@ -247,7 +257,39 @@ private String registerSharedEntityMangerIfNotAlreadyRegistered(BeanDefinitionRe } entityManagerRefs.put(config, entityManagerBeanName); - return entityManagerBeanName; + } + + private @Nullable String lookupSharedEntityManagerBeanRef(String entityManagerBeanRef, + BeanDefinitionRegistry registry) { + + if (!registry.containsBeanDefinition(entityManagerBeanRef)) { + return null; + } + + BeanDefinitionRegistry introspect = registry; + + if (introspect instanceof ConfigurableApplicationContext cac + && cac.getBeanFactory() instanceof BeanDefinitionRegistry br) { + introspect = br; + } + + if (!(introspect instanceof ConfigurableBeanFactory cbf)) { + return null; + } + + BeanDefinition beanDefinition = cbf.getMergedBeanDefinition(entityManagerBeanRef); + + if (ObjectUtils.isEmpty(beanDefinition.getBeanClassName())) { + return null; + } + + Class beanClass = org.springframework.data.util.ClassUtils.loadIfPresent(beanDefinition.getBeanClassName(), + getClass().getClassLoader()); + + // AbstractEntityManagerFactoryBean is able to create a SharedEntityManager + return beanClass != null && AbstractEntityManagerFactoryBean.class.isAssignableFrom(beanClass) + ? entityManagerBeanRef + : null; } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultJpaContext.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultJpaContext.java index 220ac48587..464fd122ff 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultJpaContext.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/DefaultJpaContext.java @@ -15,12 +15,14 @@ */ package org.springframework.data.jpa.repository.support; -import java.util.List; -import java.util.Set; - import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.ManagedType; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaContext; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -37,6 +39,25 @@ public class DefaultJpaContext implements JpaContext { private final MultiValueMap, EntityManager> entityManagers; + /** + * Creates a new {@link DefaultJpaContext} for the given {@link Set} of {@link EntityManager}s. + * + * @param entityManagers must not be {@literal null}. + */ + @Autowired + public DefaultJpaContext(ObjectProvider entityManagers) { + + Assert.notNull(entityManagers, "EntityManagerFactories must not be null"); + + this.entityManagers = new LinkedMultiValueMap<>(); + + for (EntityManager em : entityManagers) { + for (ManagedType managedType : em.getMetamodel().getManagedTypes()) { + this.entityManagers.add(managedType.getJavaType(), em); + } + } + } + /** * Creates a new {@link DefaultJpaContext} for the given {@link Set} of {@link EntityManager}s. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessor.java index 4d84dfc0d4..61096544d8 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessor.java @@ -20,6 +20,8 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import java.util.function.BiPredicate; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -44,9 +46,31 @@ * @author Réda Housni Alaoui * @author Mark Paluch * @author Donghun Shin + * @deprecated since 4.0, in favor of using either {@link org.springframework.orm.jpa.AbstractEntityManagerFactoryBean} + * that provides a shared {@link EntityManager} or using {@link SharedEntityManagerCreator} directly in your + * configuration. */ +@Deprecated(since = "4.0") public class EntityManagerBeanDefinitionRegistrarPostProcessor implements BeanFactoryPostProcessor, Ordered { + private final BiPredicate decoratorPredicate; + + public EntityManagerBeanDefinitionRegistrarPostProcessor() { + this((beanName, beanDefinition) -> true); + } + + /** + * Creates a new {@code EntityManagerBeanDefinitionRegistrarPostProcessor} allowing to filter which + * {@link EntityManagerFactory} beans should be decorated with a {@code SharedEntityManagerCreator}. + * + * @param decoratorPredicate the predicate to determine whether a given named {@link BeanDefinition} should be + * decorated with a {@code SharedEntityManagerCreator}. + * @since 4.0 + */ + public EntityManagerBeanDefinitionRegistrarPostProcessor(BiPredicate decoratorPredicate) { + this.decoratorPredicate = decoratorPredicate; + } + @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 10; @@ -55,7 +79,8 @@ public int getOrder() { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - for (EntityManagerFactoryBeanDefinition definition : getEntityManagerFactoryBeanDefinitions(beanFactory)) { + for (EntityManagerFactoryBeanDefinition definition : getEntityManagerFactoryBeanDefinitions(beanFactory, + decoratorPredicate)) { BeanFactory definitionFactory = definition.getBeanFactory(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/BeanDefinitionUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/BeanDefinitionUtils.java index a5c181ee0b..3216abef21 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/BeanDefinitionUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/util/BeanDefinitionUtils.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.BiPredicate; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; @@ -96,20 +97,37 @@ public static Iterable getEntityManagerFactoryBeanNames(ListableBeanFact */ public static Collection getEntityManagerFactoryBeanDefinitions( ConfigurableListableBeanFactory beanFactory) { + return getEntityManagerFactoryBeanDefinitions(beanFactory, (beanName, beanDefinition) -> true); + } + + /** + * Returns {@link EntityManagerFactoryBeanDefinition} instances for all {@link BeanDefinition} registered in the given + * {@link ConfigurableListableBeanFactory} hierarchy. + * + * @param beanFactory must not be {@literal null}. + * @param beanDefinitionBiPredicate predicate to determine whether a {@link EntityManagerFactory} bean should be + * decorated with a {@code SharedEntityManager} bean definition. + * @return + * @since 4.0 + */ + public static Collection getEntityManagerFactoryBeanDefinitions( + ConfigurableListableBeanFactory beanFactory, BiPredicate beanDefinitionBiPredicate) { Set definitions = new HashSet<>(); for (Class type : EMF_TYPES) { for (String name : beanFactory.getBeanNamesForType(type, true, false)) { - registerEntityManagerFactoryBeanDefinition(transformedBeanName(name), beanFactory, definitions); + registerEntityManagerFactoryBeanDefinition(transformedBeanName(name), beanFactory, definitions, + beanDefinitionBiPredicate); } } BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory(); if (parentBeanFactory instanceof ConfigurableListableBeanFactory parentConfigurableListableBeanFactory) { - definitions.addAll(getEntityManagerFactoryBeanDefinitions(parentConfigurableListableBeanFactory)); + definitions.addAll( + getEntityManagerFactoryBeanDefinitions(parentConfigurableListableBeanFactory, beanDefinitionBiPredicate)); } return definitions; @@ -122,9 +140,11 @@ public static Collection getEntityManagerFac * @param name * @param beanFactory * @param definitions + * @param decoratorPredicate */ private static void registerEntityManagerFactoryBeanDefinition(String name, - ConfigurableListableBeanFactory beanFactory, Collection definitions) { + ConfigurableListableBeanFactory beanFactory, Collection definitions, + BiPredicate decoratorPredicate) { BeanDefinition definition = beanFactory.getBeanDefinition(name); @@ -139,7 +159,9 @@ private static void registerEntityManagerFactoryBeanDefinition(String name, return; } - definitions.add(new EntityManagerFactoryBeanDefinition(name, beanFactory)); + if (decoratorPredicate.test(name, definition)) { + definitions.add(new EntityManagerFactoryBeanDefinition(name, beanFactory)); + } } /** 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 87243096db..ad36402a68 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 @@ -16,7 +16,6 @@ package org.springframework.data.jpa.repository.aot; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -28,12 +27,14 @@ 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.config.RuntimeBeanReference; 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.DefaultBeanNameGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportResource; +import org.springframework.context.annotation.Primary; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; @@ -46,7 +47,7 @@ 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.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.util.ReflectionUtils; /** @@ -76,6 +77,16 @@ EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironmen Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); } + // TODO: Remove once Cannot convert value of type 'jdk.proxy2.$Proxy129 implementing + // org.hibernate.SessionFactory,org.springframework.orm.jpa.EntityManagerFactoryInfo' to required type + // 'jakarta.persistence.EntityManager' for property 'entityManager': no matching editors or conversion strategy found + // is fixed. + @Bean + @Primary + EntityManager entityManager(LocalContainerEntityManagerFactoryBean factoryBean) throws Exception { + return factoryBean.getObject(EntityManager.class); + } + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { @@ -85,7 +96,7 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder .genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") - .addConstructorArgReference("jpaSharedEM_entityManagerFactory") + .addConstructorArgValue(new RuntimeBeanReference(EntityManager.class)) .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { @@ -125,11 +136,6 @@ private Object getFragmentFacadeProxy(Object fragment) { }); } - @Bean("jpaSharedEM_entityManagerFactory") - EntityManager sharedEntityManagerCreator(EntityManagerFactory emf) { - return SharedEntityManagerCreator.createSharedEntityManager(emf); - } - private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( TestJpaAotRepositoryContext repositoryContext) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java index df4e2097e5..6412c70f88 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java @@ -30,8 +30,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -149,20 +149,6 @@ void exposesJpaAotProcessor() { .isEqualTo(JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor.class); } - @Test // GH-2730 - void shouldNotRegisterEntityManagerAsSynthetic() { - - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - - RepositoryConfigurationExtension extension = new JpaRepositoryConfigExtension(); - extension.registerBeansForRoot(factory, configSource); - - AbstractBeanDefinition bd = (AbstractBeanDefinition) factory.getBeanDefinition("jpaSharedEM_" - + configSource.getAttribute("entityManagerFactoryRef").orElse("entityManagerFactory")); - - assertThat(bd.isSynthetic()).isEqualTo(false); - } - private void assertOnlyOnePersistenceAnnotationBeanPostProcessorRegistered(DefaultListableBeanFactory factory, String expectedBeanName) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextUnitTests.java index 899cbd406d..cc36108faf 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextUnitTests.java @@ -17,10 +17,11 @@ import static org.assertj.core.api.Assertions.*; -import java.util.Collections; - import jakarta.persistence.EntityManager; +import java.util.Collections; +import java.util.Set; + import org.junit.jupiter.api.Test; /** @@ -34,7 +35,7 @@ class DefaultJpaContextUnitTests { @Test // DATAJPA-669 void rejectsNullEntityManagers() { - assertThatIllegalArgumentException().isThrownBy(() -> new DefaultJpaContext(null)); + assertThatIllegalArgumentException().isThrownBy(() -> new DefaultJpaContext((Set) null)); } @Test // DATAJPA-669 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java index bf2fa3070e..2ac1d8a501 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EntityManagerBeanDefinitionRegistrarPostProcessorIntegrationTests.java @@ -38,6 +38,7 @@ * @author Oliver Gierke * @author Jens Schauder * @author Réda Housni Alaoui + * @author Mark Paluch */ @ExtendWith(SpringExtension.class) @ContextConfiguration @@ -61,7 +62,7 @@ static class Config { @Autowired @Qualifier("entityManagerFactory") EntityManagerFactory emf; @Bean - public static EntityManagerBeanDefinitionRegistrarPostProcessor processor() { + static EntityManagerBeanDefinitionRegistrarPostProcessor postProcessor() { return new EntityManagerBeanDefinitionRegistrarPostProcessor(); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupportIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupportIntegrationTests.java index eb411ddce3..81a9edc627 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupportIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/QuerydslRepositorySupportIntegrationTests.java @@ -61,11 +61,6 @@ public void setEntityManager(EntityManager entityManager) { }; } - @Bean - static EntityManagerBeanDefinitionRegistrarPostProcessor entityManagerBeanDefinitionRegistrarPostProcessor() { - return new EntityManagerBeanDefinitionRegistrarPostProcessor(); - } - @Bean public ReconfiguringUserRepositoryImpl reconfiguringUserRepositoryImpl() { return new ReconfiguringUserRepositoryImpl(); diff --git a/spring-data-jpa/src/test/resources/application-context.xml b/spring-data-jpa/src/test/resources/application-context.xml index 3f10133b5d..bc6692f19c 100644 --- a/spring-data-jpa/src/test/resources/application-context.xml +++ b/spring-data-jpa/src/test/resources/application-context.xml @@ -37,8 +37,6 @@ - - From 1678aba2214cfa51f15cb219006854350134f34a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 26 Jun 2025 11:12:35 +0200 Subject: [PATCH 128/224] Upgrade to Hibernate 7.0.3.Final. Closes #3925 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a521fad379..fedd155df9 100755 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 4.13.2 5.0.0-B07 5.0.0-SNAPSHOT - 7.0.0.Final - 7.0.1-SNAPSHOT + 7.0.3.Final + 7.0.4-SNAPSHOT 7.1.0-SNAPSHOT 2.7.4

        2.3.232

        From adbac41be2977b505fc406b968b4f0ee570917ea Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 27 Jun 2025 13:32:55 +0200 Subject: [PATCH 129/224] Use named and typed `RuntimeBeanReference` for shared EntityManager injection. We now declare the target type for RuntimeBeanReference to indicate the desired target type so that SmartFactoryBeans can be used for property value injection of the shared EntityManager. See #3926 --- .../config/JpaRepositoryConfigExtension.java | 13 ++++++------- .../aot/AotFragmentTestConfigurationSupport.java | 13 ------------- 2 files changed, 6 insertions(+), 20 deletions(-) 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 e21e956383..6b8069418b 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.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PersistenceContext; @@ -45,6 +46,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -138,7 +140,8 @@ 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)) { - builder.addPropertyReference("entityManager", entityManagerRefs.get(source)); + builder.addPropertyValue("entityManager", + new RuntimeBeanReference(entityManagerRefs.get(source), EntityManager.class)); } builder.addPropertyValue(ESCAPE_CHARACTER_PROPERTY, getEscapeCharacter(source).orElse('\\')); builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME); @@ -235,14 +238,10 @@ private void registerSharedEntityMangerIfNotAlreadyRegistered(BeanDefinitionRegi String entityManagerBeanRef = getEntityManagerBeanRef(config); String sharedEntityManagerBeanRef = lookupSharedEntityManagerBeanRef(entityManagerBeanRef, registry); - // TODO: Remove once Cannot convert value of type 'jdk.proxy2.$Proxy129 implementing - // org.hibernate.SessionFactory,org.springframework.orm.jpa.EntityManagerFactoryInfo' to required type - // 'jakarta.persistence.EntityManager' for property 'entityManager': no matching editors or conversion strategy - // found is fixed. - /*if (sharedEntityManagerBeanRef != null) { + if (sharedEntityManagerBeanRef != null) { entityManagerRefs.put(config, sharedEntityManagerBeanRef); return; - } */ + } String entityManagerBeanName = "jpaSharedEM_" + entityManagerBeanRef; 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 ad36402a68..ddf2cf9eb4 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 @@ -32,9 +32,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportResource; -import org.springframework.context.annotation.Primary; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; @@ -47,7 +45,6 @@ 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.LocalContainerEntityManagerFactoryBean; import org.springframework.util.ReflectionUtils; /** @@ -77,16 +74,6 @@ EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironmen Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); } - // TODO: Remove once Cannot convert value of type 'jdk.proxy2.$Proxy129 implementing - // org.hibernate.SessionFactory,org.springframework.orm.jpa.EntityManagerFactoryInfo' to required type - // 'jakarta.persistence.EntityManager' for property 'entityManager': no matching editors or conversion strategy found - // is fixed. - @Bean - @Primary - EntityManager entityManager(LocalContainerEntityManagerFactoryBean factoryBean) throws Exception { - return factoryBean.getObject(EntityManager.class); - } - @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { From 4837601e42c39ffc51a2f9abe90df60239fd71e4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Jul 2025 10:24:09 +0200 Subject: [PATCH 130/224] Simplify pom. Inherit Microbenchmark from parent pom. See #3892 --- pom.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pom.xml b/pom.xml index fedd155df9..51123083cb 100755 --- a/pom.xml +++ b/pom.xml @@ -58,14 +58,6 @@ jmh - - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-junit5 - 0.5.0.RELEASE - test - - jitpack From d9c102e96585821561f705d6de7645c02a814364 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 28 May 2025 11:07:03 +0200 Subject: [PATCH 131/224] Make identification variables and the `SELECT` clause in JPQL optional. Original pull request: #3903 Closes #3902 --- .../data/jpa/repository/query/Eql.g4 | 25 ++-- .../data/jpa/repository/query/Jpql.g4 | 22 +++- .../query/EqlCountQueryTransformer.java | 57 +++++++- .../query/EqlQueryIntrospector.java | 11 +- .../repository/query/EqlQueryRenderer.java | 110 +++++++++++++--- .../query/EqlSortedQueryTransformer.java | 40 +++++- .../query/HqlCountQueryTransformer.java | 11 +- .../query/HqlQueryIntrospector.java | 9 +- .../repository/query/HqlQueryRenderer.java | 19 ++- .../query/JpqlCountQueryTransformer.java | 55 +++++++- .../query/JpqlQueryIntrospector.java | 25 ++-- .../repository/query/JpqlQueryRenderer.java | 112 ++++++++++++++-- .../query/JpqlSortedQueryTransformer.java | 40 +++++- .../query/EqlQueryRendererTests.java | 23 ++++ .../query/EqlQueryTransformerTests.java | 122 ++++++++++++++---- .../query/HqlQueryRendererTests.java | 29 +++++ .../query/HqlQueryTransformerTests.java | 102 +++++++++++---- .../query/JpqlQueryRendererTests.java | 42 ++++-- .../query/JpqlQueryTransformerTests.java | 105 +++++++++++---- 19 files changed, 769 insertions(+), 190 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 86b0111ca1..71d9c05ab6 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 @@ -18,10 +18,9 @@ grammar Eql; @header { /** * Implementation of EclipseLink Query Language (EQL) - * See: - * * https://eclipse.dev/eclipselink/documentation/3.0/jpa/extensions/jpql.htm - * * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL * + * @see https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL + * @see https://eclipse.dev/eclipselink/documentation/3.0/jpa/extensions/jpql.htm * @author Greg Turnquist * @author Christoph Strobl * @since 3.2 @@ -43,7 +42,8 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # SelectQuery + | from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # FromQuery ; setOperator @@ -80,7 +80,7 @@ identification_variable_declaration ; range_variable_declaration - : (entity_name|function_invocation) AS? identification_variable + : (entity_name|function_invocation) AS? identification_variable? ; join @@ -246,14 +246,15 @@ orderby_clause : ORDER BY orderby_item (',' orderby_item)* ; -// TODO Error in spec BNF, correctly shown elsewhere in spec. orderby_item - : state_field_path_expression (ASC | DESC)? nullsPrecedence? - | general_identification_variable (ASC | DESC)? nullsPrecedence? - | result_variable (ASC | DESC)? nullsPrecedence? - | string_expression (ASC | DESC)? nullsPrecedence? - | scalar_expression (ASC | DESC)? nullsPrecedence? - | + : orderby_expression (ASC | DESC)? nullsPrecedence? + ; + +orderby_expression + : state_field_path_expression + | general_identification_variable + | string_expression + | scalar_expression ; nullsPrecedence 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 b3afdb9b1e..6632690a42 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,8 @@ ql_statement ; select_statement - : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? + : select_clause from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # SelectQuery + | from_clause (where_clause)? (groupby_clause)? (having_clause)? (orderby_clause)? (set_fuction)? # FromQuery ; setOperator @@ -72,6 +73,7 @@ from_clause identificationVariableDeclarationOrCollectionMemberDeclaration : identification_variable_declaration | collection_member_declaration + | '(' subquery ')' identification_variable ; identification_variable_declaration @@ -79,11 +81,11 @@ identification_variable_declaration ; range_variable_declaration - : entity_name AS? identification_variable + : entity_name AS? identification_variable? ; join - : join_spec join_association_path_expression AS? identification_variable (join_condition)? + : join_spec join_association_path_expression AS? identification_variable? (join_condition)? ; fetch_join @@ -106,11 +108,11 @@ join_association_path_expression ; join_collection_valued_path_expression - : identification_variable '.' (single_valued_embeddable_object_field '.')* collection_valued_field + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* collection_valued_field ; join_single_valued_path_expression - : identification_variable '.' (single_valued_embeddable_object_field '.')* single_valued_object_field + : (identification_variable '.')? (single_valued_embeddable_object_field '.')* single_valued_object_field ; collection_member_declaration @@ -244,9 +246,15 @@ orderby_clause : ORDER BY orderby_item (',' orderby_item)* ; -// TODO Error in spec BNF, correctly shown elsewhere in spec. orderby_item - : (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? nullsPrecedence? + : orderby_expression (ASC | DESC)? nullsPrecedence? + ; + +orderby_expression + : state_field_path_expression + | general_identification_variable + | string_expression + | scalar_expression ; nullsPrecedence 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 7b2aa13fda..9b4f06b381 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 @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; +import org.springframework.util.StringUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query into a @@ -43,7 +44,7 @@ class EqlCountQueryTransformer extends EqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -63,6 +64,49 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext return builder; } + @Override + public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + QueryRendererBuilder countBuilder = QueryRenderer.builder(); + countBuilder.append(TOKEN_SELECT_COUNT); + + if (countProjection != null) { + countBuilder.append(QueryTokens.token(countProjection)); + } else { + if (primaryFromAlias == null) { + countBuilder.append(TOKEN_DOUBLE_UNDERSCORE); + } else { + countBuilder.append(QueryTokens.token(primaryFromAlias)); + } + } + + countBuilder.append(TOKEN_CLOSE_PAREN); + + builder.appendExpression(countBuilder); + + if (ctx.from_clause() != null) { + builder.appendExpression(visit(ctx.from_clause())); + if (primaryFromAlias == null) { + builder.append(TOKEN_AS); + builder.append(TOKEN_DOUBLE_UNDERSCORE); + } + } + + if (ctx.where_clause() != null) { + builder.appendExpression(visit(ctx.where_clause())); + } + if (ctx.groupby_clause() != null) { + builder.appendExpression(visit(ctx.groupby_clause())); + } + if (ctx.having_clause() != null) { + builder.appendExpression(visit(ctx.having_clause())); + } + + return builder; + } + @Override public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { @@ -78,14 +122,21 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { 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 { + if (ctx.select_item().isEmpty()) { + // cannot happen as per grammar, but you never know… + nested.append(QueryTokens.token("1")); + } else { + nested.append(visit(ctx.select_item().get(0))); + } } } else { - builder.append(QueryTokens.token(countProjection)); if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); } + nested.append(QueryTokens.token(countProjection)); } builder.appendInline(nested); 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 5f261a16bb..8be9930b9f 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 @@ -61,8 +61,9 @@ public Void visitSelect_clause(EqlParser.Select_clauseContext ctx) { @Override public Void visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) { - if (primaryFromAlias == null) { - primaryFromAlias = capturePrimaryAlias(ctx); + if (primaryFromAlias == null && ctx.identification_variable() != null && !EqlQueryRenderer.isSubquery(ctx) + && !EqlQueryRenderer.isSetQuery(ctx)) { + primaryFromAlias = ctx.identification_variable().getText(); } return super.visitRange_variable_declaration(ctx); @@ -75,11 +76,6 @@ public Void visitConstructor_expression(EqlParser.Constructor_expressionContext return super.visitConstructor_expression(ctx); } - private static String capturePrimaryAlias(Range_variable_declarationContext ctx) { - return ctx.identification_variable() != null ? ctx.identification_variable().getText() - : ctx.entity_name().getText(); - } - private static List captureSelectItems(List selections, EqlQueryRenderer itemRenderer) { @@ -94,4 +90,5 @@ private static List captureSelectItems(List { + /** + * Is this AST tree a {@literal subquery}? + * + * @return boolean + */ + static boolean isSubquery(ParserRuleContext ctx) { + + if (ctx instanceof EqlParser.SubqueryContext) { + return true; + } else if (ctx instanceof EqlParser.Update_statementContext) { + return false; + } else if (ctx instanceof EqlParser.Delete_statementContext) { + return false; + } else { + return ctx.getParent() != null && isSubquery(ctx.getParent()); + } + } + + /** + * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? + * + * @return boolean + */ + static boolean isSetQuery(ParserRuleContext ctx) { + + if (ctx instanceof EqlParser.Set_fuctionContext) { + return true; + } + + return ctx.getParent() != null && isSetQuery(ctx.getParent()); + } + @Override public QueryTokenStream visitStart(EqlParser.StartContext ctx) { return visit(ctx.ql_statement()); @@ -56,7 +90,7 @@ public QueryTokenStream visitQl_statement(EqlParser.Ql_statementContext ctx) { } @Override - public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -86,6 +120,36 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext return builder; } + @Override + public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.from_clause())); + + if (ctx.where_clause() != null) { + builder.appendExpression(visit(ctx.where_clause())); + } + + if (ctx.groupby_clause() != null) { + builder.appendExpression(visit(ctx.groupby_clause())); + } + + if (ctx.having_clause() != null) { + builder.appendExpression(visit(ctx.having_clause())); + } + + if (ctx.orderby_clause() != null) { + builder.appendExpression(visit(ctx.orderby_clause())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } + + return builder; + } + @Override public QueryTokenStream visitUpdate_statement(EqlParser.Update_statementContext ctx) { @@ -149,7 +213,9 @@ public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMember QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(nested); - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } else { @@ -185,7 +251,9 @@ public QueryTokenStream visitRange_variable_declaration(EqlParser.Range_variable builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -309,6 +377,7 @@ public QueryTokenStream visitJoin_collection_valued_path_expression( EqlParser.Join_collection_valued_path_expressionContext ctx) { List items = new ArrayList<>(2 + ctx.single_valued_embeddable_object_field().size()); + if (ctx.identification_variable() != null) { items.add(ctx.identification_variable()); } @@ -348,7 +417,9 @@ public QueryTokenStream visitCollection_member_declaration(EqlParser.Collection_ builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -830,22 +901,11 @@ public QueryTokenStream visitOrderby_item(EqlParser.Orderby_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.state_field_path_expression() != null) { - builder.appendExpression(visit(ctx.state_field_path_expression())); - } else if (ctx.general_identification_variable() != null) { - builder.appendExpression(visit(ctx.general_identification_variable())); - } else if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } else if (ctx.string_expression() != null) { - builder.appendExpression(visit(ctx.string_expression())); - } else if (ctx.scalar_expression() != null) { - builder.appendExpression(visit(ctx.scalar_expression())); - } + builder.appendExpression(visit(ctx.orderby_expression())); 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())); } @@ -856,6 +916,22 @@ public QueryTokenStream visitOrderby_item(EqlParser.Orderby_itemContext ctx) { return builder; } + @Override + public QueryTokenStream visitOrderby_expression(EqlParser.Orderby_expressionContext ctx) { + + if (ctx.state_field_path_expression() != null) { + return visit(ctx.state_field_path_expression()); + } else if (ctx.general_identification_variable() != null) { + return visit(ctx.general_identification_variable()); + } else if (ctx.string_expression() != null) { + return visit(ctx.string_expression()); + } else if (ctx.scalar_expression() != null) { + return visit(ctx.scalar_expression()); + } + + return QueryTokenStream.empty(); + } + @Override public QueryTokenStream visitNullsPrecedence(EqlParser.NullsPrecedenceContext ctx) { 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 3c4bd92d72..2cb03ae4df 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 @@ -54,7 +54,7 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -76,7 +76,35 @@ public QueryTokenStream visitSelect_statement(EqlParser.Select_statementContext if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } else { - doVisitOrderBy(builder, ctx); + doVisitOrderBy(builder, ctx.orderby_clause()); + } + + return builder; + } + + @Override + public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.from_clause())); + + if (ctx.where_clause() != null) { + builder.appendExpression(visit(ctx.where_clause())); + } + + if (ctx.groupby_clause() != null) { + builder.appendExpression(visit(ctx.groupby_clause())); + } + + if (ctx.having_clause() != null) { + builder.appendExpression(visit(ctx.having_clause())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx.orderby_clause()); } return builder; @@ -138,10 +166,10 @@ public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { return tokens; } - private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Orderby_clauseContext ctx) { - if (ctx.orderby_clause() != null) { - QueryTokenStream existingOrder = visit(ctx.orderby_clause()); + if (ctx != null) { + QueryTokenStream existingOrder = visit(ctx); if (sort.isSorted()) { builder.appendInline(existingOrder); } else { @@ -153,7 +181,7 @@ private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_state List sortBy = transformerSupport.orderBy(primaryFromAlias, sort); - if (ctx.orderby_clause() != null) { + if (ctx != null) { QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy); 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 a4bd3af55e..6f9c3bb878 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 @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.query.HqlParser.SelectClauseContext; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream; +import org.springframework.util.StringUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query into a @@ -164,11 +165,9 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { boolean usesDistinct = ctx.DISTINCT() != null; QueryRendererBuilder nested = QueryRenderer.builder(); if (countProjection == null) { - QueryTokenStream selection = visit(ctx.selectionList()); if (usesDistinct) { - nested.append(QueryTokens.expression(ctx.DISTINCT())); - nested.append(getDistinctCountSelection(selection)); + nested.append(getDistinctCountSelection(visit(ctx.selectionList()))); } else { // with CTE primary alias fails with hibernate (WITH entities AS (…) SELECT count(c) FROM entities c) @@ -176,9 +175,7 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { nested.append(QueryTokens.token("*")); } else { - if (selection.size() == 1) { - nested.append(selection); - } else if (primaryFromAlias != null) { + if (StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); } else { nested.append(QueryTokens.token("*")); @@ -186,10 +183,10 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { } } } else { - builder.append(QueryTokens.token(countProjection)); if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); } + nested.append(QueryTokens.token(countProjection)); } builder.appendInline(nested); 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 ba88ab2df1..c32bf27d3f 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 @@ -69,7 +69,8 @@ public Void visitCte(HqlParser.CteContext ctx) { @Override public Void visitRootEntity(HqlParser.RootEntityContext ctx) { - if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx) + && !HqlQueryRenderer.isSetQuery(ctx)) { this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); } @@ -79,7 +80,8 @@ public Void visitRootEntity(HqlParser.RootEntityContext ctx) { @Override public Void visitRootSubquery(HqlParser.RootSubqueryContext ctx) { - if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx) + && !HqlQueryRenderer.isSetQuery(ctx)) { this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); } @@ -89,7 +91,8 @@ public Void visitRootSubquery(HqlParser.RootSubqueryContext ctx) { @Override public Void visitRootFunction(HqlParser.RootFunctionContext ctx) { - if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx)) { + if (this.primaryFromAlias == null && ctx.variable() != null && !HqlQueryRenderer.isSubquery(ctx) + && !HqlQueryRenderer.isSetQuery(ctx)) { this.primaryFromAlias = capturePrimaryAlias(ctx.variable()); this.hasFromFunction = true; } 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 61f3fcf215..d959246407 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 @@ -38,7 +38,7 @@ class HqlQueryRenderer extends HqlBaseVisitor { /** - * Is this select clause a {@literal subquery}? + * Is this AST tree a {@literal subquery}? * * @return boolean */ @@ -59,6 +59,23 @@ static boolean isSubquery(ParserRuleContext ctx) { } } + /** + * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? + * + * @return boolean + */ + static boolean isSetQuery(ParserRuleContext ctx) { + + if (ctx instanceof HqlParser.OrderedQueryContext + && ctx.getParent() instanceof HqlParser.QueryExpressionContext qec) { + if (qec.orderedQuery().indexOf(ctx) != 0) { + return true; + } + } + + return ctx.getParent() != null && isSetQuery(ctx.getParent()); + } + @Override public QueryTokenStream visitStart(HqlParser.StartContext ctx) { return visit(ctx.ql_statement()); 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 6318d8acfd..af7686fac2 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,7 +44,7 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -60,8 +60,48 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext if (ctx.having_clause() != null) { builder.appendExpression(visit(ctx.having_clause())); } - if (ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); + + return builder; + } + + @Override + public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + QueryRendererBuilder countBuilder = QueryRenderer.builder(); + countBuilder.append(TOKEN_SELECT_COUNT); + + if (countProjection != null) { + countBuilder.append(QueryTokens.token(countProjection)); + } else { + if (primaryFromAlias == null) { + countBuilder.append(TOKEN_DOUBLE_UNDERSCORE); + } else { + countBuilder.append(QueryTokens.token(primaryFromAlias)); + } + } + + countBuilder.append(TOKEN_CLOSE_PAREN); + + builder.appendExpression(countBuilder); + + if (ctx.from_clause() != null) { + builder.appendExpression(visit(ctx.from_clause())); + if (primaryFromAlias == null) { + builder.append(TOKEN_AS); + builder.append(TOKEN_DOUBLE_UNDERSCORE); + } + } + + if (ctx.where_clause() != null) { + builder.appendExpression(visit(ctx.where_clause())); + } + if (ctx.groupby_clause() != null) { + builder.appendExpression(visit(ctx.groupby_clause())); + } + if (ctx.having_clause() != null) { + builder.appendExpression(visit(ctx.having_clause())); } return builder; @@ -85,13 +125,18 @@ public QueryRendererBuilder visitSelect_clause(JpqlParser.Select_clauseContext c } else if (StringUtils.hasText(primaryFromAlias)) { nested.append(QueryTokens.token(primaryFromAlias)); } else { - throw new IllegalStateException("No primary alias present"); + if (ctx.select_item().isEmpty()) { + // cannot happen as per grammar, but you never know… + nested.append(QueryTokens.token("1")); + } else { + nested.append(visit(ctx.select_item().get(0))); + } } } else { - builder.append(QueryTokens.token(countProjection)); if (usesDistinct) { nested.append(QueryTokens.expression(ctx.DISTINCT())); } + nested.append(QueryTokens.token(countProjection)); } builder.appendInline(nested); 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 43f6f7fd1f..f819778ce6 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 @@ -46,39 +46,34 @@ public QueryInformation getParsedQueryInformation() { } @Override - public Void visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) { + public Void visitSelect_clause(JpqlParser.Select_clauseContext ctx) { - if (primaryFromAlias == null) { - primaryFromAlias = capturePrimaryAlias(ctx); + if (!projectionProcessed) { + projection = captureSelectItems(ctx.select_item(), renderer); + projectionProcessed = true; } - return super.visitRange_variable_declaration(ctx); + return super.visitSelect_clause(ctx); } @Override - public Void visitSelect_clause(JpqlParser.Select_clauseContext ctx) { + public Void visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) { - if (!projectionProcessed) { - projection = captureSelectItems(ctx.select_item(), renderer); - projectionProcessed = true; + if (primaryFromAlias == null && ctx.identification_variable() != null && !JpqlQueryRenderer.isSubquery(ctx) + && !JpqlQueryRenderer.isSetQuery(ctx)) { + primaryFromAlias = ctx.identification_variable().getText(); } - return super.visitSelect_clause(ctx); + return super.visitRange_variable_declaration(ctx); } @Override public Void visitConstructor_expression(JpqlParser.Constructor_expressionContext ctx) { hasConstructorExpression = true; - return super.visitConstructor_expression(ctx); } - private static String capturePrimaryAlias(JpqlParser.Range_variable_declarationContext ctx) { - return ctx.identification_variable() != null ? ctx.identification_variable().getText() - : ctx.entity_name().getText(); - } - private static List captureSelectItems(List selections, JpqlQueryRenderer itemRenderer) { 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 3167e09514..4b839dea91 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 @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext; @@ -35,11 +36,44 @@ * * @author Greg Turnquist * @author Christoph Strobl + * @author Mark Paluch * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode" }) class JpqlQueryRenderer extends JpqlBaseVisitor { + /** + * Is this AST tree a {@literal subquery}? + * + * @return boolean + */ + static boolean isSubquery(ParserRuleContext ctx) { + + if (ctx instanceof JpqlParser.SubqueryContext) { + return true; + } else if (ctx instanceof JpqlParser.Update_statementContext) { + return false; + } else if (ctx instanceof JpqlParser.Delete_statementContext) { + return false; + } else { + return ctx.getParent() != null && isSubquery(ctx.getParent()); + } + } + + /** + * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? + * + * @return boolean + */ + static boolean isSetQuery(ParserRuleContext ctx) { + + if (ctx instanceof JpqlParser.Set_fuctionContext) { + return true; + } + + return ctx.getParent() != null && isSetQuery(ctx.getParent()); + } + @Override public QueryTokenStream visitStart(JpqlParser.StartContext ctx) { return visit(ctx.ql_statement()); @@ -60,7 +94,7 @@ public QueryTokenStream visitQl_statement(JpqlParser.Ql_statementContext ctx) { } @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -90,6 +124,36 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext return builder; } + @Override + public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.from_clause())); + + if (ctx.where_clause() != null) { + builder.appendExpression(visit(ctx.where_clause())); + } + + if (ctx.groupby_clause() != null) { + builder.appendExpression(visit(ctx.groupby_clause())); + } + + if (ctx.having_clause() != null) { + builder.appendExpression(visit(ctx.having_clause())); + } + + if (ctx.orderby_clause() != null) { + builder.appendExpression(visit(ctx.orderby_clause())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } + + return builder; + } + @Override public QueryTokenStream visitUpdate_statement(JpqlParser.Update_statementContext ctx) { @@ -173,7 +237,9 @@ public QueryTokenStream visitRange_variable_declaration(JpqlParser.Range_variabl builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -297,9 +363,12 @@ public QueryTokenStream visitJoin_association_path_expression( public QueryTokenStream visitJoin_collection_valued_path_expression( JpqlParser.Join_collection_valued_path_expressionContext ctx) { - List items = new ArrayList<>(3 + ctx.single_valued_embeddable_object_field().size()); + List items = new ArrayList<>(2 + ctx.single_valued_embeddable_object_field().size()); + + if (ctx.identification_variable() != null) { + items.add(ctx.identification_variable()); + } - items.add(ctx.identification_variable()); items.addAll(ctx.single_valued_embeddable_object_field()); items.add(ctx.collection_valued_field()); @@ -333,7 +402,9 @@ public QueryTokenStream visitCollection_member_declaration(JpqlParser.Collection builder.append(QueryTokens.expression(ctx.AS())); } - builder.appendExpression(visit(ctx.identification_variable())); + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } return builder; } @@ -581,6 +652,7 @@ public QueryTokenStream visitDelete_clause(JpqlParser.Delete_clauseContext ctx) builder.append(QueryTokens.expression(ctx.DELETE())); builder.append(QueryTokens.expression(ctx.FROM())); builder.appendExpression(visit(ctx.entity_name())); + if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } @@ -801,7 +873,7 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx builder.append(QueryTokens.expression(ctx.ORDER())); builder.append(QueryTokens.expression(ctx.BY())); - builder.appendExpression(QueryTokenStream.concat(ctx.orderby_item(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokenStream.concat(ctx.orderby_item(), this::visit, TOKEN_COMMA)); return builder; } @@ -811,13 +883,7 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.state_field_path_expression() != null) { - builder.appendExpression(visit(ctx.state_field_path_expression())); - } else if (ctx.general_identification_variable() != null) { - builder.appendExpression(visit(ctx.general_identification_variable())); - } else if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } + builder.appendExpression(visit(ctx.orderby_expression())); if (ctx.ASC() != null) { builder.append(QueryTokens.expression(ctx.ASC())); @@ -826,12 +892,28 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { } if (ctx.nullsPrecedence() != null) { - builder.append(visit(ctx.nullsPrecedence())); + builder.appendExpression(visit(ctx.nullsPrecedence())); } return builder; } + @Override + public QueryTokenStream visitOrderby_expression(JpqlParser.Orderby_expressionContext ctx) { + + if (ctx.state_field_path_expression() != null) { + return visit(ctx.state_field_path_expression()); + } else if (ctx.general_identification_variable() != null) { + return visit(ctx.general_identification_variable()); + } else if (ctx.string_expression() != null) { + return visit(ctx.string_expression()); + } else if (ctx.scalar_expression() != null) { + return visit(ctx.scalar_expression()); + } + + return QueryTokenStream.empty(); + } + @Override public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) { @@ -1981,9 +2063,11 @@ public QueryTokenStream visitType_cast_function(JpqlParser.Type_cast_functionCon builder.append(QueryTokens.token(ctx.CAST())); builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); + if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } + builder.appendInline(visit(ctx.identification_variable())); if (!CollectionUtils.isEmpty(ctx.numeric_literal())) { 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 807140c5b3..9a86eb4424 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 @@ -53,7 +53,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { } @Override - public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext ctx) { + public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); @@ -75,7 +75,35 @@ public QueryTokenStream visitSelect_statement(JpqlParser.Select_statementContext if (ctx.set_fuction() != null) { builder.appendExpression(visit(ctx.set_fuction())); } else { - doVisitOrderBy(builder, ctx); + doVisitOrderBy(builder, ctx.orderby_clause()); + } + + return builder; + } + + @Override + public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.from_clause())); + + if (ctx.where_clause() != null) { + builder.appendExpression(visit(ctx.where_clause())); + } + + if (ctx.groupby_clause() != null) { + builder.appendExpression(visit(ctx.groupby_clause())); + } + + if (ctx.having_clause() != null) { + builder.appendExpression(visit(ctx.having_clause())); + } + + if (ctx.set_fuction() != null) { + builder.appendExpression(visit(ctx.set_fuction())); + } else { + doVisitOrderBy(builder, ctx.orderby_clause()); } return builder; @@ -137,10 +165,10 @@ public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { return tokens; } - private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { + private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Orderby_clauseContext ctx) { - if (ctx.orderby_clause() != null) { - QueryTokenStream existingOrder = visit(ctx.orderby_clause()); + if (ctx != null) { + QueryTokenStream existingOrder = visit(ctx); if (sort.isSorted()) { builder.appendInline(existingOrder); } else { @@ -152,7 +180,7 @@ private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_stat List sortBy = transformerSupport.orderBy(primaryFromAlias, sort); - if (ctx.orderby_clause() != null) { + if (ctx != null) { QueryRendererBuilder extension = QueryRenderer.builder().append(TOKEN_COMMA).append(sortBy); 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 17188c06fb..215aed4d6e 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 @@ -1170,4 +1170,27 @@ void reservedWordsShouldWork() { assertQuery("select f from FooEntity f where f.size IN :sizes"); } + @Test // GH-3902 + void queryWithoutSelectShouldWork() { + + assertQuery("from Person p"); + assertQuery("from Person p WHERE p.name = 'John' ORDER BY p.name"); + } + + @Test // GH-3902 + void queryWithoutSelectAndIdentificationVariableShouldWork() { + + assertQuery("from Person"); + assertQuery("from Person WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN department WHERE name = 'John' ORDER BY name"); + } + + @Test // GH-3902 + void queryWithoutIdentificationVariableShouldWork() { + + assertQuery("SELECT name, lastname from Person"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person JOIN department"); + } + } 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 8f93859699..520039d70b 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 @@ -37,6 +37,7 @@ * {@link JpaQueryEnhancer.EqlQueryParser}. * * @author Greg Turnquist + * @author Mark Paluch */ class EqlQueryTransformerTests { @@ -82,13 +83,11 @@ void nullFirstLastSorting() { assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); } @Test @@ -104,6 +103,32 @@ void applyCountToSimpleQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void applyCountToFromQuery() { + + // given + var original = "FROM Employee e where e.name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(e) FROM Employee e where e.name = :name"); + } + + @Test // GH-3902 + void applyCountToFromQueryWithoutIdentificationVariable() { + + // given + var original = "FROM Employee where name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(__) FROM Employee AS __ where name = :name"); + } + @Test void applyCountToMoreComplexQuery() { @@ -117,6 +142,12 @@ void applyCountToMoreComplexQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void usesPrimaryAliasOfMultiselectForCountQuery() { + assertCountQuery("SELECT e.foo, e.bar FROM Employee e where e.name = :name ORDER BY e.modified_date", + "SELECT count(e) FROM Employee e where e.name = :name"); + } + @Test void applyCountToAlreadySortedQuery() { @@ -143,8 +174,14 @@ void multipleAliasesShouldBeGathered() { assertThat(results).isEqualTo("select e from Employee e join e.manager m"); } - @Test + @Test // GH-3902 void createsCountQueryCorrectly() { + + assertCountQuery("SELECT id FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT p.id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id, name FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT id, name FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery(QUERY, COUNT_QUERY); } @@ -183,6 +220,14 @@ void createsCountQueryForQueriesWithSubSelects() { "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); } + @Test // GH-3902 + void createsCountQueryForQueriesWithoutVariableWithSubSelectsSelectQuery() { + + assertCountQuery( + "select name, (select foo from bar b) from User left outer join u.roles r where r in (select r from Role r)", + "select count(name) from User left outer join u.roles r where r in (select r from Role r)"); + } + @Test void createsCountQueryForAliasesCorrectly() { assertCountQuery("select u from User as u", "select count(u) from User as u"); @@ -193,7 +238,7 @@ void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); } - @Test // GH-2260 + @Test // GH-2260, GH-3902 void detectsAliasCorrectly() { assertThat(alias(QUERY)).isEqualTo("u"); @@ -210,6 +255,11 @@ void detectsAliasCorrectly() { assertThat(alias( "select u from User u where not exists (select u2 from User u2 where not exists (select u3 from User u3))")) .isEqualTo("u"); + assertThat(alias("select u, (select u2 from User u2) from User u")).isEqualTo("u"); + assertThat(alias("select firstname from User where not exists (select u2 from User u2)")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User b")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User UNION select lastname from User b")) + .isNull(); } @Test // GH-2557 @@ -226,12 +276,12 @@ where exists (select u2 """).rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) .isEqualToIgnoringWhitespace(""" - select u - from user u - where exists (select u2 - from user u2 - ) - order by u.age desc"""); + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); } @Test // GH-2563 @@ -643,20 +693,6 @@ void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() { "SELECT count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN entity1.entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799"); } - @Test // GH-3269 - void createsCountQueryUsingAliasCorrectly() { - - assertCountQuery("select distinct 1 as x from Employee e", "select count(distinct 1) from Employee e"); - assertCountQuery("SELECT DISTINCT abc AS x FROM T t", "SELECT count(DISTINCT abc) FROM T t"); - assertCountQuery("select distinct a as x, b as y from Employee e", "select count(distinct a, b) from Employee e"); - assertCountQuery("select distinct sum(amount) as x from Employee e GROUP BY n", - "select count(distinct sum(amount)) from Employee e GROUP BY n"); - assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee e GROUP BY n", - "select count(distinct a, b, sum(amount), d) from Employee e GROUP BY n"); - assertCountQuery("select distinct a, count(b) as c from Employee e GROUP BY n", - "select count(distinct a, count(b)) from Employee e GROUP BY n"); - } - @Test // GH-2393 void createCountQueryStartsWithWhitespace() { @@ -698,6 +734,36 @@ void countQueryUsesCorrectVariable() { .isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"); } + @Test // GH-3269 + void createsCountQueryUsingAliasCorrectly() { + + assertCountQuery("select distinct 1 as x from Employee e", "select count(distinct 1) from Employee e"); + assertCountQuery("SELECT DISTINCT abc AS x FROM T t", "SELECT count(DISTINCT abc) FROM T t"); + assertCountQuery("select distinct a as x, b as y from Employee e", "select count(distinct a, b) from Employee e"); + assertCountQuery("select distinct sum(amount) as x from Employee e GROUP BY n", + "select count(distinct sum(amount)) from Employee e GROUP BY n"); + assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee e GROUP BY n", + "select count(distinct a, b, sum(amount), d) from Employee e GROUP BY n"); + assertCountQuery("select distinct a, count(b) as c from Employee e GROUP BY n", + "select count(distinct a, count(b)) from Employee e GROUP BY n"); + } + + @Test // GH-3902 + void createsCountQueryWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + + @Test // GH-3902 + void createsCountQueryFromMultiselectWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity, that.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + @Test // GH-2496, GH-2522, GH-2537, GH-2045 void orderByShouldWorkWithSubSelectStatements() { 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 040c632dfb..0ed7f1ed75 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 @@ -2621,7 +2621,36 @@ void joinTwoFunctions() { from some_function(:date, :integerValue) d inner join some_function_single_param(:date) k on (d.idFunction = k.idFunctionSP) """); + } + + @Test // GH-3902 + void queryWithoutSelectShouldWork() { + + assertQuery("from Person p"); + assertQuery("from Person p WHERE p.name = 'John' ORDER BY p.name"); + } + + @Test // GH-3902 + void queryWithoutSelectAndIdentificationVariableShouldWork() { + + assertQuery("from Person"); + assertQuery("from Person WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN department WHERE name = 'John' ORDER BY name"); + assertQuery( + "from Person JOIN (select phone.number as n, phone.person as pp from Phone phone) WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN (select number, person from Phone) WHERE name = 'John' ORDER BY name"); + } + @Test // GH-3902 + void queryWithoutIdentificationVariableShouldWork() { + + assertQuery("SELECT name, lastname from Person"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person JOIN department"); + assertQuery( + "SELECT name, lastname from Person JOIN (select phone.number as n, phone.person as pp from Phone phone) WHERE name = 'John' ORDER BY name"); + assertQuery( + "SELECT name, lastname from Person JOIN (select number, person from Phone) WHERE name = 'John' ORDER BY name"); } } 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 260a788d64..d1c5adfa48 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 @@ -109,6 +109,19 @@ void applyCountToSimpleQuery() { assertThat(results).isEqualTo("select count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void applyCountToFromQueryWithoutIdentificationVariable() { + + // given + var original = "FROM Employee where name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(__) FROM Employee AS __ where name = :name"); + } + @Test // GH-3536 void shouldCreateCountQueryForDistinctCount() { @@ -139,6 +152,12 @@ void applyCountToMoreComplexQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void usesAsteriskAliasOfMultiselectForCountQuery() { + assertCountQuery("SELECT e.foo, e.bar FROM Employee e where e.name = :name ORDER BY e.modified_date", + "SELECT count(e) FROM Employee e where e.name = :name"); + } + @Test void applyCountToAlreadySortedQuery() { @@ -186,9 +205,9 @@ void multipleAliasesShouldBeGathered() { @Test void createsCountQueryCorrectly() { - assertCountQuery("SELECT id FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT id FROM Person", "SELECT count(*) FROM Person"); assertCountQuery("SELECT p.id FROM Person p", "SELECT count(p) FROM Person p"); - assertCountQuery("SELECT id FROM Person p", "SELECT count(id) FROM Person p"); + assertCountQuery("SELECT id FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery("SELECT id, name FROM Person", "SELECT count(*) FROM Person"); assertCountQuery("SELECT id, name FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery(QUERY, COUNT_QUERY); @@ -232,7 +251,15 @@ void createsCountQueryForQueriesWithSubSelectsSelectQuery() { "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); } - @Test + @Test // GH-3902 + void createsCountQueryForQueriesWithoutVariableWithSubSelectsSelectQuery() { + + assertCountQuery( + "select u, (select foo from bar b) from User left outer join u.roles r where r in (select r from Role r)", + "select count(*) from User left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3902 void createsCountQueryForQueriesWithSubSelects() { assertCountQuery("from User u left outer join u.roles r where r in (select r from Role r) select u ", @@ -249,7 +276,7 @@ void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); } - @Test // GH-2260 + @Test // GH-2260, GH-3902 void detectsAliasCorrectly() { assertThat(alias(QUERY)).isEqualTo("u"); @@ -269,6 +296,11 @@ void detectsAliasCorrectly() { assertThat(alias( "SELECT e FROM DbEvent e WHERE TREAT(modifiedFrom AS date) IS NULL OR e.modificationDate >= :modifiedFrom")) .isEqualTo("e"); + assertThat(alias("select u, (select u2 from User u2) from User u")).isEqualTo("u"); + assertThat(alias("select firstname from User JOIN (select u2 from User u2) u2")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User b")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User UNION select lastname from User b")) + .isNull(); } @Test // GH-2557 @@ -285,12 +317,12 @@ where exists (select u2 """).rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) .isEqualToIgnoringWhitespace(""" - select u - from user u - where exists (select u2 - from user u2 - ) - order by u.age desc"""); + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); } @Test // GH-2563 @@ -834,6 +866,38 @@ void countQueryUsesCorrectVariable() { .isEqualTo("SELECT count(us) FROM users_statuses us WHERE (user_created_at BETWEEN :fromDate AND :toDate)"); } + @Test // GH-3269, GH-3689 + void createsCountQueryUsingAliasCorrectly() { + + assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee"); + assertCountQuery("SELECT DISTINCT abc AS x FROM T", "SELECT count(DISTINCT abc) FROM T"); + assertCountQuery("select distinct a as x, b as y from Employee", "select count(distinct a, b) from Employee"); + assertCountQuery("select distinct sum(amount) as x from Employee GROUP BY n", + "select count(distinct sum(amount)) from Employee GROUP BY n"); + assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee GROUP BY n", + "select count(distinct a, b, sum(amount), d) from Employee GROUP BY n"); + assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n", + "select count(distinct a, count(b)) from Employee GROUP BY n"); + assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", + "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); + } + + @Test // GH-3902 + void createsCountQueryWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(*) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + + @Test // GH-3902 + void createsCountQueryFromMultiselectWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity, that.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(*) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + @Test // GH-2496, GH-2522, GH-2537, GH-2045 void orderByShouldWorkWithSubSelectStatements() { @@ -1118,22 +1182,6 @@ void aliasesShouldNotOverlapWithSortProperties() { "SELECT t3 FROM Test3 t3 JOIN t3.test2 x WHERE x.id = :test2Id order by t3.testDuplicateColumnName desc"); } - @Test // GH-3269, GH-3689 - void createsCountQueryUsingAliasCorrectly() { - - assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee"); - assertCountQuery("SELECT DISTINCT abc AS x FROM T", "SELECT count(DISTINCT abc) FROM T"); - assertCountQuery("select distinct a as x, b as y from Employee", "select count(distinct a, b) from Employee"); - assertCountQuery("select distinct sum(amount) as x from Employee GROUP BY n", - "select count(distinct sum(amount)) from Employee GROUP BY n"); - assertCountQuery("select distinct a, b, sum(amount) as c, d from Employee GROUP BY n", - "select count(distinct a, b, sum(amount), d) from Employee GROUP BY n"); - assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n", - "select count(distinct a, count(b)) from Employee GROUP BY n"); - assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", - "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); - } - @Test // GH-3864 void testCountFromFunctionWithAlias() { @@ -1148,7 +1196,7 @@ void testCountFromFunctionWithAlias() { } @Test // GH-3864 - void testCountFromFunctionNoAlias() { + void testCountFromMultiselectFunctionNoAlias() { // given var original = "select id, value from some_function(:date, :integerValue)"; 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 3d9b1bf1b2..6e3359bebf 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 @@ -44,7 +44,7 @@ class JpqlQueryRendererTests { 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}. + * Parse the query using {@link JpqlParser} then run it through the query-preserving {@link JpqlQueryRenderer}. */ private static String parseWithoutChanges(String query) { @@ -1279,13 +1279,6 @@ void typeShouldBeAValidParameter() { assertQuery("select te from TestEntity te where te.type = :type"); } - @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-3061 void alternateNotEqualsOperatorShouldWork() { assertQuery("select e from Employee e where e.firstName != :name"); @@ -1410,6 +1403,13 @@ void entityNameWithPackageContainingReservedWord(String reservedWord) { assertQuery(source); } + @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-3834 void reservedWordsShouldWork() { @@ -1417,6 +1417,32 @@ void reservedWordsShouldWork() { assertQuery("select ie.object from ItemExample ie left join ie.object io where io.externalId = :externalId"); assertQuery("select ie from ItemExample ie left join ie.object io where io.object = :externalId"); assertQuery("select ie from ItemExample ie where ie.status = com.app.domain.object.Status.UP"); + assertQuery("select f from FooEntity f where upper(f.name) IN :names"); + assertQuery("select f from FooEntity f where f.size IN :sizes"); + } + + @Test // GH-3902 + void queryWithoutSelectShouldWork() { + + assertQuery("from Person p"); + assertQuery("from Person p WHERE p.name = 'John' ORDER BY p.name"); + } + + @Test // GH-3902 + void queryWithoutSelectAndIdentificationVariableShouldWork() { + + assertQuery("from Person"); + assertQuery("from Person WHERE name = 'John' ORDER BY name"); + assertQuery("from Person JOIN department WHERE name = 'John' ORDER BY name"); + } + + @Test // GH-3902 + void queryWithoutIdentificationVariableShouldWork() { + + assertQuery("SELECT name, lastname from Person"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); + assertQuery("SELECT name, lastname from Person JOIN department"); } } 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 39ed9b6d9d..69b8514ed3 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 @@ -83,13 +83,11 @@ void nullFirstLastSorting() { assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS LAST"); - assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))) - .startsWith(original) - .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); + assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))).startsWith(original) + .endsWithIgnoringCase("e.lastName DESC NULLS FIRST"); } @Test @@ -105,6 +103,32 @@ void applyCountToSimpleQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void applyCountToFromQuery() { + + // given + var original = "FROM Employee e where e.name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(e) FROM Employee e where e.name = :name"); + } + + @Test // GH-3902 + void applyCountToFromQueryWithoutIdentificationVariable() { + + // given + var original = "FROM Employee where name = :name"; + + // when + var results = createCountQueryFor(original); + + // then + assertThat(results).isEqualTo("select count(__) FROM Employee AS __ where name = :name"); + } + @Test void applyCountToMoreComplexQuery() { @@ -118,6 +142,12 @@ void applyCountToMoreComplexQuery() { assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name"); } + @Test // GH-3902 + void usesPrimaryAliasOfMultiselectForCountQuery() { + assertCountQuery("SELECT e.foo, e.bar FROM Employee e where e.name = :name ORDER BY e.modified_date", + "SELECT count(e) FROM Employee e where e.name = :name"); + } + @Test void applyCountToAlreadySortedQuery() { @@ -144,8 +174,14 @@ void multipleAliasesShouldBeGathered() { assertThat(results).isEqualTo("select e from Employee e join e.manager m"); } - @Test + @Test // GH-3902 void createsCountQueryCorrectly() { + + assertCountQuery("SELECT id FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT p.id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id FROM Person p", "SELECT count(p) FROM Person p"); + assertCountQuery("SELECT id, name FROM Person", "SELECT count(id) FROM Person"); + assertCountQuery("SELECT id, name FROM Person p", "SELECT count(p) FROM Person p"); assertCountQuery(QUERY, COUNT_QUERY); } @@ -184,6 +220,14 @@ void createsCountQueryForQueriesWithSubSelects() { "select count(u) from User u left outer join u.roles r where r in (select r from Role r)"); } + @Test // GH-3902 + void createsCountQueryForQueriesWithoutVariableWithSubSelectsSelectQuery() { + + assertCountQuery( + "select name, (select foo from bar b) from User left outer join u.roles r where r in (select r from Role r)", + "select count(name) from User left outer join u.roles r where r in (select r from Role r)"); + } + @Test void createsCountQueryForAliasesCorrectly() { assertCountQuery("select u from User as u", "select count(u) from User as u"); @@ -194,7 +238,7 @@ void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY); } - @Test // GH-2260 + @Test // GH-2260, GH-3902 void detectsAliasCorrectly() { assertThat(alias(QUERY)).isEqualTo("u"); @@ -211,6 +255,11 @@ void detectsAliasCorrectly() { assertThat(alias( "select u from User u where not exists (select u2 from User u2 where not exists (select u3 from User u3))")) .isEqualTo("u"); + assertThat(alias("select u, (select u2 from User u2) from User u")).isEqualTo("u"); + assertThat(alias("select firstname from User where not exists (select u2 from User u2)")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User b")).isNull(); + assertThat(alias("select firstname from User UNION select lastname from User UNION select lastname from User b")) + .isNull(); } @Test // GH-2557 @@ -218,7 +267,6 @@ void applySortingAccountsForNewlinesInSubselect() { Sort sort = Sort.by(Sort.Order.desc("age")); - assertThat(newParser(""" select u from user u @@ -228,12 +276,12 @@ where exists (select u2 """).rewrite(new DefaultQueryRewriteInformation(sort, ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory())))) .isEqualToIgnoringWhitespace(""" - select u - from user u - where exists (select u2 - from user u2 - ) - order by u.age desc"""); + select u + from user u + where exists (select u2 + from user u2 + ) + order by u.age desc"""); } @Test // GH-2563 @@ -490,9 +538,6 @@ void detectsAliasWithGroupAndOrderBy() { @Test // DATAJPA-1500 void createCountQuerySupportsWhitespaceCharacters() { - // - // - // assertThat(createCountQueryFor(""" select user from User user where user.age = 18 @@ -568,10 +613,6 @@ void appliesSortCorrectlyForSimpleField() { @Test void createCountQuerySupportsLineBreakRightAfterDistinct() { - // - // - // - // assertThat(createCountQueryFor(""" select distinct @@ -595,7 +636,6 @@ void detectsAliasWithGroupAndOrderByWithLineBreaks() { .isThrownBy(() -> alias("select * from User group\nby name")); assertThatExceptionOfType(BadJpqlGrammarException.class) .isThrownBy(() -> alias("select * from User order\nby name")); - assertThat(alias("select u from User u group\nby name")).isEqualTo("u"); assertThat(alias("select u from User u order\nby name")).isEqualTo("u"); assertThat(alias("select u from User\nu\norder \n by name")).isEqualTo("u"); @@ -706,6 +746,22 @@ void createsCountQueryUsingAliasCorrectly() { "select count(distinct a, count(b)) from Employee e GROUP BY n"); } + @Test // GH-3902 + void createsCountQueryWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + + @Test // GH-3902 + void createsCountQueryFromMultiselectWithoutAlias() { + + assertCountQuery( + "SELECT this.quantity, that.quantity FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'", + "SELECT count(this.quantity) FROM Order WHERE this.customer.firstname = 'John' AND this.customer.lastname = 'Wick'"); + } + @Test // GH-2496, GH-2522, GH-2537, GH-2045 void orderByShouldWorkWithSubSelectStatements() { @@ -795,7 +851,8 @@ 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"); + 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() { From fa30902cec6efab0aaaaf3ec3cf0ab1cfcf40083 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 28 May 2025 11:07:25 +0200 Subject: [PATCH 132/224] Polishing. Align test methods between EQL and JPQL. Refine identification variable syntax grouping. Align and simplify EQL and JPQL renderers. See #3902 Original pull request: #3903 --- .../data/jpa/repository/query/Eql.g4 | 12 +- .../data/jpa/repository/query/Jpql.g4 | 12 +- .../repository/query/EqlQueryRenderer.java | 82 +++--- .../repository/query/JpqlQueryRenderer.java | 81 ++++-- .../query/EqlQueryRendererTests.java | 275 ++++++++++++++++++ .../query/JpqlQueryRendererTests.java | 41 ++- 6 files changed, 419 insertions(+), 84 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 71d9c05ab6..73b118ebed 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 @@ -72,7 +72,7 @@ from_clause identificationVariableDeclarationOrCollectionMemberDeclaration : identification_variable_declaration | collection_member_declaration - | '(' subquery ')' identification_variable + | '(' subquery ')' (AS? identification_variable)? ; identification_variable_declaration @@ -80,15 +80,15 @@ identification_variable_declaration ; range_variable_declaration - : (entity_name|function_invocation) AS? identification_variable? + : (entity_name|function_invocation) (AS? identification_variable)? ; join - : join_spec join_association_path_expression AS? identification_variable? join_condition? + : join_spec join_association_path_expression (AS? identification_variable)? join_condition? ; fetch_join - : join_spec FETCH join_association_path_expression AS? identification_variable? join_condition? + : join_spec FETCH join_association_path_expression (AS? identification_variable)? join_condition? ; join_spec @@ -115,7 +115,7 @@ join_single_valued_path_expression ; collection_member_declaration - : IN '(' collection_valued_path_expression ')' AS? identification_variable + : IN '(' collection_valued_path_expression ')' (AS? identification_variable)? ; qualified_identification_variable @@ -271,7 +271,7 @@ subquery_from_clause subselect_identification_variable_declaration : identification_variable_declaration - | derived_path_expression AS? identification_variable (join)* + | derived_path_expression (AS? identification_variable)? (join)* | derived_collection_member_declaration ; 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 6632690a42..6653f02bcd 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 @@ -73,7 +73,7 @@ from_clause identificationVariableDeclarationOrCollectionMemberDeclaration : identification_variable_declaration | collection_member_declaration - | '(' subquery ')' identification_variable + | '(' subquery ')' (AS? identification_variable)? ; identification_variable_declaration @@ -81,15 +81,15 @@ identification_variable_declaration ; range_variable_declaration - : entity_name AS? identification_variable? + : entity_name (AS? identification_variable)? ; join - : join_spec join_association_path_expression AS? identification_variable? (join_condition)? + : join_spec join_association_path_expression (AS? identification_variable)? (join_condition)? ; fetch_join - : join_spec FETCH join_association_path_expression AS? identification_variable? join_condition? + : join_spec FETCH join_association_path_expression (AS? identification_variable)? join_condition? ; join_spec @@ -116,7 +116,7 @@ join_single_valued_path_expression ; collection_member_declaration - : IN '(' collection_valued_path_expression ')' AS? identification_variable + : IN '(' collection_valued_path_expression ')' (AS? identification_variable)? ; qualified_identification_variable @@ -271,7 +271,7 @@ subquery_from_clause subselect_identification_variable_declaration : identification_variable_declaration - | derived_path_expression AS? identification_variable (join)* + | derived_path_expression (AS? identification_variable)? (join)* | derived_collection_member_declaration ; 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 e382fd9559..57e23dbc2c 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 @@ -213,6 +213,11 @@ public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMember QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(nested); + + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } + if (ctx.identification_variable() != null) { builder.appendExpression(visit(ctx.identification_variable())); } @@ -406,12 +411,15 @@ public QueryTokenStream visitJoin_single_valued_path_expression( @Override public QueryTokenStream visitCollection_member_declaration(EqlParser.Collection_member_declarationContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.IN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.collection_valued_path_expression())); - builder.append(TOKEN_CLOSE_PAREN); + nested.append(QueryTokens.token(ctx.IN())); + nested.append(TOKEN_OPEN_PAREN); + nested.appendInline(visit(ctx.collection_valued_path_expression())); + nested.append(TOKEN_CLOSE_PAREN); + + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.appendExpression(nested); if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); @@ -496,34 +504,36 @@ public QueryTokenStream visitGeneral_identification_variable(EqlParser.General_i QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.identification_variable() != null) { - builder.append(visit(ctx.identification_variable())); + return visit(ctx.identification_variable()); } else if (ctx.map_field_identification_variable() != null) { - builder.append(visit(ctx.map_field_identification_variable())); + return visit(ctx.map_field_identification_variable()); } - return builder; + return QueryTokenStream.empty(); } @Override public QueryTokenStream visitGeneral_subpath(EqlParser.General_subpathContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.simple_subpath() != null) { - builder.appendInline(visit(ctx.simple_subpath())); + return visit(ctx.simple_subpath()); } else if (ctx.treated_subpath() != null) { - builder.appendInline(visit(ctx.treated_subpath())); - builder.appendInline(QueryTokenStream.concat(ctx.single_valued_object_field(), this::visit, TOKEN_DOT)); + List items = new ArrayList<>(1 + ctx.single_valued_object_field().size()); + + items.add(ctx.treated_subpath()); + items.addAll(ctx.single_valued_object_field()); + return QueryTokenStream.concat(items, this::visit, TOKEN_DOT); } - return builder; + return QueryTokenStream.empty(); } @Override public QueryTokenStream visitSimple_subpath(EqlParser.Simple_subpathContext ctx) { List items = new ArrayList<>(1 + ctx.single_valued_object_field().size()); + items.add(ctx.general_identification_variable()); items.addAll(ctx.single_valued_object_field()); @@ -566,12 +576,12 @@ public QueryTokenStream visitState_valued_path_expression(EqlParser.State_valued QueryRendererBuilder builder = QueryRenderer.builder(); 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.general_identification_variable() != null) { - builder.append(visit(ctx.general_identification_variable())); + return visit(ctx.general_identification_variable()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -617,7 +627,7 @@ public QueryTokenStream visitUpdate_clause(EqlParser.Update_clauseContext ctx) { } builder.append(QueryTokens.expression(ctx.SET())); - builder.appendExpression(QueryTokenStream.concat(ctx.update_item(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokenStream.concat(ctx.update_item(), this::visit, TOKEN_COMMA)); return builder; } @@ -628,6 +638,7 @@ public QueryTokenStream visitUpdate_item(EqlParser.Update_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); List items = new ArrayList<>(3 + ctx.single_valued_embeddable_object_field().size()); + if (ctx.identification_variable() != null) { items.add(ctx.identification_variable()); } @@ -657,7 +668,7 @@ public QueryTokenStream visitNew_value(EqlParser.New_valueContext ctx) { } else if (ctx.NULL() != null) { return QueryTokenStream.ofToken(ctx.NULL()); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -748,7 +759,7 @@ public QueryTokenStream visitSelect_expression(EqlParser.Select_expressionContex } else if (ctx.constructor_expression() != null) { return visit(ctx.constructor_expression()); } else { - return QueryRenderer.builder(); + return QueryTokenStream.empty(); } } @@ -860,17 +871,15 @@ public QueryTokenStream visitGroupby_clause(EqlParser.Groupby_clauseContext ctx) @Override public QueryTokenStream visitGroupby_item(EqlParser.Groupby_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.identification_variable() != null) { - builder.append(visit(ctx.identification_variable())); + return visit(ctx.identification_variable()); } else if (ctx.scalar_expression() != null) { - builder.append(visit(ctx.scalar_expression())); + return visit(ctx.scalar_expression()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -1061,19 +1070,17 @@ public QueryTokenStream visitSimple_select_clause(EqlParser.Simple_select_clause @Override public QueryTokenStream visitSimple_select_expression(EqlParser.Simple_select_expressionContext 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()); } - return builder; + return QueryTokenStream.empty(); } @Override @@ -2083,9 +2090,11 @@ public QueryTokenStream visitType_cast_function(EqlParser.Type_cast_functionCont builder.append(QueryTokens.token(ctx.CAST())); builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); + if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } + builder.appendInline(visit(ctx.identification_variable())); if (!CollectionUtils.isEmpty(ctx.numeric_literal())) { @@ -2388,12 +2397,7 @@ public QueryTokenStream visitInput_parameter(EqlParser.Input_parameterContext ct @Override public QueryTokenStream visitPattern_value(EqlParser.Pattern_valueContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.string_expression())); - - return builder; + return visit(ctx.string_expression()); } @Override 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 4b839dea91..06fc23f13c 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 @@ -208,6 +208,25 @@ public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMember return visit(ctx.identification_variable_declaration()); } else if (ctx.collection_member_declaration() != null) { return visit(ctx.collection_member_declaration()); + } else if (ctx.subquery() != null) { + + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.append(TOKEN_OPEN_PAREN); + nested.appendInline(visit(ctx.subquery())); + nested.append(TOKEN_CLOSE_PAREN); + + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.appendExpression(nested); + + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } + + if (ctx.identification_variable() != null) { + builder.appendExpression(visit(ctx.identification_variable())); + } + + return builder; } else { return QueryTokenStream.empty(); } @@ -379,9 +398,11 @@ public QueryTokenStream visitJoin_collection_valued_path_expression( public QueryTokenStream visitJoin_single_valued_path_expression( JpqlParser.Join_single_valued_path_expressionContext ctx) { - List items = new ArrayList<>(3 + ctx.single_valued_embeddable_object_field().size()); + List items = new ArrayList<>(2 + ctx.single_valued_embeddable_object_field().size()); + if (ctx.identification_variable() != null) { + items.add(ctx.identification_variable()); + } - items.add(ctx.identification_variable()); items.addAll(ctx.single_valued_embeddable_object_field()); items.add(ctx.single_valued_object_field()); @@ -391,12 +412,15 @@ public QueryTokenStream visitJoin_single_valued_path_expression( @Override public QueryTokenStream visitCollection_member_declaration(JpqlParser.Collection_member_declarationContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.IN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.collection_valued_path_expression())); - builder.append(TOKEN_CLOSE_PAREN); + nested.append(QueryTokens.token(ctx.IN())); + nested.append(TOKEN_OPEN_PAREN); + nested.appendInline(visit(ctx.collection_valued_path_expression())); + nested.append(TOKEN_CLOSE_PAREN); + + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.appendExpression(nested); if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); @@ -421,7 +445,7 @@ public QueryTokenStream visitQualified_identification_variable( builder.append(QueryTokens.expression(ctx.ENTRY())); builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.identification_variable())); + builder.append(visit(ctx.identification_variable())); builder.append(TOKEN_CLOSE_PAREN); } @@ -511,6 +535,7 @@ public QueryTokenStream visitSimple_subpath(JpqlParser.Simple_subpathContext ctx items.add(ctx.general_identification_variable()); items.addAll(ctx.single_valued_object_field()); + return QueryTokenStream.concat(items, this::visit, TOKEN_DOT); } @@ -609,7 +634,7 @@ public QueryTokenStream visitUpdate_item(JpqlParser.Update_itemContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - List items = new ArrayList<>(2 + ctx.single_valued_embeddable_object_field().size()); + List items = new ArrayList<>(3 + ctx.single_valued_embeddable_object_field().size()); if (ctx.identification_variable() != null) { items.add(ctx.identification_variable()); @@ -625,7 +650,7 @@ public QueryTokenStream visitUpdate_item(JpqlParser.Update_itemContext ctx) { builder.appendInline(QueryTokenStream.concat(items, this::visit, TOKEN_DOT)); builder.append(TOKEN_EQUALS); - builder.appendInline(visit(ctx.new_value())); + builder.append(visit(ctx.new_value())); return builder; } @@ -693,15 +718,12 @@ public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { builder.appendExpression(visit(ctx.select_expression())); - if (ctx.AS() != null || ctx.result_variable() != null) { - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } - if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } + if (ctx.result_variable() != null) { + builder.appendExpression(visit(ctx.result_variable())); } return builder; @@ -1572,18 +1594,15 @@ public QueryTokenStream visitArithmetic_expression(JpqlParser.Arithmetic_express @Override public QueryTokenStream visitArithmetic_term(JpqlParser.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 @@ -1594,7 +1613,8 @@ public QueryTokenStream visitArithmetic_factor(JpqlParser.Arithmetic_factorConte if (ctx.op != null) { builder.append(QueryTokens.token(ctx.op)); } - builder.appendInline(visit(ctx.arithmetic_primary())); + + builder.append(visit(ctx.arithmetic_primary())); return builder; } @@ -2089,7 +2109,9 @@ public QueryTokenStream visitString_cast_function(JpqlParser.String_cast_functio builder.append(QueryTokens.token(ctx.CAST())); builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); - builder.append(QueryTokens.expression(ctx.AS())); + if (ctx.AS() != null) { + builder.append(QueryTokens.expression(ctx.AS())); + } builder.append(QueryTokens.token(ctx.STRING())); builder.append(TOKEN_CLOSE_PAREN); @@ -2310,7 +2332,7 @@ public QueryTokenStream visitIdentification_variable(JpqlParser.Identification_v } else if (ctx.type_literal() != null) { return visit(ctx.type_literal()); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.expression(ctx.f)); + return QueryTokenStream.ofToken(ctx.f); } else { return QueryTokenStream.empty(); } @@ -2457,9 +2479,11 @@ public QueryTokenStream visitSubtype(JpqlParser.SubtypeContext ctx) { @Override public QueryTokenStream visitCollection_valued_field(JpqlParser.Collection_valued_fieldContext ctx) { + if (ctx.reserved_word() != null) { return visit(ctx.reserved_word()); } + return visit(ctx.identification_variable()); } @@ -2469,6 +2493,7 @@ public QueryTokenStream visitSingle_valued_object_field(JpqlParser.Single_valued if (ctx.reserved_word() != null) { return visit(ctx.reserved_word()); } + return visit(ctx.identification_variable()); } @@ -2478,6 +2503,7 @@ public QueryTokenStream visitState_field(JpqlParser.State_fieldContext ctx) { if (ctx.reserved_word() != null) { return visit(ctx.reserved_word()); } + return visit(ctx.identification_variable()); } @@ -2487,6 +2513,7 @@ public QueryTokenStream visitCollection_value_field(JpqlParser.Collection_value_ if (ctx.reserved_word() != null) { return visit(ctx.reserved_word()); } + return visit(ctx.identification_variable()); } 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 215aed4d6e..a98a01adc7 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 @@ -69,6 +69,281 @@ 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"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address ORDER BY city"); + } + + @Test + void leftJoin() { + assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); + } + + @Test // GH-3902 + void fromCollection() { + + assertQuery("SELECT e FROM Employee e, IN(e.projects) AS p"); + assertQuery("SELECT e FROM Employee e, IN(e.projects) p"); + assertQuery("SELECT e FROM Employee e, IN(e.projects)"); + + assertQuery("FROM Employee e, IN(e.projects)"); + } + + @Test // GH-3902 + void fromSubquery() { + + assertQuery("SELECT e FROM Employee e, (SELECT p FROM Project p) AS sub"); + assertQuery("SELECT e FROM Employee e, (SELECT p FROM Project p) sub"); + assertQuery("SELECT e FROM Employee e, (SELECT p FROM Project p)"); + assertQuery("FROM Employee e, (SELECT p FROM Project p) sub"); + } + + @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 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 */ 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 6e3359bebf..0c35ed80bf 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 @@ -109,6 +109,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"); + assertQuery("SELECT e FROM Employee e JOIN FETCH e.address ORDER BY city"); } @Test @@ -116,6 +117,25 @@ void leftJoin() { assertQuery("SELECT e FROM Employee e LEFT JOIN e.address a ORDER BY a.city"); } + @Test // GH-3902 + void fromCollection() { + + assertQuery("SELECT e FROM Employee e, IN(e.projects) AS p"); + assertQuery("SELECT e FROM Employee e, IN(e.projects) p"); + assertQuery("SELECT e FROM Employee e, IN(e.projects)"); + + assertQuery("FROM Employee e, IN(e.projects)"); + } + + @Test // GH-3902 + void fromSubquery() { + + assertQuery("SELECT e FROM Employee e, (SELECT p FROM Project p) AS sub"); + assertQuery("SELECT e FROM Employee e, (SELECT p FROM Project p) sub"); + assertQuery("SELECT e FROM Employee e, (SELECT p FROM Project p)"); + assertQuery("FROM Employee e, (SELECT p FROM Project p) sub"); + } + @Test // GH-3277 void numericLiterals() { @@ -169,11 +189,6 @@ UNION SELECT MAX(e.salary) FROM Employee e WHERE e.address.city = :city2 """); } - @Test - void whereClause() { - // TBD - } - @Test void updateQueries() { assertQuery("UPDATE Employee e SET e.salary = 60000 WHERE e.salary = 50000"); @@ -1284,11 +1299,26 @@ void alternateNotEqualsOperatorShouldWork() { assertQuery("select e from Employee e where e.firstName != :name"); } + @Test + void regexShouldWork() { + assertQuery("select e from Employee e where e.lastName REGEXP '^Dr\\.*'"); + } + @Test // GH-3092 void dateAndFromShouldBeValidNames() { assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date BETWEEN :from AND :to"); } + @Test + void betweenStrings() { + assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date NOT BETWEEN 'a' AND 'b'"); + } + + @Test + void betweenDates() { + assertQuery("SELECT e FROM Entity e WHERE e.embeddedId.date BETWEEN CURRENT_DATE AND CURRENT_TIME"); + } + @Test // GH-3092 void timeShouldBeAValidParameterName() { assertQuery(""" @@ -1441,7 +1471,6 @@ void queryWithoutIdentificationVariableShouldWork() { assertQuery("SELECT name, lastname from Person"); assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); - assertQuery("SELECT name, lastname from Person WHERE lastname = 'Doe' ORDER BY name, lastname"); assertQuery("SELECT name, lastname from Person JOIN department"); } From 78d350bde0193c3dccf72c5de06a9a185e2ad026 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 7 Jul 2025 11:08:29 +0200 Subject: [PATCH 133/224] Exclude DTO types without custom construction from DTO constructor rewriting. We now verify that we can actually express a valid constructor expression before rewriting queries to use constructor expressions. See #3929 --- .../query/AbstractStringBasedJpaQuery.java | 5 +- .../data/jpa/domain/sample/Country.java | 41 ++++++++++++ .../jpa/domain/sample/CountryConverter.java | 33 ++++++++++ .../data/jpa/domain/sample/Customer.java | 32 ++++++++- .../CustomerRepositoryProjectionTests.java | 66 +++++++++++++++++++ .../RepositoryWithCompositeKeyTests.java | 21 +++++- .../query/SimpleJpaQueryUnitTests.java | 15 +++++ .../repository/sample/CustomerRepository.java | 32 +++++++++ .../EmployeeRepositoryWithEmbeddedId.java | 4 ++ .../test/resources/META-INF/persistence.xml | 1 + .../test/resources/META-INF/persistence2.xml | 2 + 11 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/CountryConverter.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomerRepositoryProjectionTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/CustomerRepository.java 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 95965b2693..841eaffffd 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 @@ -157,7 +157,7 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { ReturnedType getReturnedType(ResultProcessor processor) { ReturnedType returnedType = processor.getReturnedType(); - Class returnedJavaType = processor.getReturnedType().getReturnedType(); + Class returnedJavaType = returnedType.getReturnedType(); if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNative()) { return returnedType; @@ -169,7 +169,8 @@ ReturnedType getReturnedType(ResultProcessor processor) { return returnedType; } - if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) { + if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType) + || !returnedType.needsCustomConstruction()) { if (known == null) { knownProjections.put(returnedJavaType, false); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java new file mode 100644 index 0000000000..e02b800bd3 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java @@ -0,0 +1,41 @@ +/* + * 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.domain.sample; + +/** + * @author Mark Paluch + */ +public class Country { + + private final String code; + + // workaround to avoid DTO projections as needsCustomConstruction is false. + private Country(Country other) { + this.code = other.code; + } + + private Country(String code) { + this.code = code; + } + + public static Country of(String code) { + return new Country(code); + } + + public String getCode() { + return code; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/CountryConverter.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/CountryConverter.java new file mode 100644 index 0000000000..5b9b55a7a9 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/CountryConverter.java @@ -0,0 +1,33 @@ +/* + * 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.domain.sample; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class CountryConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Country attribute) { + return attribute.getCode(); + } + + @Override + public Country convertToEntityAttribute(String dbData) { + return Country.of(dbData); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Customer.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Customer.java index f8442a9ae4..875d724c13 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Customer.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Customer.java @@ -15,17 +15,45 @@ */ package org.springframework.data.jpa.domain.sample; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.Id; /** * @author Oliver Gierke * @author Patrice Blanchardie + * @author Mark Paluch */ @Entity public class Customer { - @Id Long id; + @Id Long id; - String name; + String name; + + @Convert(converter = CountryConverter.class) Country country; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Country getCountry() { + return country; + } + + public void setCountry(Country country) { + this.country = country; + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomerRepositoryProjectionTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomerRepositoryProjectionTests.java new file mode 100644 index 0000000000..73d6a6bb54 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CustomerRepositoryProjectionTests.java @@ -0,0 +1,66 @@ +/* + * 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.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +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.Country; +import org.springframework.data.jpa.domain.sample.Customer; +import org.springframework.data.jpa.repository.sample.CustomerRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration test for executing projecting query methods. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(locations = "classpath:config/namespace-application-context-h2.xml") +@Transactional +class CustomerRepositoryProjectionTests { + + @Autowired CustomerRepository repository; + + @AfterEach + void clearUp() { + repository.deleteAll(); + } + + @Test + void returnsCountries() { + + Customer customer = new Customer(); + customer.setId(42L); + customer.setCountry(Country.of("DE")); + customer.setName("someone"); + + repository.saveAndFlush(customer); + + List countries = repository.findCountries(); + + assertThat(countries).hasSize(1); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java index 20613cc1d6..99665ecbfd 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.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; @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -115,6 +116,24 @@ void shouldSupportSavingEntitiesWithCompositeKeyClassesWithEmbeddedIdsAndDerived assertThat(persistedEmp.getDepartment().getName()).isEqualTo(dep.getName()); } + @Test // GH-3929 + void shouldReturnIdentifiers() { + + EmbeddedIdExampleDepartment dep = new EmbeddedIdExampleDepartment(); + dep.setName("TestDepartment"); + dep.setDepartmentId(-1L); + + EmbeddedIdExampleEmployee emp = new EmbeddedIdExampleEmployee(); + emp.setDepartment(dep); + emp.setEmployeePk(new EmbeddedIdExampleEmployeePK(1L, 2L)); + + emp = employeeRepositoryWithEmbeddedId.save(emp); + + List identifiers = employeeRepositoryWithEmbeddedId.findIdentifiers(); + + assertThat(identifiers).hasSize(1).contains(emp.getEmployeePk()); + } + @Test // DATAJPA-472, DATAJPA-912 void shouldSupportFindAllWithPageableAndEntityWithIdClass() { 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 47949f1a6d..a87cb3d4e3 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 @@ -46,6 +46,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.Country; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.NativeQuery; @@ -325,6 +326,17 @@ void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception { assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(illegalMethod)); } + @Test // GH-3929 + void doesNotRewriteQueryForDtoWithMultipleConstructors() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("justCountries")); + + String queryString = createQuery(jpaQuery); + + assertThat(queryString).startsWith("select u.country from User u"); + } + @Test // DATAJPA-1163 void resolvesExpressionInCountQuery() throws Exception { @@ -408,6 +420,9 @@ interface SampleRepository extends Repository { @Query("select r.name from User u LEFT JOIN FETCH u.roles r") Collection projectWithJoinPaths(); + @Query("select u.country from User u") + Collection justCountries(); + @Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u") List findAllWithExpressionInCountQuery(Pageable pageable); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/CustomerRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/CustomerRepository.java new file mode 100644 index 0000000000..7a607fc655 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/CustomerRepository.java @@ -0,0 +1,32 @@ +/* + * 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.sample; + +import java.util.List; + +import org.springframework.data.jpa.domain.sample.Country; +import org.springframework.data.jpa.domain.sample.Customer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +/** + * @author Mark Paluch + */ +public interface CustomerRepository extends JpaRepository { + + @Query("SELECT c.country FROM Customer c") + List findCountries(); +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithEmbeddedId.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithEmbeddedId.java index ea1bc60f56..7e8dce12bf 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithEmbeddedId.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithEmbeddedId.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee; import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import com.querydsl.core.types.OrderSpecifier; @@ -40,6 +41,9 @@ public interface EmployeeRepositoryWithEmbeddedId @Override List findAll(Predicate predicate, OrderSpecifier... orders); + @Query("select e.employeePk from EmbeddedIdExampleEmployee e") + List findIdentifiers(); + // DATAJPA-920 boolean existsByName(String name); } 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 a12c866d21..e45c453b10 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence.xml @@ -23,6 +23,7 @@ org.springframework.data.jpa.domain.sample.ConcreteType2 org.springframework.data.jpa.domain.sample.CustomAbstractPersistable org.springframework.data.jpa.domain.sample.Customer + org.springframework.data.jpa.domain.sample.CountryConverter org.springframework.data.jpa.domain.sample.EntityWithAssignedId org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee 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 a93617de58..9b92f05a73 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence2.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence2.xml @@ -11,6 +11,7 @@ org.springframework.data.jpa.domain.sample.AuditableEmbeddable org.springframework.data.jpa.domain.sample.Book org.springframework.data.jpa.domain.sample.Category + org.springframework.data.jpa.domain.sample.Customer org.springframework.data.jpa.domain.sample.CustomAbstractPersistable org.springframework.data.jpa.domain.sample.EntityWithAssignedId org.springframework.data.jpa.domain.sample.Item @@ -32,6 +33,7 @@ org.springframework.data.jpa.domain.sample.AuditableUser org.springframework.data.jpa.domain.sample.AuditableRole org.springframework.data.jpa.domain.sample.Category + org.springframework.data.jpa.domain.sample.Customer org.springframework.data.jpa.domain.sample.CustomAbstractPersistable org.springframework.data.jpa.domain.sample.EntityWithAssignedId org.springframework.data.jpa.domain.sample.Item From c99c3c2173311e790f4b0ec560db0f57ed012d33 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 14:53:01 +0200 Subject: [PATCH 134/224] 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. Original pull request: #3878 Closes #3877 --- .../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 bebe618fe2..0c2e9de82b 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 @@ -207,13 +207,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); @@ -222,7 +227,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 2d2c46bb8c..161bb33a28 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. * @@ -783,6 +785,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(); From 5aba123e841eeeeecc4c3726183882b6dbcb105c Mon Sep 17 00:00:00 2001 From: Giheon Do Date: Wed, 9 Jul 2025 20:38:16 +0900 Subject: [PATCH 135/224] Replace regex with startsWith / endsWith check for LIKE pattern detection. Signed-off-by: Giheon Do Closes #3932 --- .../data/jpa/repository/query/ParameterBinding.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 443b6ca3ce..f5474cfbd3 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 @@ -370,6 +370,8 @@ static class LikeParameterBinding extends ParameterBinding { private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, Type.ENDING_WITH, Type.LIKE); + private static final String PERCENT = "%"; + private final Type type; /** @@ -464,15 +466,15 @@ static Type getLikeTypeFrom(String expression) { Assert.hasText(expression, "Expression must not be null or empty"); - if (expression.matches("%.*%")) { + if (expression.startsWith(PERCENT) && expression.endsWith(PERCENT)) { return Type.CONTAINING; } - if (expression.startsWith("%")) { + if (expression.startsWith(PERCENT)) { return Type.ENDING_WITH; } - if (expression.endsWith("%")) { + if (expression.endsWith(PERCENT)) { return Type.STARTING_WITH; } From 351dc077487558197d663770c32fecbaee8e2379 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Jul 2025 12:12:12 +0200 Subject: [PATCH 136/224] Polishing. Refine conditional flow. See #3932 --- .../data/jpa/repository/query/ParameterBinding.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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 f5474cfbd3..acc22fedbd 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 @@ -370,8 +370,6 @@ static class LikeParameterBinding extends ParameterBinding { private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, Type.ENDING_WITH, Type.LIKE); - private static final String PERCENT = "%"; - private final Type type; /** @@ -466,15 +464,11 @@ static Type getLikeTypeFrom(String expression) { Assert.hasText(expression, "Expression must not be null or empty"); - if (expression.startsWith(PERCENT) && expression.endsWith(PERCENT)) { - return Type.CONTAINING; - } - - if (expression.startsWith(PERCENT)) { - return Type.ENDING_WITH; + if (expression.startsWith("%")) { + return expression.endsWith("%") ? Type.CONTAINING : Type.ENDING_WITH; } - if (expression.endsWith(PERCENT)) { + if (expression.endsWith("%")) { return Type.STARTING_WITH; } From 0dcc0c2bb1a8c96f906e6790f0618714eed08e29 Mon Sep 17 00:00:00 2001 From: Giheon Do Date: Sun, 15 Jun 2025 16:41:19 +0900 Subject: [PATCH 137/224] Cache query strings in `SimpleJpaRepository`. Cache the deleteAll and count query strings as final fields in SimpleJpaRepository. This avoids repeated String.format operations and reduces unnecessary object creation on every invocation of deleteAllInBatch() and count(). No functional changes. Signed-off-by: Giheon Do Closes #3920 --- .../jpa/repository/support/SimpleJpaRepository.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 0c2e9de82b..83e7aa9cfa 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 @@ -104,6 +104,7 @@ * @author Diego Krupitza * @author Seol-JY * @author Joshua Chen + * @author Dockerel */ @Repository @Transactional(readOnly = true) @@ -121,6 +122,9 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation entityInformation, EntityM this.entityManager = entityManager; this.provider = PersistenceProvider.fromEntityManager(entityManager); this.projectionFactory = new SpelAwareProxyProjectionFactory(); + + this.deleteAllQueryString = getDeleteAllQueryString(); + this.countQueryString = getCountQueryString(); } /** @@ -318,7 +325,7 @@ public void deleteAll() { @Transactional public void deleteAllInBatch() { - Query query = entityManager.createQuery(getDeleteAllQueryString()); + Query query = entityManager.createQuery(deleteAllQueryString); applyQueryHints(query); @@ -639,7 +646,7 @@ public R findBy(Example example, Function query = entityManager.createQuery(getCountQueryString(), Long.class); + TypedQuery query = entityManager.createQuery(countQueryString, Long.class); applyQueryHintsForCount(query); From 9e704e195608a7c060f9bd81c4f00edc1f1b7123 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Jul 2025 12:17:00 +0200 Subject: [PATCH 138/224] Polishing. Inline methods, lazify query creation. See #3920 --- .../support/SimpleJpaRepository.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) 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 83e7aa9cfa..2cbd99c51c 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 @@ -74,6 +74,7 @@ import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.data.util.Lazy; import org.springframework.data.util.ProxyUtils; import org.springframework.data.util.Streamable; import org.springframework.lang.Contract; @@ -104,7 +105,7 @@ * @author Diego Krupitza * @author Seol-JY * @author Joshua Chen - * @author Dockerel + * @author Giheon Do */ @Repository @Transactional(readOnly = true) @@ -122,8 +123,8 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation deleteAllQueryString; + private final Lazy countQueryString; private @Nullable CrudMethodMetadata metadata; private ProjectionFactory projectionFactory; @@ -145,8 +146,11 @@ public SimpleJpaRepository(JpaEntityInformation entityInformation, EntityM this.provider = PersistenceProvider.fromEntityManager(entityManager); this.projectionFactory = new SpelAwareProxyProjectionFactory(); - this.deleteAllQueryString = getDeleteAllQueryString(); - this.countQueryString = getCountQueryString(); + this.deleteAllQueryString = Lazy + .of(() -> getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName())); + this.countQueryString = Lazy + .of(() -> getQueryString(String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s"), + entityInformation.getEntityName())); } /** @@ -188,16 +192,6 @@ protected Class getDomainClass() { return entityInformation.getJavaType(); } - private String getDeleteAllQueryString() { - return getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()); - } - - private String getCountQueryString() { - - String countQuery = String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s"); - return getQueryString(countQuery, entityInformation.getEntityName()); - } - @Override @Transactional public void deleteById(ID id) { @@ -325,7 +319,7 @@ public void deleteAll() { @Transactional public void deleteAllInBatch() { - Query query = entityManager.createQuery(deleteAllQueryString); + Query query = entityManager.createQuery(deleteAllQueryString.get()); applyQueryHints(query); @@ -646,7 +640,7 @@ public R findBy(Example example, Function query = entityManager.createQuery(countQueryString, Long.class); + TypedQuery query = entityManager.createQuery(countQueryString.get(), Long.class); applyQueryHintsForCount(query); From 53c412498565967a336e694934fef74fd9390984 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Jul 2025 13:53:17 +0200 Subject: [PATCH 139/224] Upgrade to Hibernate 7.0.5.Final. Closes #3933 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 51123083cb..17ffd2313d 100755 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 4.13.2 5.0.0-B07 5.0.0-SNAPSHOT - 7.0.3.Final - 7.0.4-SNAPSHOT + 7.0.5.Final + 7.0.6-SNAPSHOT 7.1.0-SNAPSHOT 2.7.4

        2.3.232

        From 42ca37fc6f4c2a6830f269623a1b5dcbf1c5d1ec Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Jul 2025 16:16:11 +0200 Subject: [PATCH 140/224] Add support for HQL JSON functions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support json_object(…), json_array(…), json_objagg(…), json_arrayagg(…), json_query(…), json_value(…), json_exists(…), and json_table(…) functions. See #3883 --- .../data/jpa/repository/query/Hql.g4 | 243 +++++++++++- .../repository/query/HqlQueryRenderer.java | 365 +++++++++++++++++- .../jpa/repository/query/QueryRenderer.java | 3 + .../repository/query/QueryTokenStream.java | 48 +++ .../query/HqlQueryRendererTests.java | 104 +++++ 5 files changed, 742 insertions(+), 21 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 fab6d9a079..7d61f25572 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 @@ -122,9 +122,9 @@ join ; joinTarget - : path variable? # JoinPath - | LATERAL? '(' subquery ')' variable? # JoinSubquery - | setReturningFunction variable? # JoinFunctionCall + : path variable? # JoinPath + | LATERAL? '(' subquery ')' variable? # JoinSubquery + | LATERAL? setReturningFunction variable? # JoinFunctionCall ; // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-update @@ -757,11 +757,15 @@ function | collectionFunctionMisuse # CollectionFunctionMisuseInvocation | jpaNonstandardFunction # JpaNonstandardFunctionInvocation | columnFunction # ColumnFunctionInvocation + | jsonFunction # JsonFunctionInvocation + | xmlFunction # XmlFunctionInvocation | genericFunction # GenericFunctionInvocation ; setReturningFunction : simpleSetReturningFunction + | jsonTableFunction + | xmlTableFunction ; simpleSetReturningFunction @@ -1248,6 +1252,175 @@ frameExclusion | EXCLUDE NO OTHERS ; +// JSON Functions + +jsonFunction + : jsonArrayFunction + | jsonExistsFunction + | jsonObjectFunction + | jsonQueryFunction + | jsonValueFunction + | jsonArrayAggFunction + | jsonObjectAggFunction + ; + +/** + * The 'json_array(… ABSENT ON NULL)' function + */ +jsonArrayFunction + : JSON_ARRAY '(' (expressionOrPredicate (',' expressionOrPredicate)* jsonNullClause?)? ')'; + +/** + * The 'json_exists(, PASSING … AS … WITH WRAPPER ERROR|NULL|DEFAULT on ERROR|EMPTY)' function + */ +jsonExistsFunction + : JSON_EXISTS '(' expression ',' expression jsonPassingClause? jsonExistsOnErrorClause? ')'; + +jsonExistsOnErrorClause + : (ERROR | TRUE | FALSE) ON ERROR + ; + +/** + * The 'json_object( foo, bar, KEY foo VALUE bar, foo:bar ABSENT ON NULL)' function + */ +jsonObjectFunction + : JSON_OBJECT '(' jsonObjectFunctionEntry? (',' jsonObjectFunctionEntry)* jsonNullClause? ')'; + +jsonObjectFunctionEntry + : (expressionOrPredicate|jsonObjectKeyValueEntry|jsonObjectAssignmentEntry); + +jsonObjectKeyValueEntry + : KEY? expressionOrPredicate VALUE expressionOrPredicate; + +jsonObjectAssignmentEntry + : expressionOrPredicate ':' expressionOrPredicate; + +/** + * The 'json_query(, PASSING … AS … WITH WRAPPER ERROR|NULL|DEFAULT on ERROR|EMPTY)' function + */ +jsonQueryFunction + : JSON_QUERY '(' expression ',' expression jsonPassingClause? jsonQueryWrapperClause? jsonQueryOnErrorOrEmptyClause? jsonQueryOnErrorOrEmptyClause? ')'; + +jsonQueryWrapperClause + : WITH (CONDITIONAL | UNCONDITIONAL)? ARRAY? WRAPPER + | WITHOUT ARRAY? WRAPPER + ; + +jsonQueryOnErrorOrEmptyClause + : (ERROR | NULL | EMPTY (ARRAY | OBJECT)?) ON (ERROR | EMPTY); + +/** + * The 'json_value(… , PASSING … AS … RETURNING … ERROR|NULL|DEFAULT on ERROR|EMPTY)' function + */ +jsonValueFunction + : JSON_VALUE '(' expression ',' expression jsonPassingClause? jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? ')' + ; + +jsonValueReturningClause + : RETURNING castTarget + ; + +jsonValueOnErrorOrEmptyClause + : (ERROR | NULL | DEFAULT expression) ON (ERROR | EMPTY) + ; + +/** + * The 'json_arrayagg( …, ABSENT ON NULL ORDER BY)' function + */ +jsonArrayAggFunction + : JSON_ARRAYAGG '(' expressionOrPredicate jsonNullClause? orderByClause? ')' filterClause?; + +/** + * The 'json_objectagg( KEY? …, ABSENT ON NULL ORDER BY WITH|WITHOUT UNIQUE KEYS)' function + */ +jsonObjectAggFunction + : JSON_OBJECTAGG '(' KEY? expressionOrPredicate (VALUE | ':') expressionOrPredicate jsonNullClause? jsonUniqueKeysClause? ')' filterClause?; + +jsonPassingClause + : PASSING jsonPassingItem (',' jsonPassingItem)* + ; + +jsonPassingItem + : expressionOrPredicate AS identifier + ; + +jsonNullClause + : (ABSENT | NULL) ON NULL; + +jsonUniqueKeysClause + : (WITH | WITHOUT) UNIQUE KEYS; + +/** + * The 'json_table(…, …, PASSING COLUMNS(…) ERROR|NULL ON ERROR)' function + */ +jsonTableFunction + : JSON_TABLE '(' expression (',' expression)? jsonPassingClause? jsonTableColumnsClause jsonTableErrorClause? ')'; + +jsonTableErrorClause + : (ERROR | NULL) ON ERROR; + +jsonTableColumnsClause + : COLUMNS '(' jsonTableColumns ')'; + +jsonTableColumns + : jsonTableColumn (',' jsonTableColumn)*; + +jsonTableColumn + : NESTED PATH? STRING_LITERAL jsonTableColumnsClause # JsonTableNestedColumn + | identifier JSON jsonQueryWrapperClause? (PATH STRING_LITERAL)? jsonQueryOnErrorOrEmptyClause? jsonQueryOnErrorOrEmptyClause? # JsonTableQueryColumn + | identifier FOR ORDINALITY # JsonTableOrdinalityColumn + | identifier EXISTS (PATH STRING_LITERAL)? jsonExistsOnErrorClause? # JsonTableExistsColumn + | identifier castTarget (PATH STRING_LITERAL)? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? # JsonTableValueColumn + ; + +xmlFunction + : xmlElementFunction + | xmlForestFunction + | xmlPiFunction + | xmlQueryFunction + | xmlExistsFunction + | xmlAggFunction + ; + +xmlElementFunction + : XMLELEMENT '(' NAME identifier (',' xmlAttributesFunction)? (',' expressionOrPredicate)* ')' + ; + +xmlAttributesFunction + : XMLATTRIBUTES '(' expressionOrPredicate AS identifier (',' expressionOrPredicate AS identifier)* ')' + ; + +xmlForestFunction + : XMLFOREST '(' expressionOrPredicate (AS identifier)? (',' expressionOrPredicate (AS identifier)?)* ')' + ; + +xmlPiFunction + : XMLPI '(' NAME identifier (',' expression)? ')'; + +xmlQueryFunction + : XMLQUERY '(' expression PASSING expression ')'; + +xmlExistsFunction + : XMLEXISTS '(' expression PASSING expression ')'; + +xmlAggFunction + : XMLAGG '(' expression orderByClause? ')' filterClause? overClause?; + +xmlTableFunction + : XMLTABLE '(' expression PASSING expression xmlTableColumnsClause ')'; + +xmlTableColumnsClause + : COLUMNS xmlTableColumn (',' xmlTableColumn)*; + +xmlTableColumn + : identifier XML (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableQueryColumn + | identifier FOR ORDINALITY # XmlTableOrdinalityColumn + | identifier castTarget (PATH STRING_LITERAL)? xmltableDefaultClause? # XmlTableValueColumn + ; + +xmltableDefaultClause + : DEFAULT expression; + // Predicates // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-conditional-expressions predicate @@ -1382,9 +1555,11 @@ entityName nakedIdentifier : IDENTIFIER | QUOTED_IDENTIFIER - | f=(ALL + | f=(ABSENT + | ALL | AND | ANY + | ARRAY | AS | ASC | AVG @@ -1396,6 +1571,8 @@ nakedIdentifier | CAST | COLLATE | COLUMN + | COLUMNS + | CONDITIONAL | CONFLICT | CONSTRAINT | CONTAINS @@ -1458,6 +1635,15 @@ nakedIdentifier | INTO | IS | JOIN + | JSON + | JSON_ARRAY + | JSON_ARRAYAGG + | JSON_EXISTS + | JSON_OBJECT + | JSON_OBJECTAGG + | JSON_QUERY + | JSON_TABLE + | JSON_VALUE | KEY | KEYS | LAST @@ -1484,9 +1670,11 @@ nakedIdentifier | MININDEX | MINUTE | MONTH + | NAME | NANOSECOND | NATURALID | NEW + | NESTED | NEXT | NO | NOT @@ -1500,12 +1688,15 @@ nakedIdentifier | ONLY | OR | ORDER + | ORDINALITY | OTHERS | OVER | OVERFLOW | OVERLAY | PAD + | PATH | PARTITION + | PASSING | PERCENT | PLACING | POSITION @@ -1513,6 +1704,7 @@ nakedIdentifier | QUARTER | RANGE | RESPECT + | RETURNING | RIGHT | ROLLUP | ROW @@ -1539,7 +1731,9 @@ nakedIdentifier | TRUNCATE | TYPE | UNBOUNDED + | UNCONDITIONAL | UNION + | UNIQUE | UPDATE | USING | VALUE @@ -1552,6 +1746,16 @@ nakedIdentifier | WITH | WITHIN | WITHOUT + | WRAPPER + | XML + | XMLAGG + | XMLATTRIBUTES + | XMLELEMENT + | XMLEXISTS + | XMLFOREST + | XMLPI + | XMLQUERY + | XMLTABLE | YEAR | ZONED) ; @@ -1610,9 +1814,11 @@ VERSION : V E R S I O N; VERSIONED : V E R S I O N E D; NATURALID : N A T U R A L I D; FK : F K; +ABSENT : A B S E N T; ALL : A L L; AND : A N D; ANY : A N Y; +ARRAY : A R R A Y; AS : A S; ASC : A S C; AVG : A V G; @@ -1624,6 +1830,8 @@ CASE : C A S E; CAST : C A S T; COLLATE : C O L L A T E; COLUMN : C O L U M N; +COLUMNS : C O L U M N S; +CONDITIONAL : C O N D I T I O N A L; CONFLICT : C O N F L I C T; CONSTRAINT : C O N S T R A I N T; CONTAINS : C O N T A I N S; @@ -1686,6 +1894,15 @@ INTERSECTS : I N T E R S E C T S; INTO : I N T O; IS : I S; JOIN : J O I N; +JSON : J S O N; +JSON_ARRAY : J S O N '_' A R R A Y; +JSON_ARRAYAGG : J S O N '_' A R R A Y A G G; +JSON_EXISTS : J S O N '_' E X I S T S; +JSON_OBJECT : J S O N '_' O B J E C T; +JSON_OBJECTAGG : J S O N '_' O B J E C T A G G; +JSON_QUERY : J S O N '_' Q U E R Y; +JSON_TABLE : J S O N '_' T A B L E; +JSON_VALUE : J S O N '_' V A L U E; KEY : K E Y; KEYS : K E Y S; LAST : L A S T; @@ -1713,8 +1930,10 @@ MINELEMENT : M I N E L E M E N T; MININDEX : M I N I N D E X; MINUTE : M I N U T E; MONTH : M O N T H; +NAME : N A M E; NANOSECOND : N A N O S E C O N D; NEW : N E W; +NESTED : N E S T E D; NEXT : N E X T; NO : N O; NOT : N O T; @@ -1728,13 +1947,16 @@ ON : O N; ONLY : O N L Y; OR : O R; ORDER : O R D E R; +ORDINALITY : O R D I N A L I T Y; OTHERS : O T H E R S; OUTER : O U T E R; OVER : O V E R; OVERFLOW : O V E R F L O W; OVERLAY : O V E R L A Y; PAD : P A D; +PATH : P A T H; PARTITION : P A R T I T I O N; +PASSING : P A S S I N G; PERCENT : P E R C E N T; PLACING : P L A C I N G; POSITION : P O S I T I O N; @@ -1742,6 +1964,7 @@ PRECEDING : P R E C E D I N G; QUARTER : Q U A R T E R; RANGE : R A N G E; RESPECT : R E S P E C T; +RETURNING : R E T U R N I N G; RIGHT : R I G H T; ROLLUP : R O L L U P; ROW : R O W; @@ -1768,7 +1991,9 @@ TRUNC : T R U N C; TRUNCATE : T R U N C A T E; TYPE : T Y P E; UNBOUNDED : U N B O U N D E D; +UNCONDITIONAL : U N C O N D I T I O N A L; UNION : U N I O N; +UNIQUE : U N I Q U E; UPDATE : U P D A T E; USING : U S I N G; VALUE : V A L U E; @@ -1779,6 +2004,16 @@ WHERE : W H E R E; WITH : W I T H; WITHIN : W I T H I N; WITHOUT : W I T H O U T; +WRAPPER : W R A P P E R; +XML : X M L; +XMLAGG : X M L A G G; +XMLATTRIBUTES : X M L A T T R I B U T E S; +XMLELEMENT : X M L E L E M E N T; +XMLEXISTS : X M L E X I S T S; +XMLFOREST : X M L F O R E S T; +XMLPI : X M L P I; +XMLQUERY : X M L Q U E R Y; +XMLTABLE : X M L T A B L E; YEAR : Y E A R; ZONED : Z O N E D; 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 d959246407..00bcde0c7e 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 @@ -22,8 +22,10 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** @@ -421,7 +423,16 @@ public QueryTokenStream visitRootFunction(HqlParser.RootFunctionContext ctx) { @Override public QueryTokenStream visitSetReturningFunction(HqlParser.SetReturningFunctionContext ctx) { - return visit(ctx.simpleSetReturningFunction()); + + if (ctx.simpleSetReturningFunction() != null) { + return visit(ctx.simpleSetReturningFunction()); + } else if (ctx.jsonTableFunction() != null) { + return visit(ctx.jsonTableFunction()); + } else if (ctx.xmlTableFunction() != null) { + return visit(ctx.xmlTableFunction()); + } + + return QueryTokenStream.empty(); } @Override @@ -462,6 +473,7 @@ public QueryTokenStream visitJoin(HqlParser.JoinContext ctx) { return builder; } + @Override public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { @@ -503,14 +515,17 @@ public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.setReturningFunction())); + if (ctx.LATERAL() != null) { + builder.append(QueryTokens.expression(ctx.LATERAL())); + } + + builder.appendExpression(visit(ctx.setReturningFunction())); if (ctx.variable() != null) { builder.appendExpression(visit(ctx.variable())); } return builder; - } @Override @@ -2947,6 +2962,322 @@ public QueryTokenStream visitFrameExclusion(HqlParser.FrameExclusionContext ctx) return builder; } + @Override + public QueryTokenStream visitJsonFunctionInvocation(HqlParser.JsonFunctionInvocationContext ctx) { + return visit(ctx.jsonFunction()); + } + + @Override + public QueryTokenStream visitJsonFunction(HqlParser.JsonFunctionContext ctx) { + + if (ctx.jsonArrayFunction() != null) { + return visit(ctx.jsonArrayFunction()); + } else if (ctx.jsonExistsFunction() != null) { + return visit(ctx.jsonExistsFunction()); + } else if (ctx.jsonObjectFunction() != null) { + return visit(ctx.jsonObjectFunction()); + } else if (ctx.jsonQueryFunction() != null) { + return visit(ctx.jsonQueryFunction()); + } else if (ctx.jsonValueFunction() != null) { + return visit(ctx.jsonValueFunction()); + } else if (ctx.jsonArrayAggFunction() != null) { + return visit(ctx.jsonArrayAggFunction()); + } else if (ctx.jsonObjectAggFunction() != null) { + return visit(ctx.jsonObjectAggFunction()); + } + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); + + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_ARRAY(), builder); + } + + @Override + public QueryTokenStream visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); + } + + if (ctx.jsonExistsOnErrorClause() != null) { + builder.appendExpression(visit(ctx.jsonExistsOnErrorClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_EXISTS(), builder); + } + + @Override + public QueryTokenStream visitJsonExistsOnErrorClause(HqlParser.JsonExistsOnErrorClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonObjectFunction(HqlParser.JsonObjectFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(QueryTokenStream.concat(ctx.jsonObjectFunctionEntry(), this::visit, TOKEN_COMMA)); + + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_OBJECT(), builder); + } + + @Override + public QueryTokenStream visitJsonObjectFunctionEntry(HqlParser.JsonObjectFunctionEntryContext ctx) { + + if (ctx.expressionOrPredicate() != null) { + return visit(ctx.expressionOrPredicate()); + } else if (ctx.jsonObjectKeyValueEntry() != null) { + return visit(ctx.jsonObjectKeyValueEntry()); + } else if (ctx.jsonObjectAssignmentEntry() != null) { + return visit(ctx.jsonObjectAssignmentEntry()); + } + + return QueryTokenStream.empty(); + } + + @Override + public QueryTokenStream visitJsonObjectKeyValueEntry(HqlParser.JsonObjectKeyValueEntryContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonObjectAssignmentEntry(HqlParser.JsonObjectAssignmentEntryContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonQueryFunction(HqlParser.JsonQueryFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); + } + + if (ctx.jsonQueryWrapperClause() != null) { + builder.appendExpression(visit(ctx.jsonQueryWrapperClause())); + } + + builder.append(QueryTokenStream.concat(ctx.jsonQueryOnErrorOrEmptyClause(), this::visit, TOKEN_SPACE)); + + return QueryTokenStream.ofFunction(ctx.JSON_QUERY(), builder); + } + + @Override + public QueryTokenStream visitJsonQueryWrapperClause(HqlParser.JsonQueryWrapperClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonQueryOnErrorOrEmptyClause(HqlParser.JsonQueryOnErrorOrEmptyClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonValueFunction(HqlParser.JsonValueFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); + } + + if (ctx.jsonValueReturningClause() != null) { + builder.appendExpression(visit(ctx.jsonValueReturningClause())); + } + + builder.append(QueryTokenStream.concat(ctx.jsonValueOnErrorOrEmptyClause(), this::visit, TOKEN_SPACE)); + + return QueryTokenStream.ofFunction(ctx.JSON_VALUE(), builder); + } + + @Override + public QueryTokenStream visitJsonValueReturningClause(HqlParser.JsonValueReturningClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonValueOnErrorOrEmptyClause(HqlParser.JsonValueOnErrorOrEmptyClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.expressionOrPredicate())); + + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); + } + + if (ctx.orderByClause() != null) { + builder.appendExpression(visit(ctx.orderByClause())); + } + + QueryTokenStream function = QueryTokenStream.ofFunction(ctx.JSON_ARRAYAGG(), builder); + + if (ctx.filterClause() == null) { + return function; + } + + QueryRendererBuilder functionWithFilter = QueryRenderer.builder(); + functionWithFilter.appendExpression(function); + functionWithFilter.appendExpression(visit(ctx.filterClause())); + + return functionWithFilter.build(); + } + + @Override + public QueryTokenStream visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.KEY() != null) { + builder.append(QueryTokens.expression(ctx.KEY())); + } + + builder.appendExpression(visit(ctx.expressionOrPredicate(0))); + + if (ctx.VALUE() != null) { + builder.append(QueryTokens.expression(ctx.VALUE())); + } else { + builder.append(TOKEN_COLON); + } + + builder.appendExpression(visit(ctx.expressionOrPredicate(1))); + + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); + } + + if (ctx.jsonUniqueKeysClause() != null) { + builder.appendExpression(visit(ctx.jsonUniqueKeysClause())); + } + + QueryTokenStream function = QueryTokenStream.ofFunction(ctx.JSON_OBJECTAGG(), builder); + + if (ctx.filterClause() == null) { + return function; + } + + QueryRendererBuilder functionWithFilter = QueryRenderer.builder(); + functionWithFilter.appendExpression(function); + functionWithFilter.appendExpression(visit(ctx.filterClause())); + + return functionWithFilter.build(); + } + + @Override + public QueryTokenStream visitJsonPassingClause(HqlParser.JsonPassingClauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.PASSING())); + + builder.append(QueryTokenStream.concat(ctx.jsonPassingItem(), this::visit, TOKEN_COMMA)); + + return builder; + } + + @Override + public QueryTokenStream visitJsonPassingItem(HqlParser.JsonPassingItemContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonNullClause(HqlParser.JsonNullClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonUniqueKeysClause(HqlParser.JsonUniqueKeysClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonTableFunction(HqlParser.JsonTableFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); + } + + builder.appendExpression(visit(ctx.jsonTableColumnsClause())); + + if (ctx.jsonTableErrorClause() != null) { + builder.appendExpression(visit(ctx.jsonTableErrorClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_TABLE(), builder); + } + + @Override + public QueryTokenStream visitJsonTableErrorClause(HqlParser.JsonTableErrorClauseContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonTableColumnsClause(HqlParser.JsonTableColumnsClauseContext ctx) { + return QueryTokenStream.ofFunction(ctx.COLUMNS(), visit(ctx.jsonTableColumns())); + } + + @Override + public QueryTokenStream visitJsonTableColumns(HqlParser.JsonTableColumnsContext ctx) { + return QueryTokenStream.concat(ctx.jsonTableColumn(), this::visit, TOKEN_COMMA); + } + + @Override + public QueryTokenStream visitJsonTableNestedColumn(HqlParser.JsonTableNestedColumnContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonTableQueryColumn(HqlParser.JsonTableQueryColumnContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonTableOrdinalityColumn(HqlParser.JsonTableOrdinalityColumnContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonTableExistsColumn(HqlParser.JsonTableExistsColumnContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitJsonTableValueColumn(HqlParser.JsonTableValueColumnContext ctx) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + @Override public QueryTokenStream visitCollectionQuantifier(HqlParser.CollectionQuantifierContext ctx) { @@ -3284,29 +3615,29 @@ public QueryTokenStream visitCastFunction(HqlParser.CastFunctionContext ctx) { @Override public QueryTokenStream visitCastTarget(HqlParser.CastTargetContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.castTargetType())); + List literals = ctx.INTEGER_LITERAL(); - if (ctx.INTEGER_LITERAL() != null && !ctx.INTEGER_LITERAL().isEmpty()) { + if (!CollectionUtils.isEmpty(literals)) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(visit(ctx.castTargetType())); builder.append(TOKEN_OPEN_PAREN); - List tokens = new ArrayList<>(); - ctx.INTEGER_LITERAL().forEach(terminalNode -> { - - if (!tokens.isEmpty()) { - tokens.add(TOKEN_COMMA); + QueryRendererBuilder args = QueryRenderer.builder(); + for (int i = 0; i < literals.size(); i++) { + if (i > 0) { + args.append(TOKEN_COMMA); } - tokens.add(QueryTokens.expression(terminalNode)); - - }); + args.append(QueryTokens.token(literals.get(i))); + } - builder.append(tokens); + builder.appendInline(args.build()); builder.append(TOKEN_CLOSE_PAREN); + + return builder.build(); } - return builder; + return visit(ctx.castTargetType()); } @Override 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 402fca8f7d..bfbc47b395 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 @@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.util.Assert; import org.springframework.util.CompositeIterator; /** @@ -188,6 +189,8 @@ public static QueryRenderer ofExpression(QueryTokenStream tokenStream) { public static QueryRenderer inline(QueryTokenStream tokenStream) { + Assert.notNull(tokenStream, "QueryTokenStream must not be null!"); + if (tokenStream instanceof QueryRendererBuilder builder) { tokenStream = builder.current; } 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 5b68191cfd..44b5c55271 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.jpa.repository.query; +import static org.springframework.data.jpa.repository.query.QueryTokens.*; + import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -94,6 +96,32 @@ static QueryTokenStream concatExpressions(Collection elements, Function QueryTokenStream concatExpressions(Collection elements, Function visitor) { + + QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); + + for (T child : elements) { + + if (child instanceof Token t) { + builder.append(QueryTokens.expression(t)); + } else if (child instanceof TerminalNode tn) { + builder.append(QueryTokens.expression(tn)); + } else { + builder.appendExpression(visitor.apply(child)); + } + } + + return builder.build(); + } + /** * Compose a {@link QueryTokenStream} from a collection of elements. * @@ -139,6 +167,26 @@ static QueryTokenStream concat(Collection elements, Function Date: Thu, 10 Jul 2025 17:45:28 +0200 Subject: [PATCH 141/224] Simplify `Hql|Eql|JpqlQueryRenderer`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use default visitChildren(…) for expression concatenation, remove simple calls to visit(…) in favor of default handling. See #3883 --- .../repository/query/EqlQueryRenderer.java | 1871 +------- .../repository/query/HqlQueryRenderer.java | 4190 ++++------------- .../repository/query/JpqlQueryRenderer.java | 1861 +------- .../jpa/repository/query/QueryRenderer.java | 100 +- .../data/jpa/repository/query/QueryToken.java | 2 +- .../repository/query/QueryTokenStream.java | 101 +- .../jpa/repository/query/QueryTokens.java | 33 +- 7 files changed, 1267 insertions(+), 6891 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 57e23dbc2c..707ccd6995 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 @@ -21,11 +21,13 @@ import java.util.List; import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RuleContext; import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; +import org.antlr.v4.runtime.tree.TerminalNode; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. @@ -75,109 +77,6 @@ public QueryTokenStream visitStart(EqlParser.StartContext ctx) { return visit(ctx.ql_statement()); } - @Override - public QueryTokenStream visitQl_statement(EqlParser.Ql_statementContext ctx) { - - if (ctx.select_statement() != null) { - return visit(ctx.select_statement()); - } else if (ctx.update_statement() != null) { - return visit(ctx.update_statement()); - } else if (ctx.delete_statement() != null) { - return visit(ctx.delete_statement()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitSelectQuery(EqlParser.SelectQueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.select_clause())); - builder.appendExpression(visit(ctx.from_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - if (ctx.groupby_clause() != null) { - builder.appendExpression(visit(ctx.groupby_clause())); - } - - if (ctx.having_clause() != null) { - builder.appendExpression(visit(ctx.having_clause())); - } - - if (ctx.orderby_clause() != null) { - builder.appendExpression(visit(ctx.orderby_clause())); - } - - if (ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFromQuery(EqlParser.FromQueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.from_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - if (ctx.groupby_clause() != null) { - builder.appendExpression(visit(ctx.groupby_clause())); - } - - if (ctx.having_clause() != null) { - builder.appendExpression(visit(ctx.having_clause())); - } - - if (ctx.orderby_clause() != null) { - builder.appendExpression(visit(ctx.orderby_clause())); - } - - if (ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); - } - - return builder; - } - - @Override - public QueryTokenStream visitUpdate_statement(EqlParser.Update_statementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.update_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitDelete_statement(EqlParser.Delete_statementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.delete_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - return builder; - } - @Override public QueryTokenStream visitFrom_clause(EqlParser.From_clauseContext ctx) { @@ -200,11 +99,7 @@ public QueryTokenStream visitFrom_clause(EqlParser.From_clauseContext ctx) { public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMemberDeclaration( EqlParser.IdentificationVariableDeclarationOrCollectionMemberDeclarationContext ctx) { - if (ctx.identification_variable_declaration() != null) { - return visit(ctx.identification_variable_declaration()); - } else if (ctx.collection_member_declaration() != null) { - return visit(ctx.collection_member_declaration()); - } else if (ctx.subquery() != null) { + if (ctx.subquery() != null) { QueryRendererBuilder nested = QueryRenderer.builder(); nested.append(TOKEN_OPEN_PAREN); @@ -223,121 +118,9 @@ public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMember } return builder; - } else { - return QueryRenderer.builder(); - } - } - - @Override - public QueryTokenStream visitIdentification_variable_declaration( - EqlParser.Identification_variable_declarationContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - 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; - } - - @Override - public QueryTokenStream visitRange_variable_declaration(EqlParser.Range_variable_declarationContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.entity_name() != null) { - builder.appendExpression(visit(ctx.entity_name())); - } else if (ctx.function_invocation() != null) { - builder.appendExpression(visit(ctx.function_invocation())); - } - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - - if (ctx.identification_variable() != null) { - builder.appendExpression(visit(ctx.identification_variable())); - } - - return builder; - } - - @Override - public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.join_spec())); - 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; - } - - @Override - public QueryTokenStream visitFetch_join(EqlParser.Fetch_joinContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.join_spec())); - builder.append(QueryTokens.expression(ctx.FETCH())); - 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; - } - - @Override - public QueryTokenStream visitJoin_spec(EqlParser.Join_specContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.LEFT() != null) { - builder.append(QueryTokens.expression(ctx.LEFT())); - } - if (ctx.OUTER() != null) { - builder.append(QueryTokens.expression(ctx.OUTER())); - } - if (ctx.INNER() != null) { - builder.append(QueryTokens.expression(ctx.INNER())); } - if (ctx.JOIN() != null) { - builder.append(QueryTokens.expression(ctx.JOIN())); - } - - return builder; - } - @Override - public QueryTokenStream visitJoin_condition(EqlParser.Join_conditionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.ON())); - builder.appendExpression(visit(ctx.conditional_expression())); - - return builder; + return super.visitIdentificationVariableDeclarationOrCollectionMemberDeclaration(ctx); } @Override @@ -498,20 +281,6 @@ public QueryTokenStream visitSingle_valued_path_expression(EqlParser.Single_valu return builder; } - @Override - public QueryTokenStream visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.map_field_identification_variable() != null) { - return visit(ctx.map_field_identification_variable()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitGeneral_subpath(EqlParser.General_subpathContext ctx) { @@ -570,20 +339,6 @@ public QueryTokenStream visitState_field_path_expression(EqlParser.State_field_p return builder; } - @Override - public QueryTokenStream visitState_valued_path_expression(EqlParser.State_valued_path_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_field_path_expression() != null) { - return visit(ctx.state_field_path_expression()); - } else if (ctx.general_identification_variable() != null) { - return visit(ctx.general_identification_variable()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitSingle_valued_object_path_expression( EqlParser.Single_valued_object_path_expressionContext ctx) { @@ -658,39 +413,6 @@ public QueryTokenStream visitUpdate_item(EqlParser.Update_itemContext ctx) { return builder; } - @Override - public QueryTokenStream visitNew_value(EqlParser.New_valueContext ctx) { - - if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.simple_entity_expression() != null) { - return visit(ctx.simple_entity_expression()); - } else if (ctx.NULL() != null) { - return QueryTokenStream.ofToken(ctx.NULL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitDelete_clause(EqlParser.Delete_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.DELETE())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendExpression(visit(ctx.entity_name())); - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - if (ctx.identification_variable() != null) { - builder.appendExpression(visit(ctx.identification_variable())); - } - - return builder; - } - @Override public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { @@ -714,53 +436,22 @@ QueryRendererBuilder prepareSelectClause(EqlParser.Select_clauseContext ctx) { return builder; } - @Override - public QueryTokenStream visitSelect_item(EqlParser.Select_itemContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.select_expression())); - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - - if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } - - return builder; - } - @Override public QueryTokenStream visitSelect_expression(EqlParser.Select_expressionContext ctx) { - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.aggregate_expression() != null) { - return visit(ctx.aggregate_expression()); - } else if (ctx.identification_variable() != null) { - - if (ctx.OBJECT() == null) { - return visit(ctx.identification_variable()); - } else { + if (ctx.identification_variable() != null && ctx.OBJECT() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.OBJECT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.identification_variable())); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokens.token(ctx.OBJECT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.identification_variable())); + builder.append(TOKEN_CLOSE_PAREN); - return builder; - } - } else if (ctx.constructor_expression() != null) { - return visit(ctx.constructor_expression()); - } else { - return QueryTokenStream.empty(); + return builder; } + + return super.visitSelect_expression(ctx); } @Override @@ -777,24 +468,6 @@ public QueryTokenStream visitConstructor_expression(EqlParser.Constructor_expres return builder; } - @Override - public QueryTokenStream visitConstructor_item(EqlParser.Constructor_itemContext ctx) { - - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.aggregate_expression() != null) { - return visit(ctx.aggregate_expression()); - } else if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.literal() != null) { - return visit(ctx.literal()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitAggregate_expression(EqlParser.Aggregate_expressionContext ctx) { @@ -845,17 +518,6 @@ public QueryTokenStream visitAggregate_expression(EqlParser.Aggregate_expression return builder; } - @Override - public QueryTokenStream visitWhere_clause(EqlParser.Where_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHERE())); - builder.appendExpression(visit(ctx.conditional_expression())); - - return builder; - } - @Override public QueryTokenStream visitGroupby_clause(EqlParser.Groupby_clauseContext ctx) { @@ -868,31 +530,6 @@ public QueryTokenStream visitGroupby_clause(EqlParser.Groupby_clauseContext ctx) return builder; } - @Override - public QueryTokenStream visitGroupby_item(EqlParser.Groupby_itemContext ctx) { - - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitHaving_clause(EqlParser.Having_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.HAVING())); - builder.appendExpression(visit(ctx.conditional_expression())); - - return builder; - } - @Override public QueryTokenStream visitOrderby_clause(EqlParser.Orderby_clauseContext ctx) { @@ -906,710 +543,90 @@ public QueryTokenStream visitOrderby_clause(EqlParser.Orderby_clauseContext ctx) } @Override - public QueryTokenStream visitOrderby_item(EqlParser.Orderby_itemContext ctx) { + public QueryTokenStream visitSubquery_from_clause(EqlParser.Subquery_from_clauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.orderby_expression())); - - if (ctx.ASC() != null) { - builder.append(QueryTokens.expression(ctx.ASC())); - } else if (ctx.DESC() != null) { - builder.append(QueryTokens.expression(ctx.DESC())); - } - - if (ctx.nullsPrecedence() != null) { - builder.appendExpression(visit(ctx.nullsPrecedence())); - } + builder.append(QueryTokens.expression(ctx.FROM())); + builder.appendExpression( + QueryTokenStream.concat(ctx.subselect_identification_variable_declaration(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitOrderby_expression(EqlParser.Orderby_expressionContext ctx) { - - if (ctx.state_field_path_expression() != null) { - return visit(ctx.state_field_path_expression()); - } else if (ctx.general_identification_variable() != null) { - return visit(ctx.general_identification_variable()); - } else if (ctx.string_expression() != null) { - return visit(ctx.string_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); + public QueryTokenStream visitConditional_primary(EqlParser.Conditional_primaryContext ctx) { + + if (ctx.conditional_expression() != null) { + return QueryTokenStream.group(visit(ctx.conditional_expression())); } - return QueryTokenStream.empty(); + return super.visitConditional_primary(ctx); } @Override - public QueryTokenStream visitNullsPrecedence(EqlParser.NullsPrecedenceContext ctx) { + public QueryTokenStream visitIn_expression(EqlParser.In_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.NULLS())); - - if (ctx.FIRST() != null) { - builder.append(QueryTokens.expression(ctx.FIRST())); - } else if (ctx.LAST() != null) { - builder.append(QueryTokens.expression(ctx.LAST())); + if (ctx.string_expression() != null) { + builder.appendExpression(visit(ctx.string_expression())); } - 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; - } - - @Override - public QueryTokenStream visitSubquery(EqlParser.SubqueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.simple_select_clause())); - builder.appendExpression(visit(ctx.subquery_from_clause())); - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - if (ctx.groupby_clause() != null) { - builder.appendExpression(visit(ctx.groupby_clause())); - } - if (ctx.having_clause() != null) { - builder.appendExpression(visit(ctx.having_clause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitSubquery_from_clause(EqlParser.Subquery_from_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendExpression( - QueryTokenStream.concat(ctx.subselect_identification_variable_declaration(), this::visit, TOKEN_COMMA)); - - return builder; - } - - @Override - public QueryTokenStream visitSubselect_identification_variable_declaration( - EqlParser.Subselect_identification_variable_declarationContext ctx) { - return super.visitSubselect_identification_variable_declaration(ctx); - } - - @Override - public QueryTokenStream visitDerived_path_expression(EqlParser.Derived_path_expressionContext ctx) { - return super.visitDerived_path_expression(ctx); - } - - @Override - public QueryTokenStream visitGeneral_derived_path(EqlParser.General_derived_pathContext ctx) { - return super.visitGeneral_derived_path(ctx); - } - - @Override - public QueryTokenStream visitSimple_derived_path(EqlParser.Simple_derived_pathContext ctx) { - return super.visitSimple_derived_path(ctx); - } - - @Override - public QueryTokenStream visitTreated_derived_path(EqlParser.Treated_derived_pathContext ctx) { - return super.visitTreated_derived_path(ctx); - } - - @Override - public QueryTokenStream visitDerived_collection_member_declaration( - EqlParser.Derived_collection_member_declarationContext ctx) { - return super.visitDerived_collection_member_declaration(ctx); - } - - @Override - public QueryTokenStream visitSimple_select_clause(EqlParser.Simple_select_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.SELECT())); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); - } - builder.append(visit(ctx.simple_select_expression())); - - return builder; - } - - @Override - public QueryTokenStream visitSimple_select_expression(EqlParser.Simple_select_expressionContext ctx) { - - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.aggregate_expression() != null) { - return visit(ctx.aggregate_expression()); - } else if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitScalar_expression(EqlParser.Scalar_expressionContext ctx) { - - if (ctx.arithmetic_expression() != null) { - return visit(ctx.arithmetic_expression()); - } else if (ctx.string_expression() != null) { - return visit(ctx.string_expression()); - } else if (ctx.enum_expression() != null) { - return visit(ctx.enum_expression()); - } else if (ctx.datetime_expression() != null) { - return visit(ctx.datetime_expression()); - } else if (ctx.boolean_expression() != null) { - return visit(ctx.boolean_expression()); - } else if (ctx.case_expression() != null) { - return visit(ctx.case_expression()); - } else if (ctx.entity_type_expression() != null) { - return visit(ctx.entity_type_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitConditional_expression(EqlParser.Conditional_expressionContext ctx) { - - if (ctx.conditional_expression() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.conditional_expression())); - builder.append(QueryTokens.expression(ctx.OR())); - builder.appendExpression(visit(ctx.conditional_term())); - - return builder; - } else { - return visit(ctx.conditional_term()); - } - } - - @Override - public QueryTokenStream visitConditional_term(EqlParser.Conditional_termContext ctx) { - - if (ctx.conditional_term() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.conditional_term())); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.conditional_factor())); - - return builder; - } else { - return visit(ctx.conditional_factor()); - } - } - - @Override - public QueryTokenStream visitConditional_factor(EqlParser.Conditional_factorContext ctx) { - - if (ctx.NOT() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.NOT())); - builder.appendExpression(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.conditional_expression() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.conditional_expression())); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitSimple_cond_expression(EqlParser.Simple_cond_expressionContext ctx) { - - if (ctx.comparison_expression() != null) { - return visit(ctx.comparison_expression()); - } else if (ctx.between_expression() != null) { - return visit(ctx.between_expression()); - } else if (ctx.in_expression() != null) { - return visit(ctx.in_expression()); - } else if (ctx.like_expression() != null) { - return visit(ctx.like_expression()); - } else if (ctx.null_comparison_expression() != null) { - return visit(ctx.null_comparison_expression()); - } else if (ctx.empty_collection_comparison_expression() != null) { - return visit(ctx.empty_collection_comparison_expression()); - } else if (ctx.collection_member_expression() != null) { - return visit(ctx.collection_member_expression()); - } else if (ctx.exists_expression() != null) { - return visit(ctx.exists_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitBetween_expression(EqlParser.Between_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.arithmetic_expression(0) != null) { - - builder.appendExpression(visit(ctx.arithmetic_expression(0))); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.arithmetic_expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.arithmetic_expression(2))); - - } else if (ctx.string_expression(0) != null) { - - builder.appendExpression(visit(ctx.string_expression(0))); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.string_expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.string_expression(2))); - - } else if (ctx.datetime_expression(0) != null) { - - builder.appendExpression(visit(ctx.datetime_expression(0))); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.datetime_expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.datetime_expression(2))); - } - - return builder; - } - - @Override - public QueryTokenStream visitIn_expression(EqlParser.In_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.string_expression() != null) { - builder.appendExpression(visit(ctx.string_expression())); - } - if (ctx.type_discriminator() != null) { - builder.appendExpression(visit(ctx.type_discriminator())); - } - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - if (ctx.IN() != null) { - builder.append(QueryTokens.expression(ctx.IN())); - } - - if (ctx.in_item() != null && !ctx.in_item().isEmpty()) { - - 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) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.collection_valued_input_parameter() != null) { - builder.append(visit(ctx.collection_valued_input_parameter())); - } - - return builder; - } - - @Override - public QueryTokenStream visitIn_item(EqlParser.In_itemContext ctx) { - - if (ctx.literal() != null) { - return visit(ctx.literal()); - } else if (ctx.string_expression() != null) { - return visit(ctx.string_expression()); - } else if (ctx.boolean_literal() != null) { - return visit(ctx.boolean_literal()); - } else if (ctx.numeric_literal() != null) { - return visit(ctx.numeric_literal()); - } else if (ctx.date_time_timestamp_literal() != null) { - return visit(ctx.date_time_timestamp_literal()); - } else if (ctx.single_valued_input_parameter() != null) { - return visit(ctx.single_valued_input_parameter()); - } else if (ctx.conditional_expression() != null) { - return visit(ctx.conditional_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitLike_expression(EqlParser.Like_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.string_expression())); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - builder.append(QueryTokens.expression(ctx.LIKE())); - builder.appendExpression(visit(ctx.pattern_value())); - - if (ctx.ESCAPE() != null) { - - builder.append(QueryTokens.expression(ctx.ESCAPE())); - builder.appendExpression(visit(ctx.escape_character())); - } - - return builder; - } - - @Override - public QueryTokenStream visitNull_comparison_expression(EqlParser.Null_comparison_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.single_valued_path_expression() != null) { - builder.appendExpression(visit(ctx.single_valued_path_expression())); - } else if (ctx.input_parameter() != null) { - builder.appendExpression(visit(ctx.input_parameter())); - } else if (ctx.nullif_expression() != null) { - builder.appendExpression(visit(ctx.nullif_expression())); - } - - if (ctx.op != null) { - builder.append(QueryTokens.expression(ctx.op.getText())); - } else { - builder.append(QueryTokens.expression(ctx.IS())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - } - builder.append(QueryTokens.expression(ctx.NULL())); - - return builder; - } - - @Override - public QueryTokenStream visitEmpty_collection_comparison_expression( - EqlParser.Empty_collection_comparison_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.collection_valued_path_expression())); - builder.append(QueryTokens.expression(ctx.IS())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - builder.append(QueryTokens.expression(ctx.EMPTY())); - - return builder; - } - - @Override - public QueryTokenStream visitCollection_member_expression(EqlParser.Collection_member_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.entity_or_value_expression())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - builder.append(QueryTokens.expression(ctx.MEMBER())); - if (ctx.OF() != null) { - builder.append(QueryTokens.expression(ctx.OF())); - } - builder.append(visit(ctx.collection_valued_path_expression())); - - return builder; - } - - @Override - public QueryTokenStream visitEntity_or_value_expression(EqlParser.Entity_or_value_expressionContext ctx) { - - if (ctx.single_valued_object_path_expression() != null) { - return visit(ctx.single_valued_object_path_expression()); - } else if (ctx.state_field_path_expression() != null) { - return visit(ctx.state_field_path_expression()); - } else if (ctx.simple_entity_or_value_expression() != null) { - return visit(ctx.simple_entity_or_value_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSimple_entity_or_value_expression( - EqlParser.Simple_entity_or_value_expressionContext ctx) { - - if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } else if (ctx.literal() != null) { - return visit(ctx.literal()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitExists_expression(EqlParser.Exists_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.EXISTS())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitAll_or_any_expression(EqlParser.All_or_any_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.ALL() != null) { - builder.append(QueryTokens.expression(ctx.ALL())); - } else if (ctx.ANY() != null) { - builder.append(QueryTokens.expression(ctx.ANY())); - } else if (ctx.SOME() != null) { - builder.append(QueryTokens.expression(ctx.SOME())); - } - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitStringComparison(EqlParser.StringComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.string_expression(0))); - builder.appendInline(visit(ctx.comparison_operator())); - - if (ctx.string_expression(1) != null) { - builder.appendExpression(visit(ctx.string_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); - } - - return builder; - } - - @Override - public QueryTokenStream visitBooleanComparison(EqlParser.BooleanComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.boolean_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.boolean_expression(1) != null) { - builder.appendExpression(visit(ctx.boolean_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); + if (ctx.type_discriminator() != null) { + builder.appendExpression(visit(ctx.type_discriminator())); } - return builder; - } - - @Override - public QueryTokenStream visitDirectBooleanCheck(EqlParser.DirectBooleanCheckContext ctx) { - return visit(ctx.boolean_expression()); - } - - @Override - public QueryTokenStream visitEnumComparison(EqlParser.EnumComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.enum_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.enum_expression(1) != null) { - builder.appendExpression(visit(ctx.enum_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); + if (ctx.NOT() != null) { + builder.append(QueryTokens.expression(ctx.NOT())); } - return builder; - } - - @Override - public QueryTokenStream visitDatetimeComparison(EqlParser.DatetimeComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.datetime_expression(0))); - builder.appendInline(visit(ctx.comparison_operator())); - - if (ctx.datetime_expression(1) != null) { - builder.appendExpression(visit(ctx.datetime_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); + if (ctx.IN() != null) { + builder.append(QueryTokens.expression(ctx.IN())); } - return builder; - } - - @Override - public QueryTokenStream visitEntityComparison(EqlParser.EntityComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.entity_expression(0))); - builder.append(QueryTokens.expression(ctx.op)); - - if (ctx.entity_expression(1) != null) { - builder.appendExpression(visit(ctx.entity_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); + if (ctx.in_item() != null && !ctx.in_item().isEmpty()) { + builder.append(QueryTokenStream.group(QueryTokenStream.concat(ctx.in_item(), this::visit, TOKEN_COMMA))); + } else if (ctx.subquery() != null) { + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); + } else if (ctx.collection_valued_input_parameter() != null) { + builder.append(visit(ctx.collection_valued_input_parameter())); } return builder; } @Override - public QueryTokenStream visitArithmeticComparison(EqlParser.ArithmeticComparisonContext ctx) { + public QueryTokenStream visitExists_expression(EqlParser.Exists_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.appendInline(visit(ctx.comparison_operator())); - - if (ctx.arithmetic_expression(1) != null) { - builder.appendExpression(visit(ctx.arithmetic_expression(1))); - } else { - builder.appendExpression(visit(ctx.all_or_any_expression())); + if (ctx.NOT() != null) { + builder.append(QueryTokens.expression(ctx.NOT())); } - return builder; - } - - @Override - public QueryTokenStream visitEntityTypeComparison(EqlParser.EntityTypeComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.entity_type_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.appendExpression(visit(ctx.entity_type_expression(1))); + builder.append(QueryTokens.expression(ctx.EXISTS())); + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); return builder; } @Override - public QueryTokenStream visitRegexpComparison(EqlParser.RegexpComparisonContext ctx) { + public QueryTokenStream visitAll_or_any_expression(EqlParser.All_or_any_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.string_expression())); - builder.append(QueryTokens.expression(ctx.REGEXP())); - builder.appendExpression(visit(ctx.string_literal())); - - return builder; - } - - @Override - public QueryTokenStream visitComparison_operator(EqlParser.Comparison_operatorContext ctx) { - return QueryTokenStream.from(QueryTokens.ventilated(ctx.op)); - } - - @Override - public QueryTokenStream visitArithmetic_expression(EqlParser.Arithmetic_expressionContext ctx) { - - if (ctx.arithmetic_expression() != null) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.arithmetic_expression())); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.append(visit(ctx.arithmetic_term())); - return builder; - - } else { - return visit(ctx.arithmetic_term()); + if (ctx.ALL() != null) { + builder.append(QueryTokens.expression(ctx.ALL())); + } else if (ctx.ANY() != null) { + builder.append(QueryTokens.expression(ctx.ANY())); + } else if (ctx.SOME() != null) { + builder.append(QueryTokens.expression(ctx.SOME())); } - } - @Override - public QueryTokenStream visitArithmetic_term(EqlParser.Arithmetic_termContext ctx) { + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); - 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 { - return visit(ctx.arithmetic_factor()); - } + return builder; } @Override @@ -1629,194 +646,53 @@ public QueryTokenStream visitArithmetic_factor(EqlParser.Arithmetic_factorContex @Override public QueryTokenStream visitArithmetic_primary(EqlParser.Arithmetic_primaryContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.numeric_literal() != null) { - builder.append(visit(ctx.numeric_literal())); - } else if (ctx.arithmetic_expression() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression())); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.functions_returning_numerics() != null) { - builder.append(visit(ctx.functions_returning_numerics())); - } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.arithmetic_cast_function() != null) { - builder.append(visit(ctx.arithmetic_cast_function())); - } else if (ctx.type_cast_function() != null) { - builder.append(visit(ctx.type_cast_function())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); + if (ctx.arithmetic_expression() != null) { + return QueryTokenStream.group(visit(ctx.arithmetic_expression())); } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitArithmetic_primary(ctx); } @Override public QueryTokenStream visitString_expression(EqlParser.String_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.string_literal() != null) { - builder.append(visit(ctx.string_literal())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.functions_returning_strings() != null) { - builder.append(visit(ctx.functions_returning_strings())); - } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.string_cast_function() != null) { - builder.append(visit(ctx.string_cast_function())); - } else if (ctx.type_cast_function() != null) { - builder.append(visit(ctx.type_cast_function())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); - } else if (ctx.subquery() != null) { - - 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))); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitString_expression(ctx); } @Override public QueryTokenStream visitDatetime_expression(EqlParser.Datetime_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.functions_returning_datetime() != null) { - builder.append(visit(ctx.functions_returning_datetime())); - } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); - } else if (ctx.date_time_timestamp_literal() != null) { - builder.append(visit(ctx.date_time_timestamp_literal())); - } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitDatetime_expression(ctx); } @Override public QueryTokenStream visitBoolean_expression(EqlParser.Boolean_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.boolean_literal() != null) { - builder.append(visit(ctx.boolean_literal())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); - } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitBoolean_expression(ctx); } @Override public QueryTokenStream visitEnum_expression(EqlParser.Enum_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.enum_literal() != null) { - builder.append(visit(ctx.enum_literal())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitEntity_expression(EqlParser.Entity_expressionContext ctx) { - - if (ctx.single_valued_object_path_expression() != null) { - return visit(ctx.single_valued_object_path_expression()); - } else if (ctx.simple_entity_expression() != null) { - return visit(ctx.simple_entity_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSimple_entity_expression(EqlParser.Simple_entity_expressionContext ctx) { - - if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitEntity_type_expression(EqlParser.Entity_type_expressionContext ctx) { - - if (ctx.type_discriminator() != null) { - return visit(ctx.type_discriminator()); - } else if (ctx.entity_type_literal() != null) { - return visit(ctx.entity_type_literal()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return QueryTokenStream.empty(); + return super.visitEnum_expression(ctx); } @Override @@ -1824,19 +700,15 @@ public QueryTokenStream visitType_discriminator(EqlParser.Type_discriminatorCont QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.TYPE())); - builder.append(TOKEN_OPEN_PAREN); - if (ctx.general_identification_variable() != null) { - builder.appendInline(visit(ctx.general_identification_variable())); + builder.append(visit(ctx.general_identification_variable())); } else if (ctx.single_valued_object_path_expression() != null) { - builder.appendInline(visit(ctx.single_valued_object_path_expression())); + builder.append(visit(ctx.single_valued_object_path_expression())); } else if (ctx.input_parameter() != null) { - builder.appendInline(visit(ctx.input_parameter())); + builder.append(visit(ctx.input_parameter())); } - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.TYPE(), builder); } @Override @@ -1845,132 +717,60 @@ public QueryTokenStream visitFunctions_returning_numerics(EqlParser.Functions_re QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.LENGTH() != null) { - - builder.append(QueryTokens.token(ctx.LENGTH())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.LENGTH(), visit(ctx.string_expression(0))); } else if (ctx.LOCATE() != null) { - builder.append(QueryTokens.token(ctx.LOCATE())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.string_expression(1))); + if (ctx.arithmetic_expression() != null) { builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(0))); } - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.ABS() != null) { - builder.append(QueryTokens.token(ctx.ABS())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.LOCATE(), builder); + } else if (ctx.ABS() != null) { + return QueryTokenStream.ofFunction(ctx.ABS(), visit(ctx.arithmetic_expression(0))); } else if (ctx.CEILING() != null) { - - builder.append(QueryTokens.token(ctx.CEILING())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.CEILING(), visit(ctx.arithmetic_expression(0))); } else if (ctx.EXP() != null) { - - builder.append(QueryTokens.token(ctx.EXP())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.EXP(), visit(ctx.arithmetic_expression(0))); } else if (ctx.FLOOR() != null) { - - builder.append(QueryTokens.token(ctx.FLOOR())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.FLOOR(), visit(ctx.arithmetic_expression(0))); } else if (ctx.LN() != null) { - - builder.append(QueryTokens.token(ctx.LN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.LN(), visit(ctx.arithmetic_expression(0))); } else if (ctx.SIGN() != null) { - - builder.append(QueryTokens.token(ctx.SIGN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.SIGN(), visit(ctx.arithmetic_expression(0))); } else if (ctx.SQRT() != null) { - - builder.append(QueryTokens.token(ctx.SQRT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.SQRT(), visit(ctx.arithmetic_expression(0))); } else if (ctx.MOD() != null) { - builder.append(QueryTokens.token(ctx.MOD())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); + + return QueryTokenStream.ofFunction(ctx.MOD(), builder); } else if (ctx.POWER() != null) { - builder.append(QueryTokens.token(ctx.POWER())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); + + return QueryTokenStream.ofFunction(ctx.POWER(), builder); } else if (ctx.ROUND() != null) { - builder.append(QueryTokens.token(ctx.ROUND())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.SIZE() != null) { - builder.append(QueryTokens.token(ctx.SIZE())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.collection_valued_path_expression())); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.ROUND(), builder); + } else if (ctx.SIZE() != null) { + return QueryTokenStream.ofFunction(ctx.SIZE(), visit(ctx.collection_valued_path_expression())); } else if (ctx.INDEX() != null) { - - builder.append(QueryTokens.token(ctx.INDEX())); - 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; - } - - @Override - public QueryTokenStream visitFunctions_returning_datetime(EqlParser.Functions_returning_datetimeContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT_DATE() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT_DATE())); - } else if (ctx.CURRENT_TIME() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT_TIME())); - } else if (ctx.CURRENT_TIMESTAMP() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT_TIMESTAMP())); - } else if (ctx.LOCAL() != null) { - - builder.append(QueryTokens.expression(ctx.LOCAL())); - - if (ctx.DATE() != null) { - builder.append(QueryTokens.expression(ctx.DATE())); - } else if (ctx.TIME() != null) { - builder.append(QueryTokens.expression(ctx.TIME())); - } 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 QueryTokenStream.ofFunction(ctx.INDEX(), visit(ctx.identification_variable())); + } else if (ctx.extract_datetime_field() != null) { + builder.append(visit(ctx.extract_datetime_field())); } return builder; @@ -1982,104 +782,70 @@ public QueryTokenStream visitFunctions_returning_strings(EqlParser.Functions_ret QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.CONCAT() != null) { - - builder.append(QueryTokens.token(ctx.CONCAT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.CONCAT(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); } else if (ctx.SUBSTRING() != null) { - 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) { - builder.append(QueryTokens.token(ctx.TRIM())); - builder.append(TOKEN_OPEN_PAREN); + return QueryTokenStream.ofFunction(ctx.SUBSTRING(), builder); + } else if (ctx.TRIM() != null) { - QueryRendererBuilder nested = QueryRenderer.builder(); if (ctx.trim_specification() != null) { - nested.appendExpression(visit(ctx.trim_specification())); + builder.appendExpression(visit(ctx.trim_specification())); } if (ctx.trim_character() != null) { - nested.appendExpression(visit(ctx.trim_character())); + builder.appendExpression(visit(ctx.trim_character())); } if (ctx.FROM() != null) { - nested.append(QueryTokens.expression(ctx.FROM())); + builder.append(QueryTokens.expression(ctx.FROM())); } - nested.append(visit(ctx.string_expression(0))); - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.LOWER() != null) { - builder.append(QueryTokens.token(ctx.LOWER())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(visit(ctx.string_expression(0))); + + return QueryTokenStream.ofFunction(ctx.TRIM(), builder); + } else if (ctx.LOWER() != null) { + return QueryTokenStream.ofFunction(ctx.LOWER(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); } else if (ctx.UPPER() != null) { + return QueryTokenStream.ofFunction(ctx.UPPER(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); + } else if (ctx.LEFT() != null) { - builder.append(QueryTokens.token(ctx.UPPER())); - 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); + builder.append(visit(ctx.arithmetic_expression(0))); + + return QueryTokenStream.ofFunction(ctx.LEFT(), builder); } 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); + builder.append(visit(ctx.arithmetic_expression(0))); + + return QueryTokenStream.ofFunction(ctx.RIGHT(), builder); } 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 QueryTokenStream.ofFunction(ctx.REPLACE(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); } return builder; } - @Override - public QueryTokenStream visitTrim_specification(EqlParser.Trim_specificationContext ctx) { - - if (ctx.LEADING() != null) { - return QueryTokenStream.ofToken(ctx.LEADING()); - } else if (ctx.TRAILING() != null) { - return QueryTokenStream.ofToken(ctx.TRAILING()); - } else { - return QueryTokenStream.ofToken(ctx.BOTH()); - } - } - @Override public QueryTokenStream visitArithmetic_cast_function(EqlParser.Arithmetic_cast_functionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.string_expression())); if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } builder.append(QueryTokens.token(ctx.f)); - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.CAST(), builder); } @Override @@ -2087,8 +853,6 @@ public QueryTokenStream visitType_cast_function(EqlParser.Type_cast_functionCont QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); if (ctx.AS() != null) { @@ -2103,9 +867,8 @@ public QueryTokenStream visitType_cast_function(EqlParser.Type_cast_functionCont builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); builder.append(TOKEN_CLOSE_PAREN); } - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.CAST(), builder); } @Override @@ -2113,16 +876,13 @@ public QueryTokenStream visitString_cast_function(EqlParser.String_cast_function QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } builder.append(QueryTokens.token(ctx.STRING())); - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.CAST(), builder); } @Override @@ -2150,231 +910,39 @@ public QueryTokenStream visitFunction_invocation(EqlParser.Function_invocationCo @Override 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.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitDatetime_field(EqlParser.Datetime_fieldContext ctx) { - return visit(ctx.identification_variable()); + return QueryTokenStream.ofFunction(ctx.EXTRACT(), nested); } @Override 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.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitDatetime_part(EqlParser.Datetime_partContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitFunction_arg(EqlParser.Function_argContext ctx) { - - if (ctx.literal() != null) { - return visit(ctx.literal()); - } else if (ctx.state_valued_path_expression() != null) { - return visit(ctx.state_valued_path_expression()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } else { - return visit(ctx.scalar_expression()); - } - } - - @Override - public QueryTokenStream visitCase_expression(EqlParser.Case_expressionContext ctx) { - - if (ctx.general_case_expression() != null) { - return visit(ctx.general_case_expression()); - } else if (ctx.simple_case_expression() != null) { - return visit(ctx.simple_case_expression()); - } else if (ctx.coalesce_expression() != null) { - return visit(ctx.coalesce_expression()); - } else { - return visit(ctx.nullif_expression()); - } - } - - @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) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CASE())); - builder.appendExpression(QueryTokenStream.concat(ctx.when_clause(), this::visit, TOKEN_SPACE)); - - builder.append(QueryTokens.expression(ctx.ELSE())); - builder.appendExpression(visit(ctx.scalar_expression())); - builder.append(QueryTokens.expression(ctx.END())); - - return builder; - } - - @Override - public QueryTokenStream visitWhen_clause(EqlParser.When_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.conditional_expression())); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.scalar_expression())); - - return builder; - } - - @Override - public QueryTokenStream visitSimple_case_expression(EqlParser.Simple_case_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CASE())); - 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.appendExpression(visit(ctx.scalar_expression())); - builder.append(QueryTokens.expression(ctx.END())); - - return builder; - } - - @Override - public QueryTokenStream visitCase_operand(EqlParser.Case_operandContext ctx) { - - if (ctx.state_valued_path_expression() != null) { - return visit(ctx.state_valued_path_expression()); - } else { - return visit(ctx.type_discriminator()); - } - } - - @Override - public QueryTokenStream visitSimple_when_clause(EqlParser.Simple_when_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.scalar_expression(0))); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.scalar_expression(1))); - - return builder; + return QueryTokenStream.ofFunction(ctx.EXTRACT(), nested); } @Override public QueryTokenStream visitCoalesce_expression(EqlParser.Coalesce_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.COALESCE())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(QueryTokenStream.concat(ctx.scalar_expression(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; + return QueryTokenStream.ofFunction(ctx.COALESCE(), + QueryTokenStream.concat(ctx.scalar_expression(), this::visit, TOKEN_COMMA)); } @Override public QueryTokenStream visitNullif_expression(EqlParser.Nullif_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.NULLIF())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.scalar_expression(0))); - builder.append(TOKEN_COMMA); - builder.appendInline(visit(ctx.scalar_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitTrim_character(EqlParser.Trim_characterContext ctx) { - - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.character_valued_input_parameter() != null) { - return visit(ctx.character_valued_input_parameter()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitIdentification_variable(EqlParser.Identification_variableContext ctx) { - - if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); - } else if (ctx.type_literal() != null) { - return visit(ctx.type_literal()); - } else if (ctx.f != null) { - return QueryTokenStream.ofToken(ctx.f); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitConstructor_name(EqlParser.Constructor_nameContext ctx) { - return visit(ctx.entity_name()); - } - - @Override - public QueryTokenStream visitLiteral(EqlParser.LiteralContext ctx) { - - if (ctx.STRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); - } else if (ctx.JAVASTRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.JAVASTRINGLITERAL()); - } else if (ctx.INTLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTLITERAL()); - } else if (ctx.FLOATLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); - } else if (ctx.LONGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.LONGLITERAL()); - } else if (ctx.boolean_literal() != null) { - return visit(ctx.boolean_literal()); - } else if (ctx.entity_type_literal() != null) { - return visit(ctx.entity_type_literal()); - } - - return QueryTokenStream.empty(); + return QueryTokenStream.ofFunction(ctx.NULLIF(), + QueryTokenStream.concat(ctx.scalar_expression(), this::visit, TOKEN_COMMA)); } @Override @@ -2395,192 +963,25 @@ public QueryTokenStream visitInput_parameter(EqlParser.Input_parameterContext ct return builder; } - @Override - public QueryTokenStream visitPattern_value(EqlParser.Pattern_valueContext ctx) { - return visit(ctx.string_expression()); - } - - @Override - public QueryTokenStream visitDate_time_timestamp_literal(EqlParser.Date_time_timestamp_literalContext ctx) { - - if (ctx.STRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); - } else if (ctx.DATELITERAL() != null) { - return QueryTokenStream.ofToken(ctx.DATELITERAL()); - } else if (ctx.TIMELITERAL() != null) { - return QueryTokenStream.ofToken(ctx.TIMELITERAL()); - } else if (ctx.TIMESTAMPLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.TIMESTAMPLITERAL()); - } else { - return QueryRenderer.builder(); - } - } - - @Override - public QueryTokenStream visitEntity_type_literal(EqlParser.Entity_type_literalContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitEscape_character(EqlParser.Escape_characterContext ctx) { - - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.character_valued_input_parameter() != null) { - return visit(ctx.character_valued_input_parameter()); - } else if (ctx.string_literal() != null) { - return visit(ctx.string_literal()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitNumeric_literal(EqlParser.Numeric_literalContext ctx) { - - if (ctx.INTLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTLITERAL()); - } else if (ctx.FLOATLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); - } else if (ctx.LONGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.LONGLITERAL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitBoolean_literal(EqlParser.Boolean_literalContext ctx) { - - if (ctx.TRUE() != null) { - return QueryTokenStream.ofToken(ctx.TRUE()); - } else if (ctx.FALSE() != null) { - return QueryTokenStream.ofToken(ctx.FALSE()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitEnum_literal(EqlParser.Enum_literalContext ctx) { - return visit(ctx.state_field_path_expression()); - } - - @Override - public QueryTokenStream visitString_literal(EqlParser.String_literalContext ctx) { - - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.STRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitSingle_valued_embeddable_object_field( - EqlParser.Single_valued_embeddable_object_fieldContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitSubtype(EqlParser.SubtypeContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitCollection_valued_field(EqlParser.Collection_valued_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitSingle_valued_object_field(EqlParser.Single_valued_object_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitState_field(EqlParser.State_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitCollection_value_field(EqlParser.Collection_value_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - @Override public QueryTokenStream visitEntity_name(EqlParser.Entity_nameContext ctx) { return QueryTokenStream.concat(ctx.reserved_word(), this::visit, TOKEN_DOT); } @Override - public QueryTokenStream visitResult_variable(EqlParser.Result_variableContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitSuperquery_identification_variable( - EqlParser.Superquery_identification_variableContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitCollection_valued_input_parameter( - EqlParser.Collection_valued_input_parameterContext ctx) { - return visit(ctx.input_parameter()); - } - - @Override - public QueryTokenStream visitSingle_valued_input_parameter(EqlParser.Single_valued_input_parameterContext ctx) { - return visit(ctx.input_parameter()); - } - - @Override - public QueryTokenStream visitFunction_name(EqlParser.Function_nameContext ctx) { - return visit(ctx.string_literal()); - } + public QueryTokenStream visitChildren(RuleNode node) { - @Override - public QueryTokenStream visitCharacter_valued_input_parameter(EqlParser.Character_valued_input_parameterContext ctx) { + int childCount = node.getChildCount(); - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } else { - return QueryTokenStream.empty(); + if (childCount == 1 && node.getChild(0) instanceof RuleContext t) { + return visit(t); } - } - @Override - public QueryTokenStream visitReserved_word(EqlParser.Reserved_wordContext ctx) { - if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); - } else if (ctx.f != null) { - return QueryTokenStream.ofToken(ctx.f); - } else { - return QueryTokenStream.empty(); + if (childCount == 1 && node.getChild(0) instanceof TerminalNode t) { + return QueryTokens.token(t); } + + return QueryTokenStream.concatExpressions(node, this::visit); } + } 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 00bcde0c7e..2949dfbc02 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 @@ -21,12 +21,13 @@ import java.util.List; import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RuleContext; import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; import org.antlr.v4.runtime.tree.TerminalNode; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. @@ -34,6 +35,7 @@ * @author Greg Turnquist * @author Christoph Strobl * @author Oscar Fanchin + * @author Mark Paluch * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode", "UnreachableCode" }) @@ -83,51 +85,11 @@ public QueryTokenStream visitStart(HqlParser.StartContext ctx) { return visit(ctx.ql_statement()); } - @Override - public QueryTokenStream visitQl_statement(HqlParser.Ql_statementContext ctx) { - - if (ctx.selectStatement() != null) { - return visit(ctx.selectStatement()); - } else if (ctx.updateStatement() != null) { - return visit(ctx.updateStatement()); - } else if (ctx.deleteStatement() != null) { - return visit(ctx.deleteStatement()); - } else if (ctx.insertStatement() != null) { - return visit(ctx.insertStatement()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitSelectStatement(HqlParser.SelectStatementContext ctx) { - return visit(ctx.queryExpression()); - } - - @Override - public QueryTokenStream visitQueryExpression(HqlParser.QueryExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.withClause() != null) { - builder.appendExpression(visit(ctx.withClause())); - } - - builder.append(visit(ctx.orderedQuery(0))); - - for (int i = 1; i < ctx.orderedQuery().size(); i++) { - - builder.append(visit(ctx.setOperator(i - 1))); - builder.append(visit(ctx.orderedQuery(i))); - } - - return builder; - } - @Override public QueryTokenStream visitWithClause(HqlParser.WithClauseContext ctx) { - QueryRendererBuilder builder = QueryRendererBuilder.builder(TOKEN_WITH); + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.WITH())); builder.append(QueryTokenStream.concatExpressions(ctx.cte(), this::visit, TOKEN_COMMA)); return builder; @@ -164,78 +126,6 @@ public QueryTokenStream visitCte(HqlParser.CteContext ctx) { return builder; } - @Override - public QueryTokenStream visitSearchClause(HqlParser.SearchClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.SEARCH())); - - if (ctx.BREADTH() != null) { - builder.append(QueryTokens.expression(ctx.BREADTH())); - } else if (ctx.DEPTH() != null) { - builder.append(QueryTokens.expression(ctx.DEPTH())); - } - - builder.append(QueryTokens.expression(ctx.FIRST())); - builder.append(QueryTokens.expression(ctx.BY())); - builder.append(visit(ctx.searchSpecifications())); - builder.append(QueryTokens.expression(ctx.SET())); - builder.append(visit(ctx.identifier())); - - return builder; - } - - @Override - public QueryTokenStream visitSearchSpecifications(HqlParser.SearchSpecificationsContext ctx) { - return QueryTokenStream.concat(ctx.searchSpecification(), this::visit, TOKEN_COMMA); - } - - @Override - public QueryTokenStream visitSearchSpecification(HqlParser.SearchSpecificationContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.identifier())); - - if (ctx.sortDirection() != null) { - builder.append(visit(ctx.sortDirection())); - } - - if (ctx.nullsPrecedence() != null) { - builder.append(visit(ctx.nullsPrecedence())); - } - - return builder; - } - - @Override - public QueryTokenStream visitCycleClause(HqlParser.CycleClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CYCLE().getText())); - builder.append(visit(ctx.cteAttributes())); - builder.append(QueryTokens.expression(ctx.SET().getText())); - builder.append(visit(ctx.identifier(0))); - - if (ctx.TO() != null) { - - builder.append(QueryTokens.expression(ctx.TO().getText())); - builder.append(visit(ctx.literal(0))); - builder.append(QueryTokens.expression(ctx.DEFAULT().getText())); - builder.append(visit(ctx.literal(1))); - } - - if (ctx.USING() != null) { - - builder.append(QueryTokens.expression(ctx.USING().getText())); - builder.append(visit(ctx.identifier(1))); - } - - return builder; - } - @Override public QueryTokenStream visitCteAttributes(HqlParser.CteAttributesContext ctx) { return QueryTokenStream.concat(ctx.identifier(), this::visit, TOKEN_COMMA); @@ -274,65 +164,6 @@ public QueryTokenStream visitOrderedQuery(HqlParser.OrderedQueryContext ctx) { return builder; } - @Override - public QueryTokenStream visitSelectQuery(HqlParser.SelectQueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.selectClause() != null) { - builder.appendExpression(visit(ctx.selectClause())); - } - - if (ctx.fromClause() != null) { - builder.appendExpression(visit(ctx.fromClause())); - } - - if (ctx.whereClause() != null) { - builder.appendExpression(visit(ctx.whereClause())); - } - - if (ctx.groupByClause() != null) { - builder.appendExpression(visit(ctx.groupByClause())); - } - - if (ctx.havingClause() != null) { - builder.appendExpression(visit(ctx.havingClause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFromQuery(HqlParser.FromQueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.fromClause())); - - if (ctx.whereClause() != null) { - builder.append(visit(ctx.whereClause())); - } - - if (ctx.groupByClause() != null) { - builder.append(visit(ctx.groupByClause())); - } - - if (ctx.havingClause() != null) { - builder.append(visit(ctx.havingClause())); - } - - if (ctx.selectClause() != null) { - builder.append(visit(ctx.selectClause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitQueryOrder(HqlParser.QueryOrderContext ctx) { - return visit(ctx.orderByClause()); - } - @Override public QueryTokenStream visitFromClause(HqlParser.FromClauseContext ctx) { @@ -355,34 +186,6 @@ public QueryTokenStream visitEntityWithJoins(HqlParser.EntityWithJoinsContext ct return builder; } - @Override - public QueryTokenStream visitJoinSpecifier(HqlParser.JoinSpecifierContext ctx) { - - if (ctx.join() != null) { - return visit(ctx.join()); - } else if (ctx.crossJoin() != null) { - return visit(ctx.crossJoin()); - } else if (ctx.jpaCollectionJoin() != null) { - return visit(ctx.jpaCollectionJoin()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitRootEntity(HqlParser.RootEntityContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.entityName())); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } - - return builder; - } - @Override public QueryTokenStream visitRootSubquery(HqlParser.RootSubqueryContext ctx) { @@ -392,27 +195,7 @@ public QueryTokenStream visitRootSubquery(HqlParser.RootSubqueryContext ctx) { builder.append(QueryTokens.expression(ctx.LATERAL())); } - QueryRendererBuilder nested = QueryRenderer.builder(); - - nested.append(TOKEN_OPEN_PAREN); - nested.appendInline(visit(ctx.subquery())); - nested.append(TOKEN_CLOSE_PAREN); - - builder.appendExpression(nested); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } - - return builder; - } - - @Override - public QueryTokenStream visitRootFunction(HqlParser.RootFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.setReturningFunction())); + builder.appendExpression(QueryTokenStream.group(visit(ctx.subquery()))); if (ctx.variable() != null) { builder.appendExpression(visit(ctx.variable())); @@ -421,20 +204,6 @@ public QueryTokenStream visitRootFunction(HqlParser.RootFunctionContext ctx) { return builder; } - @Override - public QueryTokenStream visitSetReturningFunction(HqlParser.SetReturningFunctionContext ctx) { - - if (ctx.simpleSetReturningFunction() != null) { - return visit(ctx.simpleSetReturningFunction()); - } else if (ctx.jsonTableFunction() != null) { - return visit(ctx.jsonTableFunction()); - } else if (ctx.xmlTableFunction() != null) { - return visit(ctx.xmlTableFunction()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitSimpleSetReturningFunction(HqlParser.SimpleSetReturningFunctionContext ctx) { @@ -473,23 +242,6 @@ public QueryTokenStream visitJoin(HqlParser.JoinContext ctx) { return builder; } - - @Override - public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { - - HqlParser.VariableContext variable = ctx.variable(); - - if (variable == null) { - return visit(ctx.path()); - } - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.path())); - builder.appendExpression(visit(variable)); - - return builder; - } - @Override public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { @@ -499,9 +251,7 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { builder.append(QueryTokens.expression(ctx.LATERAL())); } - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); if (ctx.variable() != null) { builder.appendExpression(visit(ctx.variable())); @@ -511,3691 +261,1605 @@ public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) { } @Override - public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext ctx) { + public QueryTokenStream visitSetClause(HqlParser.SetClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.LATERAL() != null) { - builder.append(QueryTokens.expression(ctx.LATERAL())); - } + builder.append(QueryTokens.expression(ctx.SET())); + return builder.append(QueryTokenStream.concat(ctx.assignment(), this::visit, TOKEN_COMMA)); + } - builder.appendExpression(visit(ctx.setReturningFunction())); + @Override + public QueryTokenStream visitAssignment(HqlParser.AssignmentContext ctx) { - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); - } + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.simplePath())); + builder.append(TOKEN_EQUALS); + builder.append(visit(ctx.expressionOrPredicate())); return builder; } @Override - public QueryTokenStream visitUpdateStatement(HqlParser.UpdateStatementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.UPDATE())); + public QueryTokenStream visitTargetFields(HqlParser.TargetFieldsContext ctx) { + return QueryTokenStream.group(QueryTokenStream.concat(ctx.simplePath(), this::visit, TOKEN_COMMA)); + } - if (ctx.VERSIONED() != null) { - builder.append(QueryTokens.expression(ctx.VERSIONED())); - } + @Override + public QueryTokenStream visitValuesList(HqlParser.ValuesListContext ctx) { - builder.appendExpression(visit(ctx.targetEntity())); - builder.appendExpression(visit(ctx.setClause())); + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.whereClause() != null) { - builder.appendExpression(visit(ctx.whereClause())); - } + builder.append(QueryTokens.expression(ctx.VALUES())); + builder.append(QueryTokenStream.concat(ctx.values(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitTargetEntity(HqlParser.TargetEntityContext ctx) { + public QueryTokenStream visitValues(HqlParser.ValuesContext ctx) { + return QueryTokenStream.group(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + } - HqlParser.VariableContext variable = ctx.variable(); + @Override + public QueryTokenStream visitConflictTarget(HqlParser.ConflictTargetContext ctx) { - if (variable == null) { - return visit(ctx.entityName()); + if (ctx.identifier() != null) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.entityName())); - builder.appendExpression(visit(variable)); - - return builder; + return QueryTokenStream.group(QueryTokenStream.concat(ctx.simplePath(), this::visit, TOKEN_COMMA)); } @Override - public QueryTokenStream visitSetClause(HqlParser.SetClauseContext ctx) { + public QueryTokenStream visitInstantiation(HqlParser.InstantiationContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.SET())); - return builder.append(QueryTokenStream.concat(ctx.assignment(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokens.expression(ctx.NEW())); + builder.append(visit(ctx.instantiationTarget())); + builder.append(QueryTokenStream.group(visit(ctx.instantiationArguments()))); + + return builder; + } + public QueryTokenStream visitSelectionList(HqlParser.SelectionListContext ctx) { + return QueryTokenStream.concat(ctx.selection(), this::visit, TOKEN_COMMA); } @Override - public QueryTokenStream visitAssignment(HqlParser.AssignmentContext ctx) { + public QueryTokenStream visitMapEntrySelection(HqlParser.MapEntrySelectionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.simplePath())); - builder.append(TOKEN_EQUALS); - builder.append(visit(ctx.expressionOrPredicate())); + builder.append(QueryTokens.expression(ctx.ENTRY())); + builder.append(QueryTokenStream.group(visit(ctx.path()))); return builder; } @Override - public QueryTokenStream visitDeleteStatement(HqlParser.DeleteStatementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.DELETE())); + public QueryTokenStream visitJpaSelectObjectSyntax(HqlParser.JpaSelectObjectSyntaxContext ctx) { + return QueryTokenStream.ofFunction(ctx.OBJECT(), visit(ctx.identifier())); + } - if (ctx.FROM() != null) { - builder.append(QueryTokens.expression(ctx.FROM())); - } + @Override + public QueryTokenStream visitWhereClause(HqlParser.WhereClauseContext ctx) { - builder.append(visit(ctx.targetEntity())); + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.whereClause() != null) { - builder.append(visit(ctx.whereClause())); - } + builder.append(QueryTokens.expression(ctx.WHERE())); + builder.append(QueryTokenStream.concatExpressions(ctx.predicate(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitInsertStatement(HqlParser.InsertStatementContext ctx) { + public QueryTokenStream visitJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.INSERT())); - - if (ctx.INTO() != null) { - builder.append(QueryTokens.expression(ctx.INTO())); - } - - builder.appendExpression(visit(ctx.targetEntity())); - builder.appendExpression(visit(ctx.targetFields())); - - if (ctx.queryExpression() != null) { - builder.appendExpression(visit(ctx.queryExpression())); - } else if (ctx.valuesList() != null) { - builder.appendExpression(visit(ctx.valuesList())); - } + builder.append(TOKEN_COMMA); + builder.append(QueryTokens.token(ctx.IN())); + builder.append(QueryTokenStream.group(visit(ctx.path()))); - if (ctx.conflictClause() != null) { - builder.appendExpression(visit(ctx.conflictClause())); + if (ctx.variable() != null) { + builder.appendExpression(visit(ctx.variable())); } return builder; } @Override - public QueryTokenStream visitTargetFields(HqlParser.TargetFieldsContext ctx) { + public QueryTokenStream visitGroupByClause(HqlParser.GroupByClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_OPEN_PAREN); - builder.append(QueryTokenStream.concat(ctx.simplePath(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokens.expression(ctx.GROUP())); + builder.append(QueryTokens.expression(ctx.BY())); + builder.append(QueryTokenStream.concat(ctx.groupedItem(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitValuesList(HqlParser.ValuesListContext ctx) { + public QueryTokenStream visitOrderByClause(HqlParser.OrderByClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.VALUES())); - builder.append(QueryTokenStream.concat(ctx.values(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokens.expression(ctx.ORDER())); + builder.append(QueryTokens.expression(ctx.BY())); + builder.appendExpression(QueryTokenStream.concat(ctx.sortedItem(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitValues(HqlParser.ValuesContext ctx) { + public QueryTokenStream visitHavingClause(HqlParser.HavingClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_OPEN_PAREN); - builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokens.expression(ctx.HAVING())); + builder.appendExpression(QueryTokenStream.concat(ctx.predicate(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitConflictClause(HqlParser.ConflictClauseContext ctx) { + public QueryTokenStream visitLocalDateTimeLiteral(HqlParser.LocalDateTimeLiteralContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.DATETIME() == null) { + return QueryTokenStream.group(visit(ctx.localDateTime())); + } - builder.append(QueryTokens.expression(ctx.ON())); - builder.append(QueryTokens.expression(ctx.CONFLICT())); + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } - if (ctx.conflictTarget() != null) { - builder.appendExpression(visit(ctx.conflictTarget())); - } + @Override + public QueryTokenStream visitZonedDateTimeLiteral(HqlParser.ZonedDateTimeLiteralContext ctx) { - builder.append(QueryTokens.expression(ctx.DO())); - builder.appendExpression(visit(ctx.conflictAction())); + if (ctx.DATETIME() == null) { + return QueryTokenStream.group(visit(ctx.zonedDateTime())); + } - return builder; + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitConflictTarget(HqlParser.ConflictTargetContext ctx) { + public QueryTokenStream visitOffsetDateTimeLiteral(HqlParser.OffsetDateTimeLiteralContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.DATETIME() == null) { + return QueryTokenStream.group(visit(ctx.offsetDateTime())); + } - if (ctx.identifier() != null) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } - builder.append(QueryTokens.expression(ctx.ON())); - builder.append(QueryTokens.expression(ctx.CONSTRAINT())); - builder.appendExpression(visit(ctx.identifier())); + @Override + public QueryTokenStream visitDateLiteral(HqlParser.DateLiteralContext ctx) { + + if (ctx.DATE() == null) { + return QueryTokenStream.group(visit(ctx.date())); } - if (!ObjectUtils.isEmpty(ctx.simplePath())) { + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } - builder.append(TOKEN_OPEN_PAREN); - builder.append(QueryTokenStream.concat(ctx.simplePath(), this::visit, TOKEN_COMMA)); + @Override + public QueryTokenStream visitTimeLiteral(HqlParser.TimeLiteralContext ctx) { - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.TIME() == null) { + return QueryTokenStream.group(visit(ctx.time())); } - return builder; + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitConflictAction(HqlParser.ConflictActionContext ctx) { + public QueryTokenStream visitOffsetDateTime(HqlParser.OffsetDateTimeContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.NOTHING() != null) { - builder.append(QueryTokens.expression(ctx.NOTHING())); - } else { - builder.append(QueryTokens.expression(ctx.UPDATE())); - builder.appendExpression(visit(ctx.setClause())); - - if (ctx.whereClause() != null) { - builder.appendExpression(visit(ctx.whereClause())); - } - } + builder.appendExpression(visit(ctx.date())); + builder.appendInline(visit(ctx.time())); + builder.appendInline(visit(ctx.offset())); return builder; } @Override - public QueryTokenStream visitInstantiation(HqlParser.InstantiationContext ctx) { + public QueryTokenStream visitOffsetDateTimeWithMinutes(HqlParser.OffsetDateTimeWithMinutesContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.NEW())); - builder.append(visit(ctx.instantiationTarget())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.instantiationArguments())); - builder.append(TOKEN_CLOSE_PAREN); + builder.appendExpression(visit(ctx.date())); + builder.appendInline(visit(ctx.time())); + builder.appendInline(visit(ctx.offsetWithMinutes())); return builder; } @Override - public QueryTokenStream visitGroupedItem(HqlParser.GroupedItemContext ctx) { + public QueryTokenStream visitJdbcTimestampLiteral(HqlParser.JdbcTimestampLiteralContext ctx) { - if (ctx.identifier() != null) { - return visit(ctx.identifier()); - } else if (ctx.INTEGER_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } else if (ctx.expression() != null) { - return visit(ctx.expression()); - } else { - return QueryTokenStream.empty(); - } + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(TOKEN_OPEN_BRACE); + builder.append(QueryTokens.token("ts")); + builder.appendInline(visit(ctx.dateTime() != null ? ctx.dateTime() : ctx.genericTemporalLiteralText())); + builder.append(QueryTokens.TOKEN_CLOSE_BRACE); + + return builder; } @Override - public QueryTokenStream visitSortedItem(HqlParser.SortedItemContext ctx) { + public QueryTokenStream visitJdbcDateLiteral(HqlParser.JdbcDateLiteralContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.sortExpression())); - - if (ctx.sortDirection() != null) { - builder.append(visit(ctx.sortDirection())); - } - - if (ctx.nullsPrecedence() != null) { - builder.appendExpression(visit(ctx.nullsPrecedence())); - } + builder.append(TOKEN_OPEN_BRACE); + builder.append(QueryTokens.token("d")); + builder.append(visit(ctx.date() != null ? ctx.date() : ctx.genericTemporalLiteralText())); + builder.append(QueryTokens.TOKEN_CLOSE_BRACE); return builder; } @Override - public QueryTokenStream visitSortExpression(HqlParser.SortExpressionContext ctx) { + public QueryTokenStream visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ctx) { - if (ctx.identifier() != null) { - return visit(ctx.identifier()); - } else if (ctx.INTEGER_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } else if (ctx.expression() != null) { - return visit(ctx.expression()); - } else { - return QueryTokenStream.empty(); - } - } + QueryRendererBuilder builder = QueryRenderer.builder(); - @Override - public QueryTokenStream visitSortDirection(HqlParser.SortDirectionContext ctx) { + builder.append(TOKEN_OPEN_BRACE); + builder.append(QueryTokens.token("t")); + builder.append(visit(ctx.time() != null ? ctx.time() : ctx.genericTemporalLiteralText())); + builder.append(QueryTokens.TOKEN_CLOSE_BRACE); - if (ctx.ASC() != null) { - return QueryTokenStream.ofToken(ctx.ASC()); - } else if (ctx.DESC() != null) { - return QueryTokenStream.ofToken(ctx.DESC()); - } else { - return QueryTokenStream.empty(); - } + return builder; } @Override - public QueryTokenStream visitNullsPrecedence(HqlParser.NullsPrecedenceContext ctx) { + public QueryTokenStream visitArrayLiteral(HqlParser.ArrayLiteralContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.NULLS())); - - if (ctx.FIRST() != null) { - builder.append(QueryTokens.expression(ctx.FIRST())); - } else if (ctx.LAST() != null) { - builder.append(QueryTokens.expression(ctx.LAST())); - } + builder.append(TOKEN_OPEN_SQUARE_BRACKET); + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + builder.append(TOKEN_CLOSE_SQUARE_BRACKET); return builder; } @Override - public QueryTokenStream visitLimitClause(HqlParser.LimitClauseContext ctx) { + public QueryTokenStream visitGeneralizedLiteral(HqlParser.GeneralizedLiteralContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.LIMIT())); - builder.append(visit(ctx.parameterOrIntegerLiteral())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.generalizedLiteralType())); + builder.append(TOKEN_COLON); + builder.append(visit(ctx.generalizedLiteralText())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitOffsetClause(HqlParser.OffsetClauseContext ctx) { + public QueryTokenStream visitDate(HqlParser.DateContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.OFFSET())); - builder.appendExpression(visit(ctx.parameterOrIntegerLiteral())); - - if (ctx.ROW() != null) { - builder.append(QueryTokens.expression(ctx.ROW())); - } else if (ctx.ROWS() != null) { - builder.append(QueryTokens.expression(ctx.ROWS())); - } + builder.append(visit(ctx.year())); + builder.append(TOKEN_DASH); + builder.append(visit(ctx.month())); + builder.append(TOKEN_DASH); + builder.append(visit(ctx.day())); return builder; } @Override - public QueryTokenStream visitFetchClause(HqlParser.FetchClauseContext ctx) { + public QueryTokenStream visitTime(HqlParser.TimeContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(visit(ctx.hour())); + builder.append(TOKEN_COLON); + builder.append(visit(ctx.minute())); - builder.append(QueryTokens.expression(ctx.FETCH())); - - if (ctx.FIRST() != null) { - builder.append(QueryTokens.expression(ctx.FIRST())); - } else if (ctx.NEXT() != null) { - builder.append(QueryTokens.expression(ctx.NEXT())); + if (ctx.second() != null) { + builder.append(TOKEN_COLON); + builder.append(visit(ctx.second())); } - if (ctx.parameterOrIntegerLiteral() != null) { - builder.appendExpression(visit(ctx.parameterOrIntegerLiteral())); - } else if (ctx.parameterOrNumberLiteral() != null) { - builder.appendExpression(visit(ctx.parameterOrNumberLiteral())); - } + return builder; + } - if (ctx.ROW() != null) { - builder.append(QueryTokens.expression(ctx.ROW())); - } else if (ctx.ROWS() != null) { - builder.append(QueryTokens.expression(ctx.ROWS())); - } + @Override + public QueryTokenStream visitOffset(HqlParser.OffsetContext ctx) { - if (ctx.ONLY() != null) { - builder.append(QueryTokens.expression(ctx.ONLY())); - } else if (ctx.WITH() != null) { + QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.MINUS() != null) { + builder.append(QueryTokens.token(ctx.MINUS())); + } else if (ctx.PLUS() != null) { + builder.append(QueryTokens.token(ctx.PLUS())); + } + builder.append(visit(ctx.hour())); - builder.append(QueryTokens.expression(ctx.WITH())); - builder.append(QueryTokens.expression(ctx.TIES())); + if (ctx.minute() != null) { + builder.append(TOKEN_COLON); + builder.append(visit(ctx.minute())); } return builder; } @Override - public QueryTokenStream visitSubquery(HqlParser.SubqueryContext ctx) { - return visit(ctx.queryExpression()); - } - - @Override - public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) { + public QueryTokenStream visitOffsetWithMinutes(HqlParser.OffsetWithMinutesContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.SELECT())); - - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (ctx.MINUS() != null) { + builder.append(QueryTokens.token(ctx.MINUS())); + } else if (ctx.PLUS() != null) { + builder.append(QueryTokens.token(ctx.PLUS())); } - builder.appendExpression(visit(ctx.selectionList())); + builder.append(visit(ctx.hour())); + builder.append(TOKEN_COLON); + builder.append(visit(ctx.minute())); return builder; } @Override - public QueryTokenStream visitSelectionList(HqlParser.SelectionListContext ctx) { - return QueryTokenStream.concat(ctx.selection(), this::visit, TOKEN_COMMA); - } - - @Override - public QueryTokenStream visitSelection(HqlParser.SelectionContext ctx) { + public QueryTokenStream visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.selectExpression())); + if (ctx.BINARY_LITERAL() != null) { + builder.append(QueryTokens.expression(ctx.BINARY_LITERAL())); + } else if (ctx.HEX_LITERAL() != null) { - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); + builder.append(TOKEN_OPEN_BRACE); + builder.append(QueryTokenStream.concat(ctx.HEX_LITERAL(), QueryTokens::token, TOKEN_COMMA)); + builder.append(TOKEN_CLOSE_BRACE); } - return builder.toInline(); - } - - @Override - public QueryTokenStream visitSelectExpression(HqlParser.SelectExpressionContext ctx) { - - if (ctx.instantiation() != null) { - return visit(ctx.instantiation()); - } else if (ctx.mapEntrySelection() != null) { - return visit(ctx.mapEntrySelection()); - } else if (ctx.jpaSelectObjectSyntax() != null) { - return visit(ctx.jpaSelectObjectSyntax()); - } else if (ctx.expressionOrPredicate() != null) { - return visit(ctx.expressionOrPredicate()); - } else { - return QueryTokenStream.empty(); - } + return builder; } @Override - public QueryTokenStream visitMapEntrySelection(HqlParser.MapEntrySelectionContext ctx) { + public QueryTokenStream visitTupleExpression(HqlParser.TupleExpressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.ENTRY())); builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); + builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitJpaSelectObjectSyntax(HqlParser.JpaSelectObjectSyntaxContext ctx) { + public QueryTokenStream visitHqlConcatenationExpression(HqlParser.HqlConcatenationExpressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.OBJECT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.identifier())); - builder.append(TOKEN_CLOSE_PAREN); + builder.appendInline(visit(ctx.expression(0))); + builder.append(TOKEN_DOUBLE_PIPE); + builder.append(visit(ctx.expression(1))); return builder; } @Override - public QueryTokenStream visitWhereClause(HqlParser.WhereClauseContext ctx) { + public QueryTokenStream visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) { + return QueryTokenStream.group(visit(ctx.expression())); + } + + @Override + public QueryTokenStream visitSignedNumericLiteral(HqlParser.SignedNumericLiteralContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.WHERE())); - builder.append(QueryTokenStream.concatExpressions(ctx.predicate(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokens.token(ctx.op)); + builder.append(visit(ctx.numericLiteral())); return builder; } @Override - public QueryTokenStream visitJoinType(HqlParser.JoinTypeContext ctx) { + public QueryTokenStream visitSubqueryExpression(HqlParser.SubqueryExpressionContext ctx) { + return QueryTokenStream.group(visit(ctx.subquery())); + } + + @Override + public QueryTokenStream visitSignedExpression(HqlParser.SignedExpressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.INNER() != null) { - builder.append(QueryTokens.expression(ctx.INNER())); - } - if (ctx.LEFT() != null) { - builder.append(QueryTokens.expression(ctx.LEFT())); - } - if (ctx.RIGHT() != null) { - builder.append(QueryTokens.expression(ctx.RIGHT())); - } - if (ctx.FULL() != null) { - builder.append(QueryTokens.expression(ctx.FULL())); - } - if (ctx.OUTER() != null) { - builder.append(QueryTokens.expression(ctx.OUTER())); - } - if (ctx.CROSS() != null) { - builder.append(QueryTokens.expression(ctx.CROSS())); - } + builder.append(QueryTokens.token(ctx.op)); + builder.appendInline(visit(ctx.expression())); return builder; } @Override - public QueryTokenStream visitCrossJoin(HqlParser.CrossJoinContext ctx) { + public QueryTokenStream visitSyntacticPathExpression(HqlParser.SyntacticPathExpressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.CROSS())); - builder.append(QueryTokens.expression(ctx.JOIN())); - builder.appendExpression(visit(ctx.entityName())); + builder.appendInline(visit(ctx.syntacticDomainPath())); - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); + if (ctx.pathContinuation() != null) { + builder.appendInline(visit(ctx.pathContinuation())); } return builder; } @Override - public QueryTokenStream visitJoinRestriction(HqlParser.JoinRestrictionContext ctx) { + public QueryTokenStream visitPathContinuation(HqlParser.PathContinuationContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.ON() != null) { - builder.append(QueryTokens.expression(ctx.ON())); - } else if (ctx.WITH() != null) { - builder.append(QueryTokens.expression(ctx.WITH())); - } - - builder.appendExpression(visit(ctx.predicate())); + builder.append(TOKEN_DOT); + builder.append(visit(ctx.simplePath())); return builder; } @Override - public QueryTokenStream visitJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx) { + public QueryTokenStream visitEntityTypeReference(HqlParser.EntityTypeReferenceContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_COMMA); - builder.append(QueryTokens.token(ctx.IN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.path() != null) { + builder.appendInline(visit(ctx.path())); + } - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); + if (ctx.parameter() != null) { + builder.appendInline(visit(ctx.parameter())); } - return builder; + return QueryTokenStream.ofFunction(ctx.TYPE(), builder); } @Override - public QueryTokenStream visitGroupByClause(HqlParser.GroupByClauseContext ctx) { + public QueryTokenStream visitEntityIdReference(HqlParser.EntityIdReferenceContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.GROUP())); - builder.append(QueryTokens.expression(ctx.BY())); - builder.append(QueryTokenStream.concat(ctx.groupedItem(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokens.token(ctx.ID())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.pathContinuation() != null) { + builder.appendInline(visit(ctx.pathContinuation())); + } return builder; } @Override - public QueryTokenStream visitOrderByClause(HqlParser.OrderByClauseContext ctx) { + public QueryTokenStream visitEntityVersionReference(HqlParser.EntityVersionReferenceContext ctx) { + return QueryTokenStream.ofFunction(ctx.VERSION(), visit(ctx.path())); + } + + @Override + public QueryTokenStream visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferenceContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.ORDER())); - builder.append(QueryTokens.expression(ctx.BY())); - builder.appendExpression(QueryTokenStream.concat(ctx.sortedItem(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokens.token(ctx.NATURALID())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.pathContinuation() != null) { + builder.appendInline(visit(ctx.pathContinuation())); + } return builder; } @Override - public QueryTokenStream visitHavingClause(HqlParser.HavingClauseContext ctx) { + public QueryTokenStream visitSyntacticDomainPath(HqlParser.SyntacticDomainPathContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.treatedNavigablePath() != null) { + return visit(ctx.treatedNavigablePath()); + } - builder.append(QueryTokens.expression(ctx.HAVING())); - builder.appendExpression(QueryTokenStream.concat(ctx.predicate(), this::visit, TOKEN_COMMA)); + if (ctx.collectionValueNavigablePath() != null) { + return visit(ctx.collectionValueNavigablePath()); + } - return builder; - } + if (ctx.mapKeyNavigablePath() != null) { + return visit(ctx.mapKeyNavigablePath()); + } - @Override - public QueryTokenStream visitSetOperator(HqlParser.SetOperatorContext ctx) { + if (ctx.toOneFkReference() != null) { + return visit(ctx.toOneFkReference()); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.function() != null) { - 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())); - } + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.ALL() != null) { - builder.append(QueryTokens.expression(ctx.ALL())); - } + builder.append(visit(ctx.function())); - return builder; - } + if (ctx.indexedPathAccessFragment() != null) { + builder.append(visit(ctx.indexedPathAccessFragment())); + } - @Override - public QueryTokenStream visitLiteral(HqlParser.LiteralContext ctx) { - - if (ctx.NULL() != null) { - return QueryTokenStream.ofToken(ctx.NULL()); - } else if (ctx.booleanLiteral() != null) { - return visit(ctx.booleanLiteral()); - } else if (ctx.JAVA_STRING_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.JAVA_STRING_LITERAL()); - } else if (ctx.STRING_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); - } else if (ctx.numericLiteral() != null) { - return visit(ctx.numericLiteral()); - } else if (ctx.temporalLiteral() != null) { - return visit(ctx.temporalLiteral()); - } else if (ctx.arrayLiteral() != null) { - return visit(ctx.arrayLiteral()); - } else if (ctx.generalizedLiteral() != null) { - return visit(ctx.generalizedLiteral()); - } else if (ctx.binaryLiteral() != null) { - return visit(ctx.binaryLiteral()); - } else { - return QueryTokenStream.empty(); - } - } + if (ctx.slicedPathAccessFragment() != null) { + builder.append(visit(ctx.slicedPathAccessFragment())); + } - @Override - public QueryTokenStream visitBooleanLiteral(HqlParser.BooleanLiteralContext ctx) { + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); + } - if (ctx.TRUE() != null) { - return QueryTokenStream.ofToken(ctx.TRUE()); - } else if (ctx.FALSE() != null) { - return QueryTokenStream.ofToken(ctx.FALSE()); - } else { - return QueryTokenStream.empty(); + return builder; } - } - @Override - public QueryTokenStream visitNumericLiteral(HqlParser.NumericLiteralContext ctx) { - - if (ctx.INTEGER_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } else if (ctx.LONG_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.LONG_LITERAL()); - } else if (ctx.BIG_INTEGER_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.BIG_INTEGER_LITERAL()); - } else if (ctx.FLOAT_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.FLOAT_LITERAL()); - } else if (ctx.DOUBLE_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.DOUBLE_LITERAL()); - } else if (ctx.BIG_DECIMAL_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.BIG_DECIMAL_LITERAL()); - } else if (ctx.HEX_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.HEX_LITERAL()); - } else { - return QueryTokenStream.empty(); - } - } + if (ctx.indexedPathAccessFragment() != null) { - @Override - public QueryTokenStream visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.localDateTimeLiteral() != null) { - return visit(ctx.localDateTimeLiteral()); - } + builder.append(visit(ctx.simplePath())); + builder.append(visit(ctx.indexedPathAccessFragment())); - if (ctx.offsetDateTimeLiteral() != null) { - return visit(ctx.offsetDateTimeLiteral()); + return builder; } - if (ctx.zonedDateTimeLiteral() != null) { - return visit(ctx.zonedDateTimeLiteral()); + if (ctx.slicedPathAccessFragment() != null) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(visit(ctx.simplePath())); + builder.append(visit(ctx.slicedPathAccessFragment())); + + return builder; } - return QueryTokenStream.empty(); + return QueryRenderer.empty(); } @Override - public QueryTokenStream visitLocalDateTimeLiteral(HqlParser.LocalDateTimeLiteralContext ctx) { + public QueryTokenStream visitSlicedPathAccessFragment(HqlParser.SlicedPathAccessFragmentContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.DATETIME() != null) { - if (ctx.LOCAL() != null) { - builder.append(QueryTokens.expression(ctx.LOCAL())); - } - builder.append(QueryTokens.expression(ctx.DATETIME())); - builder.append(visit(ctx.localDateTime())); - } else { - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.localDateTime())); - builder.append(TOKEN_CLOSE_PAREN); - } + builder.append(TOKEN_OPEN_SQUARE_BRACKET); + builder.appendInline(visit(ctx.expression(0))); + builder.append(TOKEN_COLON); + builder.appendInline(visit(ctx.expression(1))); + builder.append(TOKEN_CLOSE_SQUARE_BRACKET); return builder; } @Override - public QueryTokenStream visitZonedDateTimeLiteral(HqlParser.ZonedDateTimeLiteralContext ctx) { + public QueryTokenStream visitSubstringFunction(HqlParser.SubstringFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.DATETIME() != null) { - if (ctx.ZONED() != null) { - builder.append(QueryTokens.expression(ctx.ZONED())); + if (ctx.FROM() == null) { + builder.appendInline(visit(ctx.expression())); + builder.append(TOKEN_COMMA); + } else { + builder.appendExpression(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.FROM())); + } + + if (ctx.substringFunctionLengthArgument() != null) { + + if (ctx.FOR() == null) { + builder.appendInline(visit(ctx.substringFunctionStartArgument())); + builder.append(TOKEN_COMMA); + } else { + builder.appendExpression(visit(ctx.substringFunctionStartArgument())); + builder.append(QueryTokens.expression(ctx.FOR())); } - builder.append(QueryTokens.expression(ctx.DATETIME())); - builder.append(visit(ctx.zonedDateTime())); + + builder.append(visit(ctx.substringFunctionLengthArgument())); } else { - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.zonedDateTime())); - builder.append(TOKEN_CLOSE_PAREN); + builder.appendExpression(visit(ctx.substringFunctionStartArgument())); } - return builder; + return QueryTokenStream.ofFunction(ctx.SUBSTRING(), builder); } @Override - public QueryTokenStream visitOffsetDateTimeLiteral(HqlParser.OffsetDateTimeLiteralContext ctx) { + public QueryTokenStream visitPadFunction(HqlParser.PadFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.DATETIME() != null) { - if (ctx.OFFSET() != null) { - builder.append(QueryTokens.expression(ctx.OFFSET())); - } - builder.append(QueryTokens.expression(ctx.DATETIME())); - builder.append(visit(ctx.offsetDateTimeWithMinutes())); + builder.appendExpression(visit(ctx.expression())); + builder.append(QueryTokens.expression(ctx.WITH())); + builder.appendExpression(visit(ctx.padLength())); + + if (ctx.padCharacter() != null) { + builder.appendExpression(visit(ctx.padSpecification())); + builder.appendExpression(visit(ctx.padCharacter())); } else { - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.offsetDateTime())); - builder.append(TOKEN_CLOSE_PAREN); + builder.appendExpression(visit(ctx.padSpecification())); } - return builder; + return QueryTokenStream.ofFunction(ctx.PAD(), builder); } @Override - public QueryTokenStream visitDateLiteral(HqlParser.DateLiteralContext ctx) { + public QueryTokenStream visitPositionFunction(HqlParser.PositionFunctionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); - if (ctx.DATE() != null) { - if (ctx.LOCAL() != null) { - builder.append(QueryTokens.expression(ctx.LOCAL())); - } - builder.append(QueryTokens.expression(ctx.DATE())); - builder.append(visit(ctx.date())); - } else { - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.date())); - builder.append(TOKEN_CLOSE_PAREN); - } + nested.appendExpression(visit(ctx.positionFunctionPatternArgument())); + nested.append(QueryTokens.expression(ctx.IN())); + nested.append(visit(ctx.positionFunctionStringArgument())); - return builder; + return QueryTokenStream.ofFunction(ctx.POSITION(), nested); } @Override - public QueryTokenStream visitTimeLiteral(HqlParser.TimeLiteralContext ctx) { + public QueryTokenStream visitOverlayFunction(HqlParser.OverlayFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.TIME() != null) { - if (ctx.LOCAL() != null) { - builder.append(QueryTokens.expression(ctx.LOCAL())); - } - builder.append(QueryTokens.expression(ctx.TIME())); - builder.append(visit(ctx.time())); - } else { - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.time())); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokens.token(ctx.OVERLAY())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendExpression(visit(ctx.overlayFunctionStringArgument())); + builder.append(QueryTokens.expression(ctx.PLACING())); + builder.append(visit(ctx.overlayFunctionReplacementArgument())); + builder.append(QueryTokens.expression(ctx.FROM())); + builder.append(visit(ctx.overlayFunctionStartArgument())); + + if (ctx.overlayFunctionLengthArgument() != null) { + builder.append(QueryTokens.expression(ctx.FOR())); + builder.append(visit(ctx.overlayFunctionLengthArgument())); } + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitDateTime(HqlParser.DateTimeContext ctx) { + public QueryTokenStream visitCurrentDateFunction(HqlParser.CurrentDateFunctionContext ctx) { - if (ctx.localDateTime() != null) { - return visit(ctx.localDateTime()); + if (ctx.CURRENT_DATE() != null) { + return QueryTokenStream.ofFunction(ctx.CURRENT_DATE(), QueryTokenStream.empty()); } - if (ctx.offsetDateTime() != null) { - return visit(ctx.offsetDateTime()); - } + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } + + @Override + public QueryTokenStream visitCurrentTimeFunction(HqlParser.CurrentTimeFunctionContext ctx) { - if (ctx.zonedDateTime() != null) { - return visit(ctx.zonedDateTime()); + if (ctx.CURRENT_TIME() != null) { + return QueryTokenStream.ofFunction(ctx.CURRENT_TIME(), QueryTokenStream.empty()); } - return QueryTokenStream.empty(); + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitLocalDateTime(HqlParser.LocalDateTimeContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitCurrentTimestampFunction(HqlParser.CurrentTimestampFunctionContext ctx) { - builder.appendExpression(visit(ctx.date())); - builder.appendExpression(visit(ctx.time())); + if (ctx.CURRENT_TIMESTAMP() != null) { + return QueryTokenStream.ofFunction(ctx.CURRENT_TIMESTAMP(), QueryTokenStream.empty()); + } - return builder; + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitZonedDateTime(HqlParser.ZonedDateTimeContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitInstantFunction(HqlParser.InstantFunctionContext ctx) { - builder.appendExpression(visit(ctx.date())); - builder.appendExpression(visit(ctx.time())); - builder.appendExpression(visit(ctx.zoneId())); + if (ctx.CURRENT_INSTANT() != null) { + return QueryTokenStream.ofFunction(ctx.CURRENT_INSTANT(), QueryTokenStream.empty()); + } - return builder; + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitOffsetDateTime(HqlParser.OffsetDateTimeContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitLocalDateTimeFunction(HqlParser.LocalDateTimeFunctionContext ctx) { - builder.appendExpression(visit(ctx.date())); - builder.appendInline(visit(ctx.time())); - builder.appendInline(visit(ctx.offset())); + if (ctx.LOCAL_DATETIME() != null) { + return QueryTokenStream.ofFunction(ctx.LOCAL_DATETIME(), QueryTokenStream.empty()); + } - return builder; + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitOffsetDateTimeWithMinutes(HqlParser.OffsetDateTimeWithMinutesContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitOffsetDateTimeFunction(HqlParser.OffsetDateTimeFunctionContext ctx) { - builder.appendExpression(visit(ctx.date())); - builder.appendInline(visit(ctx.time())); - builder.appendInline(visit(ctx.offsetWithMinutes())); + if (ctx.OFFSET_DATETIME() != null) { + return QueryTokenStream.ofFunction(ctx.OFFSET_DATETIME(), QueryTokenStream.empty()); + } - return builder; + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitJdbcTimestampLiteral(HqlParser.JdbcTimestampLiteralContext ctx) { + public QueryTokenStream visitLocalDateFunction(HqlParser.LocalDateFunctionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.LOCAL_DATE() != null) { + return QueryTokenStream.ofFunction(ctx.LOCAL_DATE(), QueryTokenStream.empty()); + } - builder.append(TOKEN_OPEN_BRACE); - builder.append(QueryTokens.token("ts")); - builder.append(visit(ctx.dateTime() != null ? ctx.dateTime() : ctx.genericTemporalLiteralText())); - builder.append(QueryTokens.TOKEN_CLOSE_BRACE); + return QueryTokenStream.concatExpressions(ctx.children, this::visit); + } - return builder; + @Override + public QueryTokenStream visitLocalTimeFunction(HqlParser.LocalTimeFunctionContext ctx) { + + if (ctx.LOCAL_TIME() != null) { + return QueryTokenStream.ofFunction(ctx.LOCAL_TIME(), QueryTokenStream.empty()); + } + + return QueryTokenStream.concatExpressions(ctx.children, this::visit); } @Override - public QueryTokenStream visitJdbcDateLiteral(HqlParser.JdbcDateLiteralContext ctx) { + public QueryTokenStream visitFormatFunction(HqlParser.FormatFunctionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder args = QueryRenderer.builder(); - builder.append(TOKEN_OPEN_BRACE); - builder.append(QueryTokens.token("d")); - builder.append(visit(ctx.date() != null ? ctx.date() : ctx.genericTemporalLiteralText())); - builder.append(QueryTokens.TOKEN_CLOSE_BRACE); + args.appendExpression(visit(ctx.expression())); + args.append(QueryTokens.expression(ctx.AS())); + args.appendExpression(visit(ctx.format())); - return builder; + return QueryTokenStream.ofFunction(ctx.FORMAT(), args); } @Override - public QueryTokenStream visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ctx) { + public QueryTokenStream visitCollateFunction(HqlParser.CollateFunctionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder args = QueryRenderer.builder(); - builder.append(TOKEN_OPEN_BRACE); - builder.append(QueryTokens.token("t")); - builder.append(visit(ctx.time() != null ? ctx.time() : ctx.genericTemporalLiteralText())); - builder.append(QueryTokens.TOKEN_CLOSE_BRACE); + args.appendExpression(visit(ctx.expression())); + args.append(QueryTokens.expression(ctx.AS())); + args.appendExpression(visit(ctx.collation())); - return builder; + return QueryTokenStream.ofFunction(ctx.COLLATE(), args); } @Override - public QueryTokenStream visitGenericTemporalLiteralText(HqlParser.GenericTemporalLiteralTextContext ctx) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); + public QueryTokenStream visitCube(HqlParser.CubeContext ctx) { + return QueryTokenStream.ofFunction(ctx.CUBE(), + QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); } @Override - public QueryTokenStream visitArrayLiteral(HqlParser.ArrayLiteralContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_OPEN_SQUARE_BRACKET); - builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_SQUARE_BRACKET); - - return builder; + public QueryTokenStream visitRollup(HqlParser.RollupContext ctx) { + return QueryTokenStream.ofFunction(ctx.ROLLUP(), + QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); } @Override - public QueryTokenStream visitGeneralizedLiteral(HqlParser.GeneralizedLiteralContext ctx) { + public QueryTokenStream visitTruncFunction(HqlParser.TruncFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.TRUNC() != null) { + builder.append(QueryTokens.token(ctx.TRUNC())); + } else { + builder.append(QueryTokens.token(ctx.TRUNCATE())); + } + builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.generalizedLiteralType())); - builder.append(TOKEN_COLON); - builder.append(visit(ctx.generalizedLiteralText())); + + if (ctx.datetimeField() != null) { + builder.append(visit(ctx.expression(0))); + builder.append(TOKEN_COMMA); + builder.append(visit(ctx.datetimeField())); + } else { + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); + } builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitGeneralizedLiteralType(HqlParser.GeneralizedLiteralTypeContext ctx) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); - } - - @Override - public QueryTokenStream visitGeneralizedLiteralText(HqlParser.GeneralizedLiteralTextContext ctx) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); - } - - @Override - public QueryTokenStream visitDate(HqlParser.DateContext ctx) { + public QueryTokenStream visitJpaNonstandardFunction(HqlParser.JpaNonstandardFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.year())); - builder.append(TOKEN_DASH); - builder.append(visit(ctx.month())); - builder.append(TOKEN_DASH); - builder.append(visit(ctx.day())); - - return builder; - } - @Override - public QueryTokenStream visitTime(HqlParser.TimeContext ctx) { + builder.append(QueryTokens.token(ctx.FUNCTION())); + builder.append(TOKEN_OPEN_PAREN); - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.hour())); - builder.append(TOKEN_COLON); - builder.append(visit(ctx.minute())); + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.appendInline(visit(ctx.jpaNonstandardFunctionName())); - if (ctx.second() != null) { - builder.append(TOKEN_COLON); - builder.append(visit(ctx.second())); + if (ctx.castTarget() != null) { + nested.append(QueryTokens.expression(ctx.AS())); + nested.append(visit(ctx.castTarget())); } - return builder; - } - - @Override - public QueryTokenStream visitOffset(HqlParser.OffsetContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.MINUS() != null) { - builder.append(QueryTokens.token(ctx.MINUS())); - } else if (ctx.PLUS() != null) { - builder.append(QueryTokens.token(ctx.PLUS())); + if (ctx.genericFunctionArguments() != null) { + nested.append(TOKEN_COMMA); + nested.appendInline(visit(ctx.genericFunctionArguments())); } - builder.append(visit(ctx.hour())); - if (ctx.minute() != null) { - builder.append(TOKEN_COLON); - builder.append(visit(ctx.minute())); - } + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitOffsetWithMinutes(HqlParser.OffsetWithMinutesContext ctx) { + public QueryTokenStream visitColumnFunction(HqlParser.ColumnFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.MINUS() != null) { - builder.append(QueryTokens.token(ctx.MINUS())); - } else if (ctx.PLUS() != null) { - builder.append(QueryTokens.token(ctx.PLUS())); + builder.append(QueryTokens.token(ctx.COLUMN())); + builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.appendInline(visit(ctx.path())); + nested.append(TOKEN_DOT); + nested.appendExpression(visit(ctx.jpaNonstandardFunctionName())); + + if (ctx.castTarget() != null) { + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.jpaNonstandardFunctionName())); } - builder.append(visit(ctx.hour())); - builder.append(TOKEN_COLON); - builder.append(visit(ctx.minute())); + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitYear(HqlParser.YearContext ctx) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } - - @Override - public QueryTokenStream visitMonth(HqlParser.MonthContext ctx) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } + public QueryTokenStream visitGenericFunctionArguments(HqlParser.GenericFunctionArgumentsContext ctx) { - @Override - public QueryTokenStream visitDay(HqlParser.DayContext ctx) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } + QueryRendererBuilder builder = QueryRenderer.builder(); - @Override - public QueryTokenStream visitHour(HqlParser.HourContext ctx) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } + if (ctx.DISTINCT() != null) { + builder.append(QueryTokens.expression(ctx.DISTINCT())); + } - @Override - public QueryTokenStream visitMinute(HqlParser.MinuteContext ctx) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } + if (ctx.datetimeField() != null) { + builder.append(visit(ctx.datetimeField())); + builder.append(TOKEN_COMMA); + } - @Override - public QueryTokenStream visitSecond(HqlParser.SecondContext ctx) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } + builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); - @Override - public QueryTokenStream visitZoneId(HqlParser.ZoneIdContext ctx) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); + return builder; } @Override - public QueryTokenStream visitDatetimeField(HqlParser.DatetimeFieldContext ctx) { - - if (ctx.YEAR() != null) { - return QueryTokenStream.ofToken(ctx.YEAR()); - } else if (ctx.MONTH() != null) { - return QueryTokenStream.ofToken(ctx.MONTH()); - } else if (ctx.DAY() != null) { - return QueryTokenStream.ofToken(ctx.DAY()); - } else if (ctx.WEEK() != null) { - return QueryTokenStream.ofToken(ctx.WEEK()); - } else if (ctx.QUARTER() != null) { - return QueryTokenStream.ofToken(ctx.QUARTER()); - } else if (ctx.HOUR() != null) { - return QueryTokenStream.ofToken(ctx.HOUR()); - } else if (ctx.MINUTE() != null) { - return QueryTokenStream.ofToken(ctx.MINUTE()); - } else if (ctx.SECOND() != null) { - return QueryTokenStream.ofToken(ctx.SECOND()); - } else if (ctx.NANOSECOND() != null) { - return QueryTokenStream.ofToken(ctx.NANOSECOND()); - } else if (ctx.EPOCH() != null) { - return QueryTokenStream.ofToken(ctx.EPOCH()); - } else { - return QueryTokenStream.empty(); - } + public QueryTokenStream visitCollectionSizeFunction(HqlParser.CollectionSizeFunctionContext ctx) { + return QueryTokenStream.ofFunction(ctx.SIZE(), visit(ctx.path())); } @Override - public QueryTokenStream visitDayField(HqlParser.DayFieldContext ctx) { + public QueryTokenStream visitElementAggregateFunction(HqlParser.ElementAggregateFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.DAY())); - builder.append(QueryTokens.expression(ctx.OF())); - - if (ctx.MONTH() != null) { - builder.append(QueryTokens.expression(ctx.MONTH())); - } - - if (ctx.WEEK() != null) { - builder.append(QueryTokens.expression(ctx.WEEK())); - } - - if (ctx.YEAR() != null) { - builder.append(QueryTokens.expression(ctx.YEAR())); - } - - return builder; - } - - @Override - public QueryTokenStream visitWeekField(HqlParser.WeekFieldContext ctx) { + if (ctx.MAXELEMENT() != null || ctx.MINELEMENT() != null) { + builder.append(QueryTokens.token(ctx.MAXELEMENT() != null ? ctx.MAXELEMENT() : ctx.MINELEMENT())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + } else { - QueryRendererBuilder builder = QueryRenderer.builder(); + if (ctx.MAX() != null) { + builder.append(QueryTokens.token(ctx.MAX())); + } + if (ctx.MIN() != null) { + builder.append(QueryTokens.token(ctx.MIN())); + } + if (ctx.SUM() != null) { + builder.append(QueryTokens.token(ctx.SUM())); + } + if (ctx.AVG() != null) { + builder.append(QueryTokens.token(ctx.AVG())); + } - builder.append(QueryTokens.expression(ctx.WEEK())); - builder.append(QueryTokens.expression(ctx.OF())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.elementsValuesQuantifier())); + builder.append(TOKEN_OPEN_PAREN); - if (ctx.MONTH() != null) { - builder.append(QueryTokens.expression(ctx.MONTH())); - } + if (ctx.path() != null) { + builder.append(visit(ctx.path())); + } - if (ctx.YEAR() != null) { - builder.append(QueryTokens.expression(ctx.YEAR())); + builder.append(TOKEN_CLOSE_PAREN); + builder.append(TOKEN_CLOSE_PAREN); } return builder; } @Override - public QueryTokenStream visitTimeZoneField(HqlParser.TimeZoneFieldContext ctx) { + public QueryTokenStream visitIndexAggregateFunction(HqlParser.IndexAggregateFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.OFFSET() != null) { - builder.append(QueryTokens.expression(ctx.OFFSET())); + if (ctx.MAXINDEX() != null || ctx.MININDEX() != null) { + builder.append(QueryTokens.token(ctx.MAXINDEX() != null ? ctx.MAXINDEX() : ctx.MININDEX())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); + } else { - if (ctx.HOUR() != null) { - builder.append(QueryTokens.expression(ctx.HOUR())); + if (ctx.MAX() != null) { + builder.append(QueryTokens.token(ctx.MAX())); } - - if (ctx.MINUTE() != null) { - builder.append(QueryTokens.expression(ctx.MINUTE())); + if (ctx.MIN() != null) { + builder.append(QueryTokens.token(ctx.MIN())); + } + if (ctx.SUM() != null) { + builder.append(QueryTokens.token(ctx.SUM())); + } + if (ctx.AVG() != null) { + builder.append(QueryTokens.token(ctx.AVG())); } - } - if (ctx.TIMEZONE_HOUR() != null) { - builder.append(QueryTokens.expression(ctx.TIMEZONE_HOUR())); - } + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.indicesKeysQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + + if (ctx.path() != null) { + builder.append(visit(ctx.path())); + } - if (ctx.TIMEZONE_HOUR() != null) { - builder.append(QueryTokens.expression(ctx.TIMEZONE_MINUTE())); + builder.append(TOKEN_CLOSE_PAREN); + builder.append(TOKEN_CLOSE_PAREN); } return builder; } @Override - public QueryTokenStream visitDateOrTimeField(HqlParser.DateOrTimeFieldContext ctx) { - return QueryTokenStream.ofToken(ctx.DATE() != null ? ctx.DATE() : ctx.TIME()); - } - - @Override - public QueryTokenStream visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) { + public QueryTokenStream visitCollectionFunctionMisuse(HqlParser.CollectionFunctionMisuseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.BINARY_LITERAL() != null) { - builder.append(QueryTokens.expression(ctx.BINARY_LITERAL())); - } else if (ctx.HEX_LITERAL() != null) { - - builder.append(TOKEN_OPEN_BRACE); - builder.append(QueryTokenStream.concat(ctx.HEX_LITERAL(), QueryTokenStream::ofToken, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_BRACE); - } + builder.append( + visit(ctx.elementsValuesQuantifier() != null ? ctx.elementsValuesQuantifier() : ctx.indicesKeysQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitTemporalLiteral(HqlParser.TemporalLiteralContext ctx) { + public QueryTokenStream visitListaggFunction(HqlParser.ListaggFunctionContext ctx) { - if (ctx.dateTimeLiteral() != null) { - return visit(ctx.dateTimeLiteral()); - } + QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.dateLiteral() != null) { - return visit(ctx.dateLiteral()); - } + builder.append(QueryTokens.token(ctx.LISTAGG())); + builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); - if (ctx.timeLiteral() != null) { - return visit(ctx.timeLiteral()); + if (ctx.DISTINCT() != null) { + builder.append(QueryTokens.expression(ctx.DISTINCT())); } - if (ctx.jdbcTimestampLiteral() != null) { - return visit(ctx.jdbcTimestampLiteral()); + builder.appendInline(visit(ctx.expressionOrPredicate(0))); + builder.append(TOKEN_COMMA); + builder.appendInline(visit(ctx.expressionOrPredicate(1))); + + if (ctx.onOverflowClause() != null) { + builder.appendExpression(visit(ctx.onOverflowClause())); } - if (ctx.jdbcDateLiteral() != null) { - return visit(ctx.jdbcDateLiteral()); + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.withinGroupClause() != null) { + builder.appendExpression(visit(ctx.withinGroupClause())); } - if (ctx.jdbcTimeLiteral() != null) { - return visit(ctx.jdbcTimeLiteral()); + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); } - return QueryTokenStream.empty(); - } + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } - @Override - public QueryTokenStream visitPlainPrimaryExpression(HqlParser.PlainPrimaryExpressionContext ctx) { - return visit(ctx.primaryExpression()); + return builder; } @Override - public QueryTokenStream visitTupleExpression(HqlParser.TupleExpressionContext ctx) { + public QueryTokenStream visitWithinGroupClause(HqlParser.WithinGroupClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.WITHIN())); + builder.append(QueryTokens.expression(ctx.GROUP())); + builder.append(TOKEN_OPEN_PAREN); - builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); + builder.appendInline(visit(ctx.orderByClause())); builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitHqlConcatenationExpression(HqlParser.HqlConcatenationExpressionContext ctx) { + public QueryTokenStream visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendInline(visit(ctx.expression(0))); - builder.append(TOKEN_DOUBLE_PIPE); - builder.append(visit(ctx.expression(1))); + builder.appendExpression(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); - return builder; + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_ARRAY(), builder); } @Override - public QueryTokenStream visitDayOfWeekExpression(HqlParser.DayOfWeekExpressionContext ctx) { + public QueryTokenStream visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.DAY())); - builder.append(QueryTokens.expression(ctx.OF())); - builder.append(QueryTokens.expression(ctx.WEEK())); + builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - return builder; + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); + } + + if (ctx.jsonExistsOnErrorClause() != null) { + builder.appendExpression(visit(ctx.jsonExistsOnErrorClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_EXISTS(), builder); } @Override - public QueryTokenStream visitDayOfMonthExpression(HqlParser.DayOfMonthExpressionContext ctx) { + public QueryTokenStream visitJsonObjectFunction(HqlParser.JsonObjectFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.DAY())); - builder.append(QueryTokens.expression(ctx.OF())); - builder.append(QueryTokens.expression(ctx.MONTH())); + builder.appendExpression(QueryTokenStream.concat(ctx.jsonObjectFunctionEntry(), this::visit, TOKEN_COMMA)); - return builder; + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); + } + + return QueryTokenStream.ofFunction(ctx.JSON_OBJECT(), builder); } @Override - public QueryTokenStream visitWeekOfYearExpression(HqlParser.WeekOfYearExpressionContext ctx) { + public QueryTokenStream visitJsonQueryFunction(HqlParser.JsonQueryFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.WEEK())); - builder.append(QueryTokens.expression(ctx.OF())); - builder.append(QueryTokens.expression(ctx.YEAR())); + builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - return builder; - } - - @Override - public QueryTokenStream visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.expression())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitAdditionExpression(HqlParser.AdditionExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.appendInline(visit(ctx.expression(1))); - - return builder; - } - - @Override - public QueryTokenStream visitSignedNumericLiteral(HqlParser.SignedNumericLiteralContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.op)); - builder.append(visit(ctx.numericLiteral())); - - return builder; - } - - @Override - public QueryTokenStream visitMultiplicationExpression(HqlParser.MultiplicationExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.expression(0))); - builder.append(QueryTokens.expression(ctx.op)); - builder.appendExpression(visit(ctx.expression(1))); - - return builder; - } - - @Override - public QueryTokenStream visitSubqueryExpression(HqlParser.SubqueryExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitSignedExpression(HqlParser.SignedExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.op)); - builder.appendInline(visit(ctx.expression())); - - return builder; - } - - @Override - public QueryTokenStream visitToDurationExpression(HqlParser.ToDurationExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.expression())); - builder.appendExpression(visit(ctx.datetimeField())); - - return builder; - } - - @Override - public QueryTokenStream visitFromDurationExpression(HqlParser.FromDurationExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.BY())); - builder.appendExpression(visit(ctx.datetimeField())); - - return builder; - } - - @Override - public QueryTokenStream visitCaseExpression(HqlParser.CaseExpressionContext ctx) { - return visit(ctx.caseList()); - } - - @Override - public QueryTokenStream visitLiteralExpression(HqlParser.LiteralExpressionContext ctx) { - return visit(ctx.literal()); - } - - @Override - public QueryTokenStream visitParameterExpression(HqlParser.ParameterExpressionContext ctx) { - return visit(ctx.parameter()); - } - - @Override - public QueryTokenStream visitEntityTypeExpression(HqlParser.EntityTypeExpressionContext ctx) { - return visit(ctx.entityTypeReference()); - } - - @Override - public QueryTokenStream visitEntityIdExpression(HqlParser.EntityIdExpressionContext ctx) { - return visit(ctx.entityIdReference()); - } - - @Override - public QueryTokenStream visitEntityVersionExpression(HqlParser.EntityVersionExpressionContext ctx) { - return visit(ctx.entityVersionReference()); - } - - @Override - public QueryTokenStream visitEntityNaturalIdExpression(HqlParser.EntityNaturalIdExpressionContext ctx) { - return visit(ctx.entityNaturalIdReference()); - } - - @Override - public QueryTokenStream visitSyntacticPathExpression(HqlParser.SyntacticPathExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.syntacticDomainPath())); - - if (ctx.pathContinuation() != null) { - builder.appendInline(visit(ctx.pathContinuation())); - } - - return builder; - } - - @Override - public QueryTokenStream visitPathContinuation(HqlParser.PathContinuationContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(TOKEN_DOT); - builder.append(visit(ctx.simplePath())); - - return builder; - } - - @Override - public QueryTokenStream visitEntityTypeReference(HqlParser.EntityTypeReferenceContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.TYPE())); - builder.append(TOKEN_OPEN_PAREN); - - if (ctx.path() != null) { - builder.appendInline(visit(ctx.path())); - } - - if (ctx.parameter() != null) { - builder.appendInline(visit(ctx.parameter())); - } - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitEntityIdReference(HqlParser.EntityIdReferenceContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.ID())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - - if (ctx.pathContinuation() != null) { - builder.appendInline(visit(ctx.pathContinuation())); - } - - return builder; - } - - @Override - public QueryTokenStream visitEntityVersionReference(HqlParser.EntityVersionReferenceContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.VERSION())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferenceContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.NATURALID())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - - if (ctx.pathContinuation() != null) { - builder.appendInline(visit(ctx.pathContinuation())); - } - - return builder; - } - - @Override - public QueryTokenStream visitSyntacticDomainPath(HqlParser.SyntacticDomainPathContext ctx) { - - if (ctx.treatedNavigablePath() != null) { - return visit(ctx.treatedNavigablePath()); - } - - if (ctx.collectionValueNavigablePath() != null) { - return visit(ctx.collectionValueNavigablePath()); - } - - if (ctx.mapKeyNavigablePath() != null) { - return visit(ctx.mapKeyNavigablePath()); - } - - if (ctx.toOneFkReference() != null) { - return visit(ctx.toOneFkReference()); - } - - if (ctx.function() != null) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.function())); - - if (ctx.indexedPathAccessFragment() != null) { - builder.append(visit(ctx.indexedPathAccessFragment())); - } - - if (ctx.slicedPathAccessFragment() != null) { - builder.append(visit(ctx.slicedPathAccessFragment())); - } - - if (ctx.pathContinuation() != null) { - builder.append(visit(ctx.pathContinuation())); - } - - return builder; - } - - if (ctx.indexedPathAccessFragment() != null) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.simplePath())); - builder.append(visit(ctx.indexedPathAccessFragment())); - - return builder; - } - - if (ctx.slicedPathAccessFragment() != null) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.simplePath())); - builder.append(visit(ctx.slicedPathAccessFragment())); - - return builder; - } - - return QueryRenderer.empty(); - } - - @Override - public QueryTokenStream visitSlicedPathAccessFragment(HqlParser.SlicedPathAccessFragmentContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(TOKEN_OPEN_SQUARE_BRACKET); - builder.appendInline(visit(ctx.expression(0))); - builder.append(TOKEN_COLON); - builder.appendInline(visit(ctx.expression(1))); - builder.append(TOKEN_CLOSE_SQUARE_BRACKET); - - return builder; - } - - @Override - public QueryTokenStream visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) { - return visit(ctx.function()); - } - - @Override - public QueryTokenStream visitStandardFunctionInvocation(HqlParser.StandardFunctionInvocationContext ctx) { - return visit(ctx.standardFunction()); - } - - @Override - public QueryTokenStream visitAggregateFunctionInvocation(HqlParser.AggregateFunctionInvocationContext ctx) { - return visit(ctx.aggregateFunction()); - } - - @Override - public QueryTokenStream visitCollectionSizeFunctionInvocation(HqlParser.CollectionSizeFunctionInvocationContext ctx) { - return visit(ctx.collectionSizeFunction()); - } - - @Override - public QueryTokenStream visitCollectionAggregateFunctionInvocation( - HqlParser.CollectionAggregateFunctionInvocationContext ctx) { - return visit(ctx.collectionAggregateFunction()); - } - - @Override - public QueryTokenStream visitCollectionFunctionMisuseInvocation( - HqlParser.CollectionFunctionMisuseInvocationContext ctx) { - return visit(ctx.collectionFunctionMisuse()); - } - - @Override - public QueryTokenStream visitJpaNonstandardFunctionInvocation(HqlParser.JpaNonstandardFunctionInvocationContext ctx) { - return visit(ctx.jpaNonstandardFunction()); - } - - @Override - public QueryTokenStream visitColumnFunctionInvocation(HqlParser.ColumnFunctionInvocationContext ctx) { - return visit(ctx.columnFunction()); - } - - @Override - public QueryTokenStream visitGenericFunctionInvocation(HqlParser.GenericFunctionInvocationContext ctx) { - return visit(ctx.genericFunction()); - } - - @Override - public QueryTokenStream visitStandardFunction(HqlParser.StandardFunctionContext ctx) { - - if (ctx.castFunction() != null) { - return visit(ctx.castFunction()); - } - - if (ctx.extractFunction() != null) { - return visit(ctx.extractFunction()); - } - - if (ctx.truncFunction() != null) { - return visit(ctx.truncFunction()); - } - - if (ctx.formatFunction() != null) { - return visit(ctx.formatFunction()); - } - - if (ctx.collateFunction() != null) { - return visit(ctx.collateFunction()); - } - - if (ctx.substringFunction() != null) { - return visit(ctx.substringFunction()); - } - - if (ctx.overlayFunction() != null) { - return visit(ctx.overlayFunction()); - } - - if (ctx.trimFunction() != null) { - return visit(ctx.trimFunction()); - } - - if (ctx.padFunction() != null) { - return visit(ctx.padFunction()); - } - - if (ctx.positionFunction() != null) { - return visit(ctx.positionFunction()); - } - - if (ctx.currentDateFunction() != null) { - return visit(ctx.currentDateFunction()); - } - - if (ctx.currentTimeFunction() != null) { - return visit(ctx.currentTimeFunction()); - } - - if (ctx.currentTimestampFunction() != null) { - return visit(ctx.currentTimestampFunction()); - } - - if (ctx.instantFunction() != null) { - return visit(ctx.instantFunction()); - } - - if (ctx.localDateFunction() != null) { - return visit(ctx.localDateFunction()); - } - - if (ctx.localTimeFunction() != null) { - return visit(ctx.localTimeFunction()); - } - - if (ctx.localDateTimeFunction() != null) { - return visit(ctx.localDateTimeFunction()); - } - - if (ctx.offsetDateTimeFunction() != null) { - return visit(ctx.offsetDateTimeFunction()); - } - - if (ctx.cube() != null) { - return visit(ctx.cube()); - } - - if (ctx.rollup() != null) { - return visit(ctx.rollup()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSubstringFunction(HqlParser.SubstringFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.SUBSTRING())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.expression())); - - if (ctx.FROM() == null) { - builder.append(TOKEN_COMMA); - } else { - builder.append(QueryTokens.expression(ctx.FROM())); - } - - builder.append(visit(ctx.substringFunctionStartArgument())); - - if (ctx.substringFunctionLengthArgument() != null) { - if (ctx.FOR() == null) { - builder.append(TOKEN_COMMA); - } else { - builder.append(QueryTokens.expression(ctx.FOR())); - } - - builder.append(visit(ctx.substringFunctionLengthArgument())); - } - - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitSubstringFunctionStartArgument(HqlParser.SubstringFunctionStartArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitSubstringFunctionLengthArgument(HqlParser.SubstringFunctionLengthArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitPadFunction(HqlParser.PadFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.PAD())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.WITH())); - builder.appendExpression(visit(ctx.padLength())); - - if (ctx.padCharacter() != null) { - builder.appendExpression(visit(ctx.padSpecification())); - builder.appendInline(visit(ctx.padCharacter())); - } else { - builder.append(visit(ctx.padSpecification())); - } - - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitPadSpecification(HqlParser.PadSpecificationContext ctx) { - return QueryTokenStream.ofToken(ctx.LEADING() != null ? ctx.LEADING() : ctx.TRAILING()); - } - - @Override - public QueryTokenStream visitPadCharacter(HqlParser.PadCharacterContext ctx) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); - } - - @Override - public QueryTokenStream visitPadLength(HqlParser.PadLengthContext ctx) { - return visit(ctx.expression()); - } - - @Override - 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); - - 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; - } - - @Override - public QueryTokenStream visitPositionFunctionPatternArgument(HqlParser.PositionFunctionPatternArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitPositionFunctionStringArgument(HqlParser.PositionFunctionStringArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitOverlayFunction(HqlParser.OverlayFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.OVERLAY())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendExpression(visit(ctx.overlayFunctionStringArgument())); - builder.append(QueryTokens.expression(ctx.PLACING())); - builder.append(visit(ctx.overlayFunctionReplacementArgument())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.append(visit(ctx.overlayFunctionStartArgument())); - - if (ctx.overlayFunctionLengthArgument() != null) { - builder.append(QueryTokens.expression(ctx.FOR())); - builder.append(visit(ctx.overlayFunctionLengthArgument())); - } - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitOverlayFunctionStringArgument(HqlParser.OverlayFunctionStringArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitOverlayFunctionReplacementArgument( - HqlParser.OverlayFunctionReplacementArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitOverlayFunctionStartArgument(HqlParser.OverlayFunctionStartArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitOverlayFunctionLengthArgument(HqlParser.OverlayFunctionLengthArgumentContext ctx) { - return visit(ctx.expression()); - } - - @Override - public QueryTokenStream visitCurrentDateFunction(HqlParser.CurrentDateFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT_DATE() != null) { - builder.append(QueryTokens.token(ctx.CURRENT_DATE())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.DATE())); - } - - return builder; - } - - @Override - public QueryTokenStream visitCurrentTimeFunction(HqlParser.CurrentTimeFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT_TIME() != null) { - builder.append(QueryTokens.token(ctx.CURRENT_TIME())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.TIME())); - } - - return builder; - } - - @Override - public QueryTokenStream visitCurrentTimestampFunction(HqlParser.CurrentTimestampFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT_TIMESTAMP() != null) { - builder.append(QueryTokens.token(ctx.CURRENT_TIMESTAMP())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.TIMESTAMP())); - } - - return builder; - } - - @Override - public QueryTokenStream visitInstantFunction(HqlParser.InstantFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT_INSTANT() != null) { - builder.append(QueryTokens.token(ctx.CURRENT_INSTANT())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.INSTANT())); - } - - return builder; - } - - @Override - public QueryTokenStream visitLocalDateTimeFunction(HqlParser.LocalDateTimeFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.LOCAL_DATETIME() != null) { - builder.append(QueryTokens.token(ctx.LOCAL_DATETIME())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.LOCAL())); - builder.append(QueryTokens.expression(ctx.DATETIME())); - } - - return builder; - } - - @Override - public QueryTokenStream visitOffsetDateTimeFunction(HqlParser.OffsetDateTimeFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.OFFSET_DATETIME() != null) { - builder.append(QueryTokens.token(ctx.OFFSET_DATETIME())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.OFFSET())); - builder.append(QueryTokens.expression(ctx.DATETIME())); - } - - return builder; - } - - @Override - public QueryTokenStream visitLocalDateFunction(HqlParser.LocalDateFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.LOCAL_DATE() != null) { - builder.append(QueryTokens.token(ctx.LOCAL_DATE())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.LOCAL())); - builder.append(QueryTokens.expression(ctx.DATE())); - } - - return builder; - } - - @Override - public QueryTokenStream visitLocalTimeFunction(HqlParser.LocalTimeFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.LOCAL_TIME() != null) { - builder.append(QueryTokens.token(ctx.LOCAL_TIME())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } else { - builder.append(QueryTokens.expression(ctx.LOCAL())); - builder.append(QueryTokens.expression(ctx.TIME())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFormatFunction(HqlParser.FormatFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.FORMAT())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.format())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitCollation(HqlParser.CollationContext ctx) { - return visit(ctx.simplePath()); - } - - @Override - public QueryTokenStream visitCollateFunction(HqlParser.CollateFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.COLLATE())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.AS())); - builder.appendInline(visit(ctx.collation())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitCube(HqlParser.CubeContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.CUBE())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitRollup(HqlParser.RollupContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.ROLLUP())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitFormat(HqlParser.FormatContext ctx) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); - } - - @Override - public QueryTokenStream visitTruncFunction(HqlParser.TruncFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.TRUNC() != null) { - builder.append(QueryTokens.token(ctx.TRUNC())); - } else { - builder.append(QueryTokens.token(ctx.TRUNCATE())); - } - - builder.append(TOKEN_OPEN_PAREN); - - if (ctx.datetimeField() != null) { - builder.append(visit(ctx.expression(0))); - builder.append(TOKEN_COMMA); - builder.append(visit(ctx.datetimeField())); - } else { - builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - } - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitJpaNonstandardFunction(HqlParser.JpaNonstandardFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.FUNCTION())); - builder.append(TOKEN_OPEN_PAREN); - - QueryRendererBuilder nested = QueryRenderer.builder(); - nested.appendInline(visit(ctx.jpaNonstandardFunctionName())); - - if (ctx.castTarget() != null) { - nested.append(QueryTokens.expression(ctx.AS())); - nested.append(visit(ctx.castTarget())); - } - - if (ctx.genericFunctionArguments() != null) { - nested.append(TOKEN_COMMA); - nested.appendInline(visit(ctx.genericFunctionArguments())); - } - - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitJpaNonstandardFunctionName(HqlParser.JpaNonstandardFunctionNameContext ctx) { - - if (ctx.identifier() != null) { - return visit(ctx.identifier()); - } - - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); - } - - @Override - public QueryTokenStream visitColumnFunction(HqlParser.ColumnFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.COLUMN())); - builder.append(TOKEN_OPEN_PAREN); - - QueryRendererBuilder nested = QueryRenderer.builder(); - nested.appendInline(visit(ctx.path())); - nested.append(TOKEN_DOT); - nested.appendExpression(visit(ctx.jpaNonstandardFunctionName())); - - if (ctx.castTarget() != null) { - nested.append(QueryTokens.expression(ctx.AS())); - nested.appendExpression(visit(ctx.jpaNonstandardFunctionName())); - } - - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitGenericFunctionName(HqlParser.GenericFunctionNameContext ctx) { - return visit(ctx.simplePath()); - } - - @Override - public QueryTokenStream visitGenericFunctionArguments(HqlParser.GenericFunctionArgumentsContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); - } - - if (ctx.datetimeField() != null) { - builder.append(visit(ctx.datetimeField())); - builder.append(TOKEN_COMMA); - } - - builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); - - return builder; - } - - @Override - public QueryTokenStream visitCollectionSizeFunction(HqlParser.CollectionSizeFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.SIZE())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitElementAggregateFunction(HqlParser.ElementAggregateFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.MAXELEMENT() != null || ctx.MINELEMENT() != null) { - builder.append(QueryTokens.token(ctx.MAXELEMENT() != null ? ctx.MAXELEMENT() : ctx.MINELEMENT())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - } else { - - if (ctx.MAX() != null) { - builder.append(QueryTokens.token(ctx.MAX())); - } - if (ctx.MIN() != null) { - builder.append(QueryTokens.token(ctx.MIN())); - } - if (ctx.SUM() != null) { - builder.append(QueryTokens.token(ctx.SUM())); - } - if (ctx.AVG() != null) { - builder.append(QueryTokens.token(ctx.AVG())); - } - - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.elementsValuesQuantifier())); - builder.append(TOKEN_OPEN_PAREN); - - if (ctx.path() != null) { - builder.append(visit(ctx.path())); - } - - builder.append(TOKEN_CLOSE_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitIndexAggregateFunction(HqlParser.IndexAggregateFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.MAXINDEX() != null || ctx.MININDEX() != null) { - builder.append(QueryTokens.token(ctx.MAXINDEX() != null ? ctx.MAXINDEX() : ctx.MININDEX())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - } else { - - if (ctx.MAX() != null) { - builder.append(QueryTokens.token(ctx.MAX())); - } - if (ctx.MIN() != null) { - builder.append(QueryTokens.token(ctx.MIN())); - } - if (ctx.SUM() != null) { - builder.append(QueryTokens.token(ctx.SUM())); - } - if (ctx.AVG() != null) { - builder.append(QueryTokens.token(ctx.AVG())); - } - - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.indicesKeysQuantifier())); - builder.append(TOKEN_OPEN_PAREN); - - if (ctx.path() != null) { - builder.append(visit(ctx.path())); - } - - builder.append(TOKEN_CLOSE_PAREN); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitCollectionFunctionMisuse(HqlParser.CollectionFunctionMisuseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append( - visit(ctx.elementsValuesQuantifier() != null ? ctx.elementsValuesQuantifier() : ctx.indicesKeysQuantifier())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitAggregateFunction(HqlParser.AggregateFunctionContext ctx) { - - if (ctx.everyFunction() != null) { - return visit(ctx.everyFunction()); - } - - if (ctx.anyFunction() != null) { - return visit(ctx.anyFunction()); - } - - return visit(ctx.listaggFunction()); - } - - @Override - public QueryTokenStream visitEveryAllQuantifier(HqlParser.EveryAllQuantifierContext ctx) { - - if (ctx.EVERY() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.EVERY())); - } - - return QueryRenderer.from(QueryTokens.token(ctx.ALL())); - } - - @Override - public QueryTokenStream visitAnySomeQuantifier(HqlParser.AnySomeQuantifierContext ctx) { - - if (ctx.ANY() != null) { - return QueryRenderer.from(QueryTokens.token(ctx.ANY())); - } - - return QueryRenderer.from(QueryTokens.token(ctx.SOME())); - } - - @Override - public QueryTokenStream visitListaggFunction(HqlParser.ListaggFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.LISTAGG())); - builder.append(TOKEN_OPEN_PAREN); - - QueryRendererBuilder nested = QueryRenderer.builder(); - - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); - } - - builder.appendInline(visit(ctx.expressionOrPredicate(0))); - builder.append(TOKEN_COMMA); - builder.appendInline(visit(ctx.expressionOrPredicate(1))); - - if (ctx.onOverflowClause() != null) { - builder.appendExpression(visit(ctx.onOverflowClause())); - } - - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - if (ctx.withinGroupClause() != null) { - builder.appendExpression(visit(ctx.withinGroupClause())); - } - - if (ctx.filterClause() != null) { - builder.appendExpression(visit(ctx.filterClause())); - } - - if (ctx.overClause() != null) { - builder.appendExpression(visit(ctx.overClause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitOnOverflowClause(HqlParser.OnOverflowClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.ON())); - builder.append(QueryTokens.expression(ctx.OVERFLOW())); - - if (ctx.ERROR() != null) { - builder.append(QueryTokens.expression(ctx.ERROR())); - } else { - - builder.append(QueryTokens.expression(ctx.TRUNCATE())); - - if (ctx.expression() != null) { - builder.appendExpression(visit(ctx.expression())); - } - - if (ctx.WITH() != null) { - builder.append(QueryTokens.expression(ctx.WITH())); - } - - if (ctx.WITHOUT() != null) { - builder.append(QueryTokens.expression(ctx.WITHOUT())); - } - - if (ctx.COUNT() != null) { - builder.append(QueryTokens.expression(ctx.COUNT())); - } - } - - return builder; - } - - @Override - public QueryTokenStream visitWithinGroupClause(HqlParser.WithinGroupClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WITHIN())); - builder.append(QueryTokens.expression(ctx.GROUP())); - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.orderByClause())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitNullsClause(HqlParser.NullsClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.IGNORE() != null) { - builder.append(QueryTokens.expression(ctx.IGNORE())); - } else { - builder.append(QueryTokens.expression(ctx.RESPECT())); - } - - builder.append(QueryTokens.expression(ctx.NULLS())); - - return builder; - } - - @Override - public QueryTokenStream visitNthSideClause(HqlParser.NthSideClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.FROM())); - - if (ctx.FIRST() != null) { - builder.append(QueryTokens.expression(ctx.FIRST())); - } else { - builder.append(QueryTokens.expression(ctx.LAST())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFrameStart(HqlParser.FrameStartContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT() != null) { - - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.ROW())); - } else if (ctx.UNBOUNDED() != null) { - builder.append(QueryTokens.expression(ctx.UNBOUNDED())); - builder.append(QueryTokens.expression(ctx.PRECEDING())); - } else { - - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.PRECEDING() != null ? ctx.PRECEDING() : ctx.FOLLOWING())); - } - - return builder; - - } - - @Override - public QueryTokenStream visitFrameEnd(HqlParser.FrameEndContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT() != null) { - - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.ROW())); - } else if (ctx.UNBOUNDED() != null) { - builder.append(QueryTokens.expression(ctx.UNBOUNDED())); - builder.append(QueryTokens.expression(ctx.FOLLOWING())); - } else { - - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.PRECEDING() != null ? ctx.PRECEDING() : ctx.FOLLOWING())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFrameExclusion(HqlParser.FrameExclusionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.EXCLUDE())); - - if (ctx.CURRENT() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT())); - builder.append(QueryTokens.expression(ctx.ROW())); - } else if (ctx.GROUP() != null) { - builder.append(QueryTokens.expression(ctx.GROUP())); - } else if (ctx.TIES() != null) { - builder.append(QueryTokens.expression(ctx.TIES())); - } else { - builder.append(QueryTokens.expression(ctx.NO())); - builder.append(QueryTokens.expression(ctx.OTHERS())); - } - - return builder; - } - - @Override - public QueryTokenStream visitJsonFunctionInvocation(HqlParser.JsonFunctionInvocationContext ctx) { - return visit(ctx.jsonFunction()); - } - - @Override - public QueryTokenStream visitJsonFunction(HqlParser.JsonFunctionContext ctx) { - - if (ctx.jsonArrayFunction() != null) { - return visit(ctx.jsonArrayFunction()); - } else if (ctx.jsonExistsFunction() != null) { - return visit(ctx.jsonExistsFunction()); - } else if (ctx.jsonObjectFunction() != null) { - return visit(ctx.jsonObjectFunction()); - } else if (ctx.jsonQueryFunction() != null) { - return visit(ctx.jsonQueryFunction()); - } else if (ctx.jsonValueFunction() != null) { - return visit(ctx.jsonValueFunction()); - } else if (ctx.jsonArrayAggFunction() != null) { - return visit(ctx.jsonArrayAggFunction()); - } else if (ctx.jsonObjectAggFunction() != null) { - return visit(ctx.jsonObjectAggFunction()); - } - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); - - if (ctx.jsonNullClause() != null) { - builder.appendExpression(visit(ctx.jsonNullClause())); - } - - return QueryTokenStream.ofFunction(ctx.JSON_ARRAY(), builder); - } - - @Override - public QueryTokenStream visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - - if (ctx.jsonPassingClause() != null) { - builder.appendExpression(visit(ctx.jsonPassingClause())); - } - - if (ctx.jsonExistsOnErrorClause() != null) { - builder.appendExpression(visit(ctx.jsonExistsOnErrorClause())); - } - - return QueryTokenStream.ofFunction(ctx.JSON_EXISTS(), builder); - } - - @Override - public QueryTokenStream visitJsonExistsOnErrorClause(HqlParser.JsonExistsOnErrorClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonObjectFunction(HqlParser.JsonObjectFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(QueryTokenStream.concat(ctx.jsonObjectFunctionEntry(), this::visit, TOKEN_COMMA)); - - if (ctx.jsonNullClause() != null) { - builder.appendExpression(visit(ctx.jsonNullClause())); - } - - return QueryTokenStream.ofFunction(ctx.JSON_OBJECT(), builder); - } - - @Override - public QueryTokenStream visitJsonObjectFunctionEntry(HqlParser.JsonObjectFunctionEntryContext ctx) { - - if (ctx.expressionOrPredicate() != null) { - return visit(ctx.expressionOrPredicate()); - } else if (ctx.jsonObjectKeyValueEntry() != null) { - return visit(ctx.jsonObjectKeyValueEntry()); - } else if (ctx.jsonObjectAssignmentEntry() != null) { - return visit(ctx.jsonObjectAssignmentEntry()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitJsonObjectKeyValueEntry(HqlParser.JsonObjectKeyValueEntryContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonObjectAssignmentEntry(HqlParser.JsonObjectAssignmentEntryContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonQueryFunction(HqlParser.JsonQueryFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - - if (ctx.jsonPassingClause() != null) { - builder.appendExpression(visit(ctx.jsonPassingClause())); - } - - if (ctx.jsonQueryWrapperClause() != null) { - builder.appendExpression(visit(ctx.jsonQueryWrapperClause())); - } - - builder.append(QueryTokenStream.concat(ctx.jsonQueryOnErrorOrEmptyClause(), this::visit, TOKEN_SPACE)); - - return QueryTokenStream.ofFunction(ctx.JSON_QUERY(), builder); - } - - @Override - public QueryTokenStream visitJsonQueryWrapperClause(HqlParser.JsonQueryWrapperClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonQueryOnErrorOrEmptyClause(HqlParser.JsonQueryOnErrorOrEmptyClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonValueFunction(HqlParser.JsonValueFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - - if (ctx.jsonPassingClause() != null) { - builder.appendExpression(visit(ctx.jsonPassingClause())); - } - - if (ctx.jsonValueReturningClause() != null) { - builder.appendExpression(visit(ctx.jsonValueReturningClause())); - } - - builder.append(QueryTokenStream.concat(ctx.jsonValueOnErrorOrEmptyClause(), this::visit, TOKEN_SPACE)); - - return QueryTokenStream.ofFunction(ctx.JSON_VALUE(), builder); - } - - @Override - public QueryTokenStream visitJsonValueReturningClause(HqlParser.JsonValueReturningClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonValueOnErrorOrEmptyClause(HqlParser.JsonValueOnErrorOrEmptyClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.expressionOrPredicate())); - - if (ctx.jsonNullClause() != null) { - builder.appendExpression(visit(ctx.jsonNullClause())); - } - - if (ctx.orderByClause() != null) { - builder.appendExpression(visit(ctx.orderByClause())); - } - - QueryTokenStream function = QueryTokenStream.ofFunction(ctx.JSON_ARRAYAGG(), builder); - - if (ctx.filterClause() == null) { - return function; - } - - QueryRendererBuilder functionWithFilter = QueryRenderer.builder(); - functionWithFilter.appendExpression(function); - functionWithFilter.appendExpression(visit(ctx.filterClause())); - - return functionWithFilter.build(); - } - - @Override - public QueryTokenStream visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.KEY() != null) { - builder.append(QueryTokens.expression(ctx.KEY())); - } - - builder.appendExpression(visit(ctx.expressionOrPredicate(0))); - - if (ctx.VALUE() != null) { - builder.append(QueryTokens.expression(ctx.VALUE())); - } else { - builder.append(TOKEN_COLON); - } - - builder.appendExpression(visit(ctx.expressionOrPredicate(1))); - - if (ctx.jsonNullClause() != null) { - builder.appendExpression(visit(ctx.jsonNullClause())); - } - - if (ctx.jsonUniqueKeysClause() != null) { - builder.appendExpression(visit(ctx.jsonUniqueKeysClause())); - } - - QueryTokenStream function = QueryTokenStream.ofFunction(ctx.JSON_OBJECTAGG(), builder); - - if (ctx.filterClause() == null) { - return function; - } - - QueryRendererBuilder functionWithFilter = QueryRenderer.builder(); - functionWithFilter.appendExpression(function); - functionWithFilter.appendExpression(visit(ctx.filterClause())); - - return functionWithFilter.build(); - } - - @Override - public QueryTokenStream visitJsonPassingClause(HqlParser.JsonPassingClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.PASSING())); - - builder.append(QueryTokenStream.concat(ctx.jsonPassingItem(), this::visit, TOKEN_COMMA)); - - return builder; - } - - @Override - public QueryTokenStream visitJsonPassingItem(HqlParser.JsonPassingItemContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonNullClause(HqlParser.JsonNullClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonUniqueKeysClause(HqlParser.JsonUniqueKeysClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonTableFunction(HqlParser.JsonTableFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - - if (ctx.jsonPassingClause() != null) { - builder.appendExpression(visit(ctx.jsonPassingClause())); - } - - builder.appendExpression(visit(ctx.jsonTableColumnsClause())); - - if (ctx.jsonTableErrorClause() != null) { - builder.appendExpression(visit(ctx.jsonTableErrorClause())); - } - - return QueryTokenStream.ofFunction(ctx.JSON_TABLE(), builder); - } - - @Override - public QueryTokenStream visitJsonTableErrorClause(HqlParser.JsonTableErrorClauseContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonTableColumnsClause(HqlParser.JsonTableColumnsClauseContext ctx) { - return QueryTokenStream.ofFunction(ctx.COLUMNS(), visit(ctx.jsonTableColumns())); - } - - @Override - public QueryTokenStream visitJsonTableColumns(HqlParser.JsonTableColumnsContext ctx) { - return QueryTokenStream.concat(ctx.jsonTableColumn(), this::visit, TOKEN_COMMA); - } - - @Override - public QueryTokenStream visitJsonTableNestedColumn(HqlParser.JsonTableNestedColumnContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonTableQueryColumn(HqlParser.JsonTableQueryColumnContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonTableOrdinalityColumn(HqlParser.JsonTableOrdinalityColumnContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonTableExistsColumn(HqlParser.JsonTableExistsColumnContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitJsonTableValueColumn(HqlParser.JsonTableValueColumnContext ctx) { - return QueryTokenStream.concatExpressions(ctx.children, this::visit); - } - - @Override - public QueryTokenStream visitCollectionQuantifier(HqlParser.CollectionQuantifierContext ctx) { - - if (ctx.elementsValuesQuantifier() != null) { - return visit(ctx.elementsValuesQuantifier()); - } - - return visit(ctx.indicesKeysQuantifier()); - } - - @Override - public QueryTokenStream visitElementsValuesQuantifier(HqlParser.ElementsValuesQuantifierContext ctx) { - return QueryRenderer.from(QueryTokens.token(ctx.ELEMENTS() != null ? ctx.ELEMENTS() : ctx.VALUES())); - } - - @Override - public QueryTokenStream visitIndicesKeysQuantifier(HqlParser.IndicesKeysQuantifierContext ctx) { - return QueryRenderer.from(QueryTokens.token(ctx.INDICES() != null ? ctx.INDICES() : ctx.KEYS())); - } - - @Override - public QueryTokenStream visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext ctx) { - return visit(ctx.generalPathFragment()); - } - - @Override - public QueryTokenStream visitPath(HqlParser.PathContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.syntacticDomainPath() != null) { - - builder.append(visit(ctx.syntacticDomainPath())); - - if (ctx.pathContinuation() != null) { - builder.append(visit(ctx.pathContinuation())); - } - } else if (ctx.generalPathFragment() != null) { - builder.append(visit(ctx.generalPathFragment())); - } - - return builder; - } - - @Override - public QueryTokenStream visitGeneralPathFragment(HqlParser.GeneralPathFragmentContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.simplePath())); - - if (ctx.indexedPathAccessFragment() != null) { - builder.append(visit(ctx.indexedPathAccessFragment())); - } - - return builder; - } - - @Override - public QueryTokenStream visitIndexedPathAccessFragment(HqlParser.IndexedPathAccessFragmentContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(TOKEN_OPEN_SQUARE_BRACKET); - builder.appendInline(visit(ctx.expression())); - builder.append(TOKEN_CLOSE_SQUARE_BRACKET); - - if (ctx.generalPathFragment() != null) { - - builder.append(TOKEN_DOT); - builder.append(visit(ctx.generalPathFragment())); - } - - return builder; - } - - @Override - public QueryTokenStream visitSimplePath(HqlParser.SimplePathContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.identifier())); - - if (!ctx.simplePathElement().isEmpty()) { - builder.append(TOKEN_DOT); - } - - builder.append(QueryTokenStream.concat(ctx.simplePathElement(), this::visit, TOKEN_DOT)); - - return builder; - } - - @Override - public QueryTokenStream visitSimplePathElement(HqlParser.SimplePathElementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.identifier())); - - return builder; - } - - @Override - public QueryTokenStream visitCaseList(HqlParser.CaseListContext ctx) { - - if (ctx.simpleCaseExpression() != null) { - return visit(ctx.simpleCaseExpression()); - } else if (ctx.searchedCaseExpression() != null) { - return visit(ctx.searchedCaseExpression()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CASE())); - builder.append(visit(ctx.expressionOrPredicate(0))); - builder.appendExpression(QueryTokenStream.concat(ctx.caseWhenExpressionClause(), this::visit, TOKEN_SPACE)); - - if (ctx.ELSE() != null) { - - builder.append(QueryTokens.expression(ctx.ELSE())); - builder.append(visit(ctx.expressionOrPredicate(1))); - } - - builder.append(QueryTokens.expression(ctx.END())); - - return builder; - } - - @Override - public QueryTokenStream visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CASE())); - - builder.append(QueryTokenStream.concat(ctx.caseWhenPredicateClause(), this::visit, TOKEN_SPACE)); - - if (ctx.ELSE() != null) { - - builder.append(QueryTokens.expression(ctx.ELSE())); - builder.appendExpression(visit(ctx.expressionOrPredicate())); - } - - builder.append(QueryTokens.expression(ctx.END())); - - return builder; - } - - @Override - public QueryTokenStream visitCaseWhenExpressionClause(HqlParser.CaseWhenExpressionClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.expressionOrPredicate())); - - return builder; - } - - @Override - public QueryTokenStream visitCaseWhenPredicateClause(HqlParser.CaseWhenPredicateClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.predicate())); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.expressionOrPredicate())); - - return builder; - } - - @Override - public QueryTokenStream visitGenericFunction(HqlParser.GenericFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - QueryRendererBuilder nested = QueryRenderer.builder(); - - nested.append(visit(ctx.genericFunctionName())); - nested.append(TOKEN_OPEN_PAREN); - - if (ctx.genericFunctionArguments() != null) { - nested.appendInline(visit(ctx.genericFunctionArguments())); - } else if (ctx.ASTERISK() != null) { - nested.append(QueryTokens.token(ctx.ASTERISK())); - } - - nested.append(TOKEN_CLOSE_PAREN); - builder.append(nested); - - if (ctx.pathContinuation() != null) { - builder.append(visit(ctx.pathContinuation())); - } - - if (ctx.nthSideClause() != null) { - builder.appendExpression(visit(ctx.nthSideClause())); - } - - if (ctx.nullsClause() != null) { - builder.appendExpression(visit(ctx.nullsClause())); - } - - if (ctx.withinGroupClause() != null) { - builder.appendExpression(visit(ctx.withinGroupClause())); - } - - if (ctx.filterClause() != null) { - builder.appendExpression(visit(ctx.filterClause())); - } - - if (ctx.overClause() != null) { - builder.appendExpression(visit(ctx.overClause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFilterClause(HqlParser.FilterClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.FILTER())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.whereClause())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitOverClause(HqlParser.OverClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.OVER())); - - QueryRendererBuilder nested = QueryRenderer.builder(); - nested.append(TOKEN_OPEN_PAREN); - - List trees = new ArrayList<>(); - - if (ctx.partitionClause() != null) { - trees.add(ctx.partitionClause()); - } - - if (ctx.orderByClause() != null) { - trees.add(ctx.orderByClause()); - } - - if (ctx.frameClause() != null) { - trees.add(ctx.frameClause()); - } - - nested.appendInline(QueryTokenStream.concat(trees, this::visit, TOKEN_SPACE)); - nested.append(TOKEN_CLOSE_PAREN); - - builder.appendInline(nested); - - return builder; - } - - @Override - public QueryTokenStream visitPartitionClause(HqlParser.PartitionClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.PARTITION())); - builder.append(QueryTokens.expression(ctx.BY())); - - builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - - return builder; - } - - @Override - public QueryTokenStream visitFrameClause(HqlParser.FrameClauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.RANGE() != null) { - builder.append(QueryTokens.expression(ctx.RANGE())); - } else if (ctx.ROWS() != null) { - builder.append(QueryTokens.expression(ctx.ROWS())); - } else if (ctx.GROUPS() != null) { - builder.append(QueryTokens.expression(ctx.GROUPS())); - } - - if (ctx.BETWEEN() != null) { - builder.append(QueryTokens.expression(ctx.BETWEEN())); - } - - builder.appendExpression(visit(ctx.frameStart())); - - if (ctx.AND() != null) { - - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.frameEnd())); - } - - if (ctx.frameExclusion() != null) { - builder.appendExpression(visit(ctx.frameExclusion())); - } - - return builder; - } - - @Override - public QueryTokenStream visitCastFunction(HqlParser.CastFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.CAST())); - - builder.append(TOKEN_OPEN_PAREN); - - QueryRendererBuilder nested = QueryRenderer.builder(); - nested.appendExpression(visit(ctx.expression())); - nested.append(QueryTokens.expression(ctx.AS())); - nested.appendExpression(visit(ctx.castTarget())); - - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitCastTarget(HqlParser.CastTargetContext ctx) { - - List literals = ctx.INTEGER_LITERAL(); - - if (!CollectionUtils.isEmpty(literals)) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.castTargetType())); - builder.append(TOKEN_OPEN_PAREN); - - QueryRendererBuilder args = QueryRenderer.builder(); - for (int i = 0; i < literals.size(); i++) { - if (i > 0) { - args.append(TOKEN_COMMA); - } - args.append(QueryTokens.token(literals.get(i))); - } - - builder.appendInline(args.build()); - builder.append(TOKEN_CLOSE_PAREN); - - return builder.build(); - } - - return visit(ctx.castTargetType()); - } - - @Override - public QueryTokenStream visitCastTargetType(HqlParser.CastTargetTypeContext ctx) { - return QueryTokenStream.from(QueryTokens.token(ctx.fullTargetName)); - } - - @Override - public QueryTokenStream visitExtractFunction(HqlParser.ExtractFunctionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.EXTRACT() != null) { - - builder.append(QueryTokens.token(ctx.EXTRACT())); - builder.append(TOKEN_OPEN_PAREN); - - QueryRendererBuilder nested = QueryRenderer.builder(); - - nested.appendExpression(visit(ctx.extractField())); - nested.append(QueryTokens.expression(ctx.FROM())); - nested.appendExpression(visit(ctx.expression())); - - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.datetimeField() != null) { - - builder.append(visit(ctx.datetimeField())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.expression())); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitExtractField(HqlParser.ExtractFieldContext ctx) { - - if (ctx.datetimeField() != null) { - return visit(ctx.datetimeField()); - } - - if (ctx.dayField() != null) { - return visit(ctx.dayField()); - } - - if (ctx.weekField() != null) { - return visit(ctx.weekField()); + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); } - if (ctx.timeZoneField() != null) { - return visit(ctx.timeZoneField()); + if (ctx.jsonQueryWrapperClause() != null) { + builder.appendExpression(visit(ctx.jsonQueryWrapperClause())); } - if (ctx.dateOrTimeField() != null) { - return visit(ctx.dateOrTimeField()); - } + builder.append(QueryTokenStream.concat(ctx.jsonQueryOnErrorOrEmptyClause(), this::visit, TOKEN_SPACE)); - return QueryRenderer.builder(); + return QueryTokenStream.ofFunction(ctx.JSON_QUERY(), builder); } @Override - public QueryTokenStream visitTrimFunction(HqlParser.TrimFunctionContext ctx) { + public QueryTokenStream visitJsonValueFunction(HqlParser.JsonValueFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.TRIM())); - builder.append(TOKEN_OPEN_PAREN); - - if (ctx.trimSpecification() != null) { - builder.appendExpression(visit(ctx.trimSpecification())); - } - - if (ctx.trimCharacter() != null) { - builder.appendExpression(visit(ctx.trimCharacter())); - } + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - if (ctx.FROM() != null) { - builder.append(QueryTokens.expression(ctx.FROM())); + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); } - if (ctx.expression() != null) { - builder.append(visit(ctx.expression())); + if (ctx.jsonValueReturningClause() != null) { + builder.appendExpression(visit(ctx.jsonValueReturningClause())); } - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokenStream.concat(ctx.jsonValueOnErrorOrEmptyClause(), this::visit, TOKEN_SPACE)); - return builder; + return QueryTokenStream.ofFunction(ctx.JSON_VALUE(), builder); } @Override - public QueryTokenStream visitTrimSpecification(HqlParser.TrimSpecificationContext ctx) { + public QueryTokenStream visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.BOTH() != null) { - builder.append(QueryTokens.expression(ctx.BOTH())); - } else if (ctx.LEADING() != null) { - builder.append(QueryTokens.expression(ctx.LEADING())); - } else if (ctx.TRAILING() != null) { - builder.append(QueryTokens.expression(ctx.TRAILING())); + builder.appendExpression(visit(ctx.expressionOrPredicate())); + + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); } - return builder.build(); - } + if (ctx.orderByClause() != null) { + builder.appendExpression(visit(ctx.orderByClause())); + } - @Override - public QueryTokenStream visitTrimCharacter(HqlParser.TrimCharacterContext ctx) { + QueryTokenStream function = QueryTokenStream.ofFunction(ctx.JSON_ARRAYAGG(), builder); - if (ctx.STRING_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRING_LITERAL()); + if (ctx.filterClause() == null) { + return function; } - return visit(ctx.parameter()); + QueryRendererBuilder functionWithFilter = QueryRenderer.builder(); + functionWithFilter.appendExpression(function); + functionWithFilter.appendExpression(visit(ctx.filterClause())); + + return functionWithFilter.build(); } @Override - public QueryTokenStream visitEveryFunction(HqlParser.EveryFunctionContext ctx) { + public QueryTokenStream visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.everyAllQuantifier())); - - if (ctx.predicate() != null) { - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.predicate())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.KEY() != null) { + builder.append(QueryTokens.expression(ctx.KEY())); + } - if (ctx.filterClause() != null) { - builder.appendExpression(visit(ctx.filterClause())); - } + builder.appendExpression(visit(ctx.expressionOrPredicate(0))); - if (ctx.overClause() != null) { - builder.appendExpression(visit(ctx.overClause())); - } - } else if (ctx.subquery() != null) { - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.VALUE() != null) { + builder.append(QueryTokens.expression(ctx.VALUE())); } else { + builder.append(TOKEN_COLON); + } - builder.append(visit(ctx.collectionQuantifier())); + builder.appendExpression(visit(ctx.expressionOrPredicate(1))); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.simplePath())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.jsonNullClause() != null) { + builder.appendExpression(visit(ctx.jsonNullClause())); } - return builder; - } - - @Override - public QueryTokenStream visitAnyFunction(HqlParser.AnyFunctionContext ctx) { + if (ctx.jsonUniqueKeysClause() != null) { + builder.appendExpression(visit(ctx.jsonUniqueKeysClause())); + } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryTokenStream function = QueryTokenStream.ofFunction(ctx.JSON_OBJECTAGG(), builder); - builder.appendExpression(visit(ctx.anySomeQuantifier())); + if (ctx.filterClause() == null) { + return function; + } - if (ctx.predicate() != null) { - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.predicate())); - builder.append(TOKEN_CLOSE_PAREN); + QueryRendererBuilder functionWithFilter = QueryRenderer.builder(); + functionWithFilter.appendExpression(function); + functionWithFilter.appendExpression(visit(ctx.filterClause())); - if (ctx.filterClause() != null) { - builder.appendExpression(visit(ctx.filterClause())); - } + return functionWithFilter.build(); + } - if (ctx.overClause() != null) { - builder.appendExpression(visit(ctx.overClause())); - } - } else if (ctx.subquery() != null) { - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - } else { + @Override + public QueryTokenStream visitJsonPassingClause(HqlParser.JsonPassingClauseContext ctx) { - builder.append(visit(ctx.collectionQuantifier())); + QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.simplePath())); - builder.append(TOKEN_CLOSE_PAREN); - } + builder.append(QueryTokens.expression(ctx.PASSING())); + builder.append(QueryTokenStream.concat(ctx.jsonPassingItem(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitTreatedNavigablePath(HqlParser.TreatedNavigablePathContext ctx) { + public QueryTokenStream visitJsonTableFunction(HqlParser.JsonTableFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.TREAT())); - builder.append(TOKEN_OPEN_PAREN); + builder.appendExpression(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); - QueryRendererBuilder nested = QueryRenderer.builder(); - nested.appendExpression(visit(ctx.path())); - nested.append(QueryTokens.expression(ctx.AS())); - nested.append(visit(ctx.simplePath())); + if (ctx.jsonPassingClause() != null) { + builder.appendExpression(visit(ctx.jsonPassingClause())); + } - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); + builder.appendExpression(visit(ctx.jsonTableColumnsClause())); - if (ctx.pathContinuation() != null) { - builder.append(visit(ctx.pathContinuation())); + if (ctx.jsonTableErrorClause() != null) { + builder.appendExpression(visit(ctx.jsonTableErrorClause())); } - return builder; + return QueryTokenStream.ofFunction(ctx.JSON_TABLE(), builder); } @Override - public QueryTokenStream visitCollectionValueNavigablePath(HqlParser.CollectionValueNavigablePathContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitJsonTableColumnsClause(HqlParser.JsonTableColumnsClauseContext ctx) { + return QueryTokenStream.ofFunction(ctx.COLUMNS(), visit(ctx.jsonTableColumns())); + } - builder.append(visit(ctx.elementValueQuantifier())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); + @Override + public QueryTokenStream visitJsonTableColumns(HqlParser.JsonTableColumnsContext ctx) { + return QueryTokenStream.concat(ctx.jsonTableColumn(), this::visit, TOKEN_COMMA); + } - if (ctx.pathContinuation() != null) { - builder.append(visit(ctx.pathContinuation())); - } + @Override + public QueryTokenStream visitPath(HqlParser.PathContext ctx) { + return QueryTokenStream.concat(ctx.children, this::visit, EMPTY_TOKEN); + } - return builder; + @Override + public QueryTokenStream visitGeneralPathFragment(HqlParser.GeneralPathFragmentContext ctx) { + return QueryTokenStream.concat(ctx.children, this::visit, EMPTY_TOKEN); } @Override - public QueryTokenStream visitMapKeyNavigablePath(HqlParser.MapKeyNavigablePathContext ctx) { + public QueryTokenStream visitIndexedPathAccessFragment(HqlParser.IndexedPathAccessFragmentContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.indexKeyQuantifier())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(TOKEN_OPEN_SQUARE_BRACKET); + builder.appendInline(visit(ctx.expression())); + builder.append(TOKEN_CLOSE_SQUARE_BRACKET); - if (ctx.pathContinuation() != null) { - builder.append(visit(ctx.pathContinuation())); + if (ctx.generalPathFragment() != null) { + + builder.append(TOKEN_DOT); + builder.append(visit(ctx.generalPathFragment())); } return builder; } @Override - public QueryTokenStream visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) { + public QueryTokenStream visitSimplePath(HqlParser.SimplePathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.FK())); - builder.append(TOKEN_OPEN_PAREN); - builder.append(visit(ctx.path())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitElementValueQuantifier(HqlParser.ElementValueQuantifierContext ctx) { + builder.append(visit(ctx.identifier())); - if (ctx.ELEMENT() != null) { - return QueryTokenStream.ofToken(ctx.ELEMENT()); + if (!ctx.simplePathElement().isEmpty()) { + builder.append(TOKEN_DOT); } - if (ctx.VALUE() != null) { - return QueryTokenStream.ofToken(ctx.VALUE()); - } + builder.append(QueryTokenStream.concat(ctx.simplePathElement(), this::visit, TOKEN_DOT)); - return QueryTokenStream.empty(); + return builder; } @Override - public QueryTokenStream visitIndexKeyQuantifier(HqlParser.IndexKeyQuantifierContext ctx) { - - if (ctx.INDEX() != null) { - return QueryTokenStream.ofToken(ctx.INDEX()); - } - - if (ctx.KEY() != null) { - return QueryTokenStream.ofToken(ctx.KEY()); - } - - return QueryTokenStream.empty(); + public QueryTokenStream visitSimplePathElement(HqlParser.SimplePathElementContext ctx) { + return visit(ctx.identifier()); } @Override - public QueryTokenStream visitIsBooleanPredicate(HqlParser.IsBooleanPredicateContext ctx) { + public QueryTokenStream visitGenericFunction(HqlParser.GenericFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.expression())); - builder.append(QueryTokens.expression(ctx.IS())); + nested.append(visit(ctx.genericFunctionName())); + nested.append(TOKEN_OPEN_PAREN); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); + if (ctx.genericFunctionArguments() != null) { + nested.appendInline(visit(ctx.genericFunctionArguments())); + } else if (ctx.ASTERISK() != null) { + nested.append(QueryTokens.token(ctx.ASTERISK())); } - if (ctx.NULL() != null) { - builder.append(QueryTokens.expression(ctx.NULL())); - } + nested.append(TOKEN_CLOSE_PAREN); + builder.append(nested); - if (ctx.TRUE() != null) { - builder.append(QueryTokens.expression(ctx.TRUE())); + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } - if (ctx.FALSE() != null) { - builder.append(QueryTokens.expression(ctx.FALSE())); + if (ctx.nthSideClause() != null) { + builder.appendExpression(visit(ctx.nthSideClause())); } - if (ctx.EMPTY() != null) { - builder.append(QueryTokens.expression(ctx.EMPTY())); + if (ctx.nullsClause() != null) { + builder.appendExpression(visit(ctx.nullsClause())); } - return builder; - } - - @Override - public QueryTokenStream visitMemberOfPredicate(HqlParser.MemberOfPredicateContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.expression())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - if (ctx.MEMBER() != null) { - builder.append(QueryTokens.expression(ctx.MEMBER())); + if (ctx.withinGroupClause() != null) { + builder.appendExpression(visit(ctx.withinGroupClause())); } - if (ctx.OF() != null) { - builder.append(QueryTokens.expression(ctx.OF())); + + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); } - builder.append(visit(ctx.path())); + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } return builder; } @Override - public QueryTokenStream visitIsDistinctFromPredicate(HqlParser.IsDistinctFromPredicateContext ctx) { + public QueryTokenStream visitFilterClause(HqlParser.FilterClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.expression(0))); - builder.append(QueryTokens.expression(ctx.IS())); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - if (ctx.DISTINCT() != null) { - - builder.append(QueryTokens.expression(ctx.DISTINCT())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendExpression(visit(ctx.expression(1))); - } + builder.append(QueryTokens.expression(ctx.FILTER())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.whereClause())); + builder.append(TOKEN_CLOSE_PAREN); return builder; } @Override - public QueryTokenStream visitBetweenPredicate(HqlParser.BetweenPredicateContext ctx) { - return visit(ctx.betweenExpression()); - } - - @Override - public QueryTokenStream visitContainsPredicate(HqlParser.ContainsPredicateContext ctx) { + public QueryTokenStream visitOverClause(HqlParser.OverClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.expression(ctx.OVER())); - builder.appendExpression(visit(ctx.expression(0))); + QueryRendererBuilder nested = QueryRenderer.builder(); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } + List trees = new ArrayList<>(); - if (ctx.CONTAINS() != null) { - builder.append(QueryTokens.expression(ctx.CONTAINS())); + if (ctx.partitionClause() != null) { + trees.add(ctx.partitionClause()); } - if (ctx.INCLUDES() != null) { - builder.append(QueryTokens.expression(ctx.INCLUDES())); + + if (ctx.orderByClause() != null) { + trees.add(ctx.orderByClause()); } - if (ctx.INTERSECTS() != null) { - builder.append(QueryTokens.expression(ctx.INTERSECTS())); + + if (ctx.frameClause() != null) { + trees.add(ctx.frameClause()); } - builder.appendExpression(visit(ctx.expression(1))); + nested.appendInline(QueryTokenStream.concat(trees, this::visit, TOKEN_SPACE)); + builder.appendInline(QueryTokenStream.group(nested)); return builder; - } @Override - public QueryTokenStream visitOrPredicate(HqlParser.OrPredicateContext ctx) { + public QueryTokenStream visitPartitionClause(HqlParser.PartitionClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.predicate(0))); - builder.append(QueryTokens.expression(ctx.OR())); - builder.appendExpression(visit(ctx.predicate(1))); + builder.append(QueryTokens.expression(ctx.PARTITION())); + builder.append(QueryTokens.expression(ctx.BY())); + + builder.append(QueryTokenStream.concat(ctx.expression(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitRelationalPredicate(HqlParser.RelationalPredicateContext ctx) { - return visit(ctx.relationalExpression()); - } + public QueryTokenStream visitCastFunction(HqlParser.CastFunctionContext ctx) { - @Override - public QueryTokenStream visitExistsPredicate(HqlParser.ExistsPredicateContext ctx) { - return visit(ctx.existsExpression()); + QueryRendererBuilder nested = QueryRenderer.builder(); + nested.appendExpression(visit(ctx.expression())); + nested.append(QueryTokens.expression(ctx.AS())); + nested.appendExpression(visit(ctx.castTarget())); + + return QueryTokenStream.ofFunction(ctx.CAST(), nested); } @Override - public QueryTokenStream visitAndPredicate(HqlParser.AndPredicateContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); + public QueryTokenStream visitCastTarget(HqlParser.CastTargetContext ctx) { - builder.appendExpression(visit(ctx.predicate(0))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.predicate(1))); + List literals = ctx.INTEGER_LITERAL(); - return builder; - } + if (!CollectionUtils.isEmpty(literals)) { - @Override - public QueryTokenStream visitGroupedPredicate(HqlParser.GroupedPredicateContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(visit(ctx.castTargetType())); + builder.append(TOKEN_OPEN_PAREN); - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder args = QueryRenderer.builder(); + for (int i = 0; i < literals.size(); i++) { + if (i > 0) { + args.append(TOKEN_COMMA); + } + args.append(QueryTokens.token(literals.get(i))); + } - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.predicate())); - builder.append(TOKEN_CLOSE_PAREN); + builder.appendInline(args.build()); + builder.append(TOKEN_CLOSE_PAREN); - return builder; - } + return builder.build(); + } - @Override - public QueryTokenStream visitLikePredicate(HqlParser.LikePredicateContext ctx) { - return visit(ctx.stringPatternMatching()); + return visit(ctx.castTargetType()); } @Override - public QueryTokenStream visitInPredicate(HqlParser.InPredicateContext ctx) { - return visit(ctx.inExpression()); + public QueryTokenStream visitCastTargetType(HqlParser.CastTargetTypeContext ctx) { + return QueryTokens.token(ctx.fullTargetName); } @Override - public QueryTokenStream visitNotPredicate(HqlParser.NotPredicateContext ctx) { + public QueryTokenStream visitExtractFunction(HqlParser.ExtractFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(TOKEN_NOT); - builder.append(visit(ctx.predicate())); + if (ctx.EXTRACT() != null) { + + builder.append(QueryTokens.token(ctx.EXTRACT())); + builder.append(TOKEN_OPEN_PAREN); + + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.extractField())); + nested.append(QueryTokens.expression(ctx.FROM())); + nested.appendExpression(visit(ctx.expression())); + + builder.appendInline(nested); + builder.append(TOKEN_CLOSE_PAREN); + } else if (ctx.datetimeField() != null) { + + builder.append(visit(ctx.datetimeField())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.expression())); + builder.append(TOKEN_CLOSE_PAREN); + } return builder; } @Override - public QueryTokenStream visitExpressionPredicate(HqlParser.ExpressionPredicateContext ctx) { - return visit(ctx.expression()); - } + public QueryTokenStream visitTrimFunction(HqlParser.TrimFunctionContext ctx) { - @Override - public QueryTokenStream visitExpressionOrPredicate(HqlParser.ExpressionOrPredicateContext ctx) { + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.trimSpecification() != null) { + builder.appendExpression(visit(ctx.trimSpecification())); + } + + if (ctx.trimCharacter() != null) { + builder.appendExpression(visit(ctx.trimCharacter())); + } + + if (ctx.FROM() != null) { + builder.append(QueryTokens.expression(ctx.FROM())); + } if (ctx.expression() != null) { - return visit(ctx.expression()); - } else if (ctx.predicate() != null) { - return visit(ctx.predicate()); - } else { - return QueryTokenStream.empty(); + builder.append(visit(ctx.expression())); } + + return QueryTokenStream.ofFunction(ctx.TRIM(), builder); } @Override - public QueryTokenStream visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) { + public QueryTokenStream visitEveryFunction(HqlParser.EveryFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendInline(visit(ctx.expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.appendInline(visit(ctx.expression(1))); + builder.appendExpression(visit(ctx.everyAllQuantifier())); + + if (ctx.predicate() != null) { + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.predicate())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } + + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } + } else if (ctx.subquery() != null) { + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.subquery())); + builder.append(TOKEN_CLOSE_PAREN); + } else { + + builder.append(visit(ctx.collectionQuantifier())); + + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.simplePath())); + builder.append(TOKEN_CLOSE_PAREN); + } return builder; } @Override - public QueryTokenStream visitBetweenExpression(HqlParser.BetweenExpressionContext ctx) { + public QueryTokenStream visitAnyFunction(HqlParser.AnyFunctionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.expression(0))); + builder.appendExpression(visit(ctx.anySomeQuantifier())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } + if (ctx.predicate() != null) { + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.predicate())); + builder.append(TOKEN_CLOSE_PAREN); + + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } + + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } + } else if (ctx.subquery() != null) { + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.subquery())); + builder.append(TOKEN_CLOSE_PAREN); + } else { + + builder.append(visit(ctx.collectionQuantifier())); - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.expression(2))); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.simplePath())); + builder.append(TOKEN_CLOSE_PAREN); + } return builder; } @Override - public QueryTokenStream visitStringPatternMatching(HqlParser.StringPatternMatchingContext ctx) { + public QueryTokenStream visitTreatedNavigablePath(HqlParser.TreatedNavigablePathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder nested = QueryRenderer.builder(); + + nested.appendExpression(visit(ctx.path())); + nested.append(QueryTokens.expression(ctx.AS())); + nested.append(visit(ctx.simplePath())); - builder.appendExpression(visit(ctx.expression(0))); + builder.append(QueryTokenStream.ofFunction(ctx.TREAT(), nested)); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } - if (ctx.LIKE() != null) { - builder.append(QueryTokens.expression(ctx.LIKE())); - } else if (ctx.ILIKE() != null) { - builder.append(QueryTokens.expression(ctx.ILIKE())); - } + return builder; + } - builder.appendExpression(visit(ctx.expression(1))); + @Override + public QueryTokenStream visitCollectionValueNavigablePath(HqlParser.CollectionValueNavigablePathContext ctx) { - if (ctx.ESCAPE() != null) { + QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.ESCAPE())); + builder.append(visit(ctx.elementValueQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); - if (ctx.STRING_LITERAL() != null) { - builder.append(QueryTokens.expression(ctx.STRING_LITERAL())); - } else if (ctx.JAVA_STRING_LITERAL() != null) { - builder.append(QueryTokens.expression(ctx.JAVA_STRING_LITERAL())); - } else if (ctx.parameter() != null) { - builder.appendExpression(visit(ctx.parameter())); - } + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } return builder; } @Override - public QueryTokenStream visitInExpression(HqlParser.InExpressionContext ctx) { + public QueryTokenStream visitMapKeyNavigablePath(HqlParser.MapKeyNavigablePathContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.expression())); + builder.append(visit(ctx.indexKeyQuantifier())); + builder.append(TOKEN_OPEN_PAREN); + builder.append(visit(ctx.path())); + builder.append(TOKEN_CLOSE_PAREN); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); + if (ctx.pathContinuation() != null) { + builder.append(visit(ctx.pathContinuation())); } - builder.append(QueryTokens.expression(ctx.IN())); - builder.appendExpression(visit(ctx.inList())); - return builder; } + @Override + public QueryTokenStream visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) { + return QueryTokenStream.ofFunction(ctx.FK(), visit(ctx.path())); + } + + @Override + public QueryTokenStream visitGroupedPredicate(HqlParser.GroupedPredicateContext ctx) { + return QueryTokenStream.group(visit(ctx.predicate())); + } + @Override public QueryTokenStream visitInList(HqlParser.InListContext ctx) { @@ -4257,83 +1921,11 @@ public QueryTokenStream visitExistsExpression(HqlParser.ExistsExpressionContext return builder; } - @Override - public QueryTokenStream visitInstantiationTarget(HqlParser.InstantiationTargetContext ctx) { - - if (ctx.LIST() != null) { - return QueryTokenStream.ofToken(ctx.LIST()); - } else if (ctx.MAP() != null) { - return QueryTokenStream.ofToken(ctx.MAP()); - } else if (ctx.simplePath() != null) { - return visit(ctx.simplePath()); - } else { - return QueryTokenStream.empty(); - } - } - @Override public QueryTokenStream visitInstantiationArguments(HqlParser.InstantiationArgumentsContext ctx) { return QueryTokenStream.concat(ctx.instantiationArgument(), this::visit, TOKEN_COMMA); } - @Override - public QueryTokenStream visitInstantiationArgument(HqlParser.InstantiationArgumentContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.expressionOrPredicate() != null) { - builder.appendExpression(visit(ctx.expressionOrPredicate())); - } else if (ctx.instantiation() != null) { - builder.appendExpression(visit(ctx.instantiation())); - } - - if (ctx.variable() != null) { - builder.append(visit(ctx.variable())); - } - - return builder; - } - - @Override - public QueryTokenStream visitParameterOrIntegerLiteral(HqlParser.ParameterOrIntegerLiteralContext ctx) { - - if (ctx.parameter() != null) { - return visit(ctx.parameter()); - } else if (ctx.INTEGER_LITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTEGER_LITERAL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitParameterOrNumberLiteral(HqlParser.ParameterOrNumberLiteralContext ctx) { - - if (ctx.parameter() != null) { - return visit(ctx.parameter()); - } else if (ctx.numericLiteral() != null) { - return visit(ctx.numericLiteral()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.identifier() != null) { - - builder.append(QueryTokens.expression(ctx.AS())); - builder.append(visit(ctx.identifier())); - } else if (ctx.nakedIdentifier() != null) { - builder.append(visit(ctx.nakedIdentifier())); - } - - return builder; - } - @Override public QueryTokenStream visitParameter(HqlParser.ParameterContext ctx) { @@ -4348,7 +1940,7 @@ public QueryTokenStream visitParameter(HqlParser.ParameterContext ctx) { builder.append(TOKEN_QUESTION_MARK); if (ctx.INTEGER_LITERAL() != null) { - builder.append(QueryTokens.expression(ctx.INTEGER_LITERAL())); + builder.append(QueryTokens.token(ctx.INTEGER_LITERAL())); } } @@ -4361,35 +1953,19 @@ public QueryTokenStream visitEntityName(HqlParser.EntityNameContext ctx) { } @Override - public QueryTokenStream visitIdentifier(HqlParser.IdentifierContext ctx) { - - if (ctx.nakedIdentifier() != null) { - return visit(ctx.nakedIdentifier()); - } else if (ctx.FULL() != null) { - return QueryTokenStream.ofToken(ctx.FULL()); - } else if (ctx.LEFT() != null) { - return QueryTokenStream.ofToken(ctx.LEFT()); - } else if (ctx.INNER() != null) { - return QueryTokenStream.ofToken(ctx.INNER()); - } else if (ctx.OUTER() != null) { - return QueryTokenStream.ofToken(ctx.OUTER()); - } else if (ctx.RIGHT() != null) { - return QueryTokenStream.ofToken(ctx.RIGHT()); - } + public QueryTokenStream visitChildren(RuleNode node) { - return QueryTokenStream.empty(); - } + int childCount = node.getChildCount(); - @Override - public QueryTokenStream visitNakedIdentifier(HqlParser.NakedIdentifierContext ctx) { + if (childCount == 1 && node.getChild(0) instanceof RuleContext t) { + return visit(t); + } - if (ctx.IDENTIFIER() != null) { - return QueryTokenStream.ofToken(ctx.IDENTIFIER()); - } else if (ctx.QUOTED_IDENTIFIER() != null) { - return QueryTokenStream.ofToken(ctx.QUOTED_IDENTIFIER()); - } else { - return QueryTokenStream.ofToken(ctx.f); + if (childCount == 1 && node.getChild(0) instanceof TerminalNode t) { + return QueryTokens.token(t); } + + return QueryTokenStream.concatExpressions(node, this::visit); } } 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 06fc23f13c..3fe487516c 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 @@ -21,15 +21,13 @@ import java.util.List; import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RuleContext; import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; +import org.antlr.v4.runtime.tree.TerminalNode; -import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext; -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.CollectionUtils; -import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. @@ -79,109 +77,6 @@ public QueryTokenStream visitStart(JpqlParser.StartContext ctx) { return visit(ctx.ql_statement()); } - @Override - public QueryTokenStream visitQl_statement(JpqlParser.Ql_statementContext ctx) { - - if (ctx.select_statement() != null) { - return visit(ctx.select_statement()); - } else if (ctx.update_statement() != null) { - return visit(ctx.update_statement()); - } else if (ctx.delete_statement() != null) { - return visit(ctx.delete_statement()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitSelectQuery(JpqlParser.SelectQueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.select_clause())); - builder.appendExpression(visit(ctx.from_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - if (ctx.groupby_clause() != null) { - builder.appendExpression(visit(ctx.groupby_clause())); - } - - if (ctx.having_clause() != null) { - builder.appendExpression(visit(ctx.having_clause())); - } - - if (ctx.orderby_clause() != null) { - builder.appendExpression(visit(ctx.orderby_clause())); - } - - if (ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); - } - - return builder; - } - - @Override - public QueryTokenStream visitFromQuery(JpqlParser.FromQueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.from_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - if (ctx.groupby_clause() != null) { - builder.appendExpression(visit(ctx.groupby_clause())); - } - - if (ctx.having_clause() != null) { - builder.appendExpression(visit(ctx.having_clause())); - } - - if (ctx.orderby_clause() != null) { - builder.appendExpression(visit(ctx.orderby_clause())); - } - - if (ctx.set_fuction() != null) { - builder.appendExpression(visit(ctx.set_fuction())); - } - - return builder; - } - - @Override - public QueryTokenStream visitUpdate_statement(JpqlParser.Update_statementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(visit(ctx.update_clause())); - - if (ctx.where_clause() != null) { - builder.append(visit(ctx.where_clause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitDelete_statement(JpqlParser.Delete_statementContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.delete_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - - return builder; - } - @Override public QueryTokenStream visitFrom_clause(JpqlParser.From_clauseContext ctx) { @@ -191,12 +86,12 @@ public QueryTokenStream visitFrom_clause(JpqlParser.From_clauseContext ctx) { builder.appendInline(visit(ctx.identification_variable_declaration())); if (!ctx.identificationVariableDeclarationOrCollectionMemberDeclaration().isEmpty()) { - builder.append(TOKEN_COMMA); - builder.appendExpression(QueryTokenStream - .concat(ctx.identificationVariableDeclarationOrCollectionMemberDeclaration(), this::visit, TOKEN_COMMA)); } + builder.appendExpression(QueryTokenStream + .concat(ctx.identificationVariableDeclarationOrCollectionMemberDeclaration(), this::visit, TOKEN_COMMA)); + return builder; } @@ -204,11 +99,7 @@ public QueryTokenStream visitFrom_clause(JpqlParser.From_clauseContext ctx) { public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMemberDeclaration( JpqlParser.IdentificationVariableDeclarationOrCollectionMemberDeclarationContext ctx) { - if (ctx.identification_variable_declaration() != null) { - return visit(ctx.identification_variable_declaration()); - } else if (ctx.collection_member_declaration() != null) { - return visit(ctx.collection_member_declaration()); - } else if (ctx.subquery() != null) { + if (ctx.subquery() != null) { QueryRendererBuilder nested = QueryRenderer.builder(); nested.append(TOKEN_OPEN_PAREN); @@ -227,117 +118,9 @@ public QueryTokenStream visitIdentificationVariableDeclarationOrCollectionMember } return builder; - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitIdentification_variable_declaration( - JpqlParser.Identification_variable_declarationContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - 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; - } - - @Override - public QueryTokenStream visitRange_variable_declaration(JpqlParser.Range_variable_declarationContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.entity_name())); - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - - if (ctx.identification_variable() != null) { - builder.appendExpression(visit(ctx.identification_variable())); - } - - return builder; - } - - @Override - public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.join_spec())); - 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; - } - - @Override - public QueryTokenStream visitFetch_join(JpqlParser.Fetch_joinContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.join_spec())); - builder.append(QueryTokens.expression(ctx.FETCH())); - 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; - } - - @Override - public QueryTokenStream visitJoin_spec(JpqlParser.Join_specContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.LEFT() != null) { - builder.append(QueryTokens.expression(ctx.LEFT())); - } - if (ctx.OUTER() != null) { - builder.append(QueryTokens.expression(ctx.OUTER())); } - if (ctx.INNER() != null) { - builder.append(QueryTokens.expression(ctx.INNER())); - } - if (ctx.JOIN() != null) { - builder.append(QueryTokens.expression(ctx.JOIN())); - } - - return builder; - } - - @Override - public QueryTokenStream visitJoin_condition(JpqlParser.Join_conditionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.ON())); - builder.appendExpression(visit(ctx.conditional_expression())); - return builder; + return super.visitIdentificationVariableDeclarationOrCollectionMemberDeclaration(ctx); } @Override @@ -499,18 +282,6 @@ public QueryTokenStream visitSingle_valued_path_expression(JpqlParser.Single_val return builder; } - @Override - public QueryTokenStream visitGeneral_identification_variable(JpqlParser.General_identification_variableContext ctx) { - - if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.map_field_identification_variable() != null) { - return visit(ctx.map_field_identification_variable()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitGeneral_subpath(JpqlParser.General_subpathContext ctx) { @@ -569,18 +340,6 @@ public QueryTokenStream visitState_field_path_expression(JpqlParser.State_field_ return builder; } - @Override - public QueryTokenStream visitState_valued_path_expression(JpqlParser.State_valued_path_expressionContext ctx) { - - if (ctx.state_field_path_expression() != null) { - return visit(ctx.state_field_path_expression()); - } else if (ctx.general_identification_variable() != null) { - return visit(ctx.general_identification_variable()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitSingle_valued_object_path_expression( JpqlParser.Single_valued_object_path_expressionContext ctx) { @@ -655,39 +414,6 @@ public QueryTokenStream visitUpdate_item(JpqlParser.Update_itemContext ctx) { return builder; } - @Override - public QueryTokenStream visitNew_value(JpqlParser.New_valueContext ctx) { - - if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.simple_entity_expression() != null) { - return visit(ctx.simple_entity_expression()); - } else if (ctx.NULL() != null) { - return QueryTokenStream.ofToken(ctx.NULL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitDelete_clause(JpqlParser.Delete_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.DELETE())); - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendExpression(visit(ctx.entity_name())); - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - if (ctx.identification_variable() != null) { - builder.appendExpression(visit(ctx.identification_variable())); - } - - return builder; - } - @Override public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) { @@ -711,53 +437,22 @@ QueryRendererBuilder prepareSelectClause(JpqlParser.Select_clauseContext ctx) { return builder; } - @Override - public QueryTokenStream visitSelect_item(JpqlParser.Select_itemContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.select_expression())); - - if (ctx.AS() != null) { - builder.append(QueryTokens.expression(ctx.AS())); - } - - if (ctx.result_variable() != null) { - builder.appendExpression(visit(ctx.result_variable())); - } - - return builder; - } - @Override public QueryTokenStream visitSelect_expression(JpqlParser.Select_expressionContext ctx) { - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.aggregate_expression() != null) { - return visit(ctx.aggregate_expression()); - } else if (ctx.identification_variable() != null) { - - if (ctx.OBJECT() == null) { - return visit(ctx.identification_variable()); - } else { + if (ctx.identification_variable() != null && ctx.OBJECT() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.OBJECT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.identification_variable())); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(QueryTokens.token(ctx.OBJECT())); + builder.append(TOKEN_OPEN_PAREN); + builder.appendInline(visit(ctx.identification_variable())); + builder.append(TOKEN_CLOSE_PAREN); - return builder; - } - } else if (ctx.constructor_expression() != null) { - return visit(ctx.constructor_expression()); - } else { - return QueryTokenStream.empty(); + return builder; } + + return super.visitSelect_expression(ctx); } @Override @@ -774,24 +469,6 @@ public QueryTokenStream visitConstructor_expression(JpqlParser.Constructor_expre return builder; } - @Override - public QueryTokenStream visitConstructor_item(JpqlParser.Constructor_itemContext ctx) { - - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.aggregate_expression() != null) { - return visit(ctx.aggregate_expression()); - } else if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.literal() != null) { - return visit(ctx.literal()); - } - - return QueryTokenStream.empty(); - } - @Override public QueryTokenStream visitAggregate_expression(JpqlParser.Aggregate_expressionContext ctx) { @@ -842,17 +519,6 @@ public QueryTokenStream visitAggregate_expression(JpqlParser.Aggregate_expressio return builder; } - @Override - public QueryTokenStream visitWhere_clause(JpqlParser.Where_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHERE())); - builder.appendExpression(visit(ctx.conditional_expression())); - - return builder; - } - @Override public QueryTokenStream visitGroupby_clause(JpqlParser.Groupby_clauseContext ctx) { @@ -865,29 +531,6 @@ public QueryTokenStream visitGroupby_clause(JpqlParser.Groupby_clauseContext ctx return builder; } - @Override - public QueryTokenStream visitGroupby_item(JpqlParser.Groupby_itemContext ctx) { - - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitHaving_clause(JpqlParser.Having_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.HAVING())); - builder.appendExpression(visit(ctx.conditional_expression())); - - return builder; - } - @Override public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx) { @@ -901,710 +544,92 @@ public QueryTokenStream visitOrderby_clause(JpqlParser.Orderby_clauseContext ctx } @Override - public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { + public QueryTokenStream visitSubquery_from_clause(JpqlParser.Subquery_from_clauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendExpression(visit(ctx.orderby_expression())); - - if (ctx.ASC() != null) { - builder.append(QueryTokens.expression(ctx.ASC())); - } else if (ctx.DESC() != null) { - builder.append(QueryTokens.expression(ctx.DESC())); - } - - if (ctx.nullsPrecedence() != null) { - builder.appendExpression(visit(ctx.nullsPrecedence())); - } + builder.append(QueryTokens.expression(ctx.FROM())); + builder.appendExpression( + QueryTokenStream.concat(ctx.subselect_identification_variable_declaration(), this::visit, TOKEN_COMMA)); return builder; } @Override - public QueryTokenStream visitOrderby_expression(JpqlParser.Orderby_expressionContext ctx) { - - if (ctx.state_field_path_expression() != null) { - return visit(ctx.state_field_path_expression()); - } else if (ctx.general_identification_variable() != null) { - return visit(ctx.general_identification_variable()); - } else if (ctx.string_expression() != null) { - return visit(ctx.string_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); + public QueryTokenStream visitConditional_primary(JpqlParser.Conditional_primaryContext ctx) { + + if (ctx.conditional_expression() != null) { + return QueryTokenStream.group(visit(ctx.conditional_expression())); } - return QueryTokenStream.empty(); + return super.visitConditional_primary(ctx); } @Override - public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) { + public QueryTokenStream visitIn_expression(JpqlParser.In_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.expression(ctx.NULLS())); + if (ctx.string_expression() != null) { + builder.appendExpression(visit(ctx.string_expression())); + } - if (ctx.FIRST() != null) { - builder.append(QueryTokens.expression(ctx.FIRST())); - } else if (ctx.LAST() != null) { - builder.append(QueryTokens.expression(ctx.LAST())); + if (ctx.type_discriminator() != null) { + builder.appendExpression(visit(ctx.type_discriminator())); } - return builder; - } - - @Override - public QueryTokenStream visitSet_fuction(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(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; - } - - @Override - public QueryTokenStream visitSubquery(JpqlParser.SubqueryContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.simple_select_clause())); - builder.appendExpression(visit(ctx.subquery_from_clause())); - - if (ctx.where_clause() != null) { - builder.appendExpression(visit(ctx.where_clause())); - } - if (ctx.groupby_clause() != null) { - builder.appendExpression(visit(ctx.groupby_clause())); - } - if (ctx.having_clause() != null) { - builder.appendExpression(visit(ctx.having_clause())); - } - - return builder; - } - - @Override - public QueryTokenStream visitSubquery_from_clause(JpqlParser.Subquery_from_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.FROM())); - builder.appendExpression( - QueryTokenStream.concat(ctx.subselect_identification_variable_declaration(), this::visit, TOKEN_COMMA)); - - return builder; - } - - @Override - public QueryTokenStream visitSubselect_identification_variable_declaration( - JpqlParser.Subselect_identification_variable_declarationContext ctx) { - return super.visitSubselect_identification_variable_declaration(ctx); - } - - @Override - public QueryTokenStream visitDerived_path_expression(JpqlParser.Derived_path_expressionContext ctx) { - return super.visitDerived_path_expression(ctx); - } - - @Override - public QueryTokenStream visitGeneral_derived_path(JpqlParser.General_derived_pathContext ctx) { - return super.visitGeneral_derived_path(ctx); - } - - @Override - public QueryTokenStream visitSimple_derived_path(JpqlParser.Simple_derived_pathContext ctx) { - return super.visitSimple_derived_path(ctx); - } - - @Override - public QueryTokenStream visitTreated_derived_path(JpqlParser.Treated_derived_pathContext ctx) { - return super.visitTreated_derived_path(ctx); - } - - @Override - public QueryTokenStream visitDerived_collection_member_declaration( - JpqlParser.Derived_collection_member_declarationContext ctx) { - return super.visitDerived_collection_member_declaration(ctx); - } - - @Override - public QueryTokenStream visitSimple_select_clause(JpqlParser.Simple_select_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.SELECT())); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); - } - builder.appendExpression(visit(ctx.simple_select_expression())); - - return builder; - } - - @Override - public QueryTokenStream visitSimple_select_expression(JpqlParser.Simple_select_expressionContext ctx) { - - if (ctx.single_valued_path_expression() != null) { - return visit(ctx.single_valued_path_expression()); - } else if (ctx.scalar_expression() != null) { - return visit(ctx.scalar_expression()); - } else if (ctx.aggregate_expression() != null) { - return visit(ctx.aggregate_expression()); - } else if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitScalar_expression(JpqlParser.Scalar_expressionContext ctx) { - - if (ctx.arithmetic_expression() != null) { - return visit(ctx.arithmetic_expression()); - } else if (ctx.string_expression() != null) { - return visit(ctx.string_expression()); - } else if (ctx.enum_expression() != null) { - return visit(ctx.enum_expression()); - } else if (ctx.datetime_expression() != null) { - return visit(ctx.datetime_expression()); - } else if (ctx.boolean_expression() != null) { - return visit(ctx.boolean_expression()); - } else if (ctx.case_expression() != null) { - return visit(ctx.case_expression()); - } else if (ctx.entity_type_expression() != null) { - return visit(ctx.entity_type_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitConditional_expression(JpqlParser.Conditional_expressionContext ctx) { - - if (ctx.conditional_expression() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.conditional_expression())); - builder.append(QueryTokens.expression(ctx.OR())); - builder.appendExpression(visit(ctx.conditional_term())); - - return builder; - } else { - return visit(ctx.conditional_term()); - } - } - - @Override - public QueryTokenStream visitConditional_term(JpqlParser.Conditional_termContext ctx) { - - if (ctx.conditional_term() != null) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.conditional_term())); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.conditional_factor())); - - return builder; - } else { - return visit(ctx.conditional_factor()); - } - } - - @Override - public QueryTokenStream visitConditional_factor(JpqlParser.Conditional_factorContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(visit(ctx.conditional_primary())); - - return builder; - } - - @Override - public QueryTokenStream visitConditional_primary(JpqlParser.Conditional_primaryContext ctx) { - - if (ctx.simple_cond_expression() != null) { - return visit(ctx.simple_cond_expression()); - } - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.conditional_expression() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.conditional_expression())); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitSimple_cond_expression(JpqlParser.Simple_cond_expressionContext ctx) { - - if (ctx.comparison_expression() != null) { - return visit(ctx.comparison_expression()); - } else if (ctx.between_expression() != null) { - return visit(ctx.between_expression()); - } else if (ctx.in_expression() != null) { - return visit(ctx.in_expression()); - } else if (ctx.like_expression() != null) { - return visit(ctx.like_expression()); - } else if (ctx.null_comparison_expression() != null) { - return visit(ctx.null_comparison_expression()); - } else if (ctx.empty_collection_comparison_expression() != null) { - return visit(ctx.empty_collection_comparison_expression()); - } else if (ctx.collection_member_expression() != null) { - return visit(ctx.collection_member_expression()); - } else if (ctx.exists_expression() != null) { - return visit(ctx.exists_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitBetween_expression(JpqlParser.Between_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.arithmetic_expression(0) != null) { - - builder.appendExpression(visit(ctx.arithmetic_expression(0))); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.arithmetic_expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.arithmetic_expression(2))); - - } else if (ctx.string_expression(0) != null) { - - builder.appendExpression(visit(ctx.string_expression(0))); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.string_expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.string_expression(2))); - - } else if (ctx.datetime_expression(0) != null) { - - builder.appendExpression(visit(ctx.datetime_expression(0))); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.BETWEEN())); - builder.appendExpression(visit(ctx.datetime_expression(1))); - builder.append(QueryTokens.expression(ctx.AND())); - builder.appendExpression(visit(ctx.datetime_expression(2))); - } - - return builder; - } - - @Override - public QueryTokenStream visitIn_expression(JpqlParser.In_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.string_expression() != null) { - builder.appendExpression(visit(ctx.string_expression())); - } - if (ctx.type_discriminator() != null) { - builder.appendExpression(visit(ctx.type_discriminator())); - } - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - if (ctx.IN() != null) { - builder.append(QueryTokens.expression(ctx.IN())); - } - - if (ctx.in_item() != null && !ctx.in_item().isEmpty()) { - - 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) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.collection_valued_input_parameter() != null) { - builder.append(visit(ctx.collection_valued_input_parameter())); - } - - return builder; - } - - @Override - public QueryTokenStream visitIn_item(JpqlParser.In_itemContext ctx) { - - if (ctx.literal() != null) { - return visit(ctx.literal()); - } else if (ctx.string_expression() != null) { - return visit(ctx.string_expression()); - } else if (ctx.boolean_literal() != null) { - return visit(ctx.boolean_literal()); - } else if (ctx.numeric_literal() != null) { - return visit(ctx.numeric_literal()); - } else if (ctx.date_time_timestamp_literal() != null) { - return visit(ctx.date_time_timestamp_literal()); - } else if (ctx.single_valued_input_parameter() != null) { - return visit(ctx.single_valued_input_parameter()); - } else if (ctx.conditional_expression() != null) { - return visit(ctx.conditional_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitLike_expression(JpqlParser.Like_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.string_expression())); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - builder.append(QueryTokens.expression(ctx.LIKE())); - builder.appendExpression(visit(ctx.pattern_value())); - - if (ctx.ESCAPE() != null) { - - builder.append(QueryTokens.expression(ctx.ESCAPE())); - builder.appendExpression(visit(ctx.escape_character())); - } - - return builder; - } - - @Override - public QueryTokenStream visitNull_comparison_expression(JpqlParser.Null_comparison_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.single_valued_path_expression() != null) { - builder.appendExpression(visit(ctx.single_valued_path_expression())); - } else if (ctx.input_parameter() != null) { - builder.appendExpression(visit(ctx.input_parameter())); - } - - builder.append(QueryTokens.expression(ctx.IS())); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.NULL())); - - return builder; - } - - @Override - public QueryTokenStream visitEmpty_collection_comparison_expression( - JpqlParser.Empty_collection_comparison_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.collection_valued_path_expression())); - builder.append(QueryTokens.expression(ctx.IS())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - builder.append(QueryTokens.expression(ctx.EMPTY())); - - return builder; - } - - @Override - public QueryTokenStream visitCollection_member_expression(JpqlParser.Collection_member_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.entity_or_value_expression())); - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - builder.append(QueryTokens.expression(ctx.MEMBER())); - if (ctx.OF() != null) { - builder.append(QueryTokens.expression(ctx.OF())); - } - builder.append(visit(ctx.collection_valued_path_expression())); - - return builder; - } - - @Override - public QueryTokenStream visitEntity_or_value_expression(JpqlParser.Entity_or_value_expressionContext ctx) { - - if (ctx.single_valued_object_path_expression() != null) { - return visit(ctx.single_valued_object_path_expression()); - } else if (ctx.state_field_path_expression() != null) { - return visit(ctx.state_field_path_expression()); - } else if (ctx.simple_entity_or_value_expression() != null) { - return visit(ctx.simple_entity_or_value_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSimple_entity_or_value_expression( - JpqlParser.Simple_entity_or_value_expressionContext ctx) { - - if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } else if (ctx.literal() != null) { - return visit(ctx.literal()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitExists_expression(JpqlParser.Exists_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.NOT() != null) { - builder.append(QueryTokens.expression(ctx.NOT())); - } - - builder.append(QueryTokens.expression(ctx.EXISTS())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitAll_or_any_expression(JpqlParser.All_or_any_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.ALL() != null) { - builder.append(QueryTokens.expression(ctx.ALL())); - } else if (ctx.ANY() != null) { - builder.append(QueryTokens.expression(ctx.ANY())); - } else if (ctx.SOME() != null) { - builder.append(QueryTokens.expression(ctx.SOME())); - } - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitStringComparison(JpqlParser.StringComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.string_expression(0))); - builder.appendInline(visit(ctx.comparison_operator())); - - if (ctx.string_expression(1) != null) { - builder.append(visit(ctx.string_expression(1))); - } else { - builder.append(visit(ctx.all_or_any_expression())); + if (ctx.NOT() != null) { + builder.append(QueryTokens.expression(ctx.NOT())); } - return builder; - } - - @Override - public QueryTokenStream visitBooleanComparison(JpqlParser.BooleanComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.boolean_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.boolean_expression(1) != null) { - builder.append(visit(ctx.boolean_expression(1))); - } else { - builder.append(visit(ctx.all_or_any_expression())); + if (ctx.IN() != null) { + builder.append(QueryTokens.expression(ctx.IN())); } - return builder; - } - - @Override - public QueryTokenStream visitDirectBooleanCheck(JpqlParser.DirectBooleanCheckContext ctx) { - return visit(ctx.boolean_expression()); - } - - @Override - public QueryTokenStream visitEnumComparison(JpqlParser.EnumComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.enum_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - - if (ctx.enum_expression(1) != null) { - builder.append(visit(ctx.enum_expression(1))); - } else { - builder.append(visit(ctx.all_or_any_expression())); + if (ctx.in_item() != null && !ctx.in_item().isEmpty()) { + builder.append(QueryTokenStream.group(QueryTokenStream.concat(ctx.in_item(), this::visit, TOKEN_COMMA))); + } else if (ctx.subquery() != null) { + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); + } else if (ctx.collection_valued_input_parameter() != null) { + builder.append(visit(ctx.collection_valued_input_parameter())); } return builder; } @Override - public QueryTokenStream visitDatetimeComparison(JpqlParser.DatetimeComparisonContext ctx) { + public QueryTokenStream visitExists_expression(JpqlParser.Exists_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendInline(visit(ctx.datetime_expression(0))); - builder.appendInline(visit(ctx.comparison_operator())); - - if (ctx.datetime_expression(1) != null) { - builder.append(visit(ctx.datetime_expression(1))); - } else { - builder.append(visit(ctx.all_or_any_expression())); + if (ctx.NOT() != null) { + builder.append(QueryTokens.expression(ctx.NOT())); } - return builder; - } - - @Override - public QueryTokenStream visitEntityComparison(JpqlParser.EntityComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.entity_expression(0))); - builder.append(QueryTokens.expression(ctx.op)); - - if (ctx.entity_expression(1) != null) { - builder.append(visit(ctx.entity_expression(1))); - } else { - builder.append(visit(ctx.all_or_any_expression())); - } + builder.append(QueryTokens.expression(ctx.EXISTS())); + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); return builder; } @Override - public QueryTokenStream visitArithmeticComparison(JpqlParser.ArithmeticComparisonContext ctx) { + public QueryTokenStream visitAll_or_any_expression(JpqlParser.All_or_any_expressionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.appendInline(visit(ctx.comparison_operator())); - - if (ctx.arithmetic_expression(1) != null) { - builder.append(visit(ctx.arithmetic_expression(1))); - } else { - builder.append(visit(ctx.all_or_any_expression())); + if (ctx.ALL() != null) { + builder.append(QueryTokens.expression(ctx.ALL())); + } else if (ctx.ANY() != null) { + builder.append(QueryTokens.expression(ctx.ANY())); + } else if (ctx.SOME() != null) { + builder.append(QueryTokens.expression(ctx.SOME())); } - return builder; - } - - @Override - public QueryTokenStream visitEntityTypeComparison(JpqlParser.EntityTypeComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendInline(visit(ctx.entity_type_expression(0))); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.append(visit(ctx.entity_type_expression(1))); - - return builder; - } - - @Override - public QueryTokenStream visitRegexpComparison(JpqlParser.RegexpComparisonContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.appendExpression(visit(ctx.string_expression())); - builder.append(QueryTokens.expression(ctx.REGEXP())); - builder.appendExpression(visit(ctx.string_literal())); + builder.append(QueryTokenStream.group(visit(ctx.subquery()))); return builder; } - @Override - public QueryTokenStream visitComparison_operator(JpqlParser.Comparison_operatorContext ctx) { - return QueryTokenStream.from(QueryTokens.ventilated(ctx.op)); - } - - @Override - public QueryTokenStream visitArithmetic_expression(JpqlParser.Arithmetic_expressionContext ctx) { - - if (ctx.arithmetic_expression() != null) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(visit(ctx.arithmetic_expression())); - builder.append(QueryTokens.ventilated(ctx.op)); - builder.append(visit(ctx.arithmetic_term())); - return builder; - - } else { - return visit(ctx.arithmetic_term()); - } - } - - @Override - public QueryTokenStream visitArithmetic_term(JpqlParser.Arithmetic_termContext ctx) { - - 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 { - return visit(ctx.arithmetic_factor()); - } - } - @Override public QueryTokenStream visitArithmetic_factor(JpqlParser.Arithmetic_factorContext ctx) { @@ -1622,39 +647,13 @@ public QueryTokenStream visitArithmetic_factor(JpqlParser.Arithmetic_factorConte @Override public QueryTokenStream visitArithmetic_primary(JpqlParser.Arithmetic_primaryContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.numeric_literal() != null) { - builder.append(visit(ctx.numeric_literal())); - } else if (ctx.arithmetic_expression() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression())); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.functions_returning_numerics() != null) { - builder.append(visit(ctx.functions_returning_numerics())); - } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.arithmetic_cast_function() != null) { - builder.append(visit(ctx.arithmetic_cast_function())); - } else if (ctx.type_cast_function() != null) { - builder.append(visit(ctx.type_cast_function())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); + if (ctx.arithmetic_expression() != null) { + return QueryTokenStream.group(visit(ctx.arithmetic_expression())); } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitArithmetic_primary(ctx); } @Override @@ -1662,154 +661,41 @@ public QueryTokenStream visitString_expression(JpqlParser.String_expressionConte QueryRendererBuilder builder = QueryRenderer.builder(); - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.string_literal() != null) { - builder.append(visit(ctx.string_literal())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.functions_returning_strings() != null) { - builder.append(visit(ctx.functions_returning_strings())); - } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.string_cast_function() != null) { - builder.append(visit(ctx.string_cast_function())); - } else if (ctx.type_cast_function() != null) { - builder.append(visit(ctx.type_cast_function())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); - } else if (ctx.subquery() != null) { - - 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))); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitString_expression(ctx); } @Override public QueryTokenStream visitDatetime_expression(JpqlParser.Datetime_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.functions_returning_datetime() != null) { - builder.append(visit(ctx.functions_returning_datetime())); - } else if (ctx.aggregate_expression() != null) { - builder.append(visit(ctx.aggregate_expression())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); - } else if (ctx.date_time_timestamp_literal() != null) { - builder.append(visit(ctx.date_time_timestamp_literal())); - } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitDatetime_expression(ctx); } @Override public QueryTokenStream visitBoolean_expression(JpqlParser.Boolean_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.boolean_literal() != null) { - builder.append(visit(ctx.boolean_literal())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.function_invocation() != null) { - builder.append(visit(ctx.function_invocation())); - } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return builder; + return super.visitBoolean_expression(ctx); } @Override public QueryTokenStream visitEnum_expression(JpqlParser.Enum_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.state_valued_path_expression() != null) { - builder.append(visit(ctx.state_valued_path_expression())); - } else if (ctx.enum_literal() != null) { - builder.append(visit(ctx.enum_literal())); - } else if (ctx.input_parameter() != null) { - builder.append(visit(ctx.input_parameter())); - } else if (ctx.case_expression() != null) { - builder.append(visit(ctx.case_expression())); - } else if (ctx.subquery() != null) { - - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.subquery())); - builder.append(TOKEN_CLOSE_PAREN); - } - - return builder; - } - - @Override - public QueryTokenStream visitEntity_expression(JpqlParser.Entity_expressionContext ctx) { - - if (ctx.single_valued_object_path_expression() != null) { - return visit(ctx.single_valued_object_path_expression()); - } else if (ctx.simple_entity_expression() != null) { - return visit(ctx.simple_entity_expression()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitSimple_entity_expression(JpqlParser.Simple_entity_expressionContext ctx) { - - if (ctx.identification_variable() != null) { - return visit(ctx.identification_variable()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitEntity_type_expression(JpqlParser.Entity_type_expressionContext ctx) { - - if (ctx.type_discriminator() != null) { - return visit(ctx.type_discriminator()); - } else if (ctx.entity_type_literal() != null) { - return visit(ctx.entity_type_literal()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); + if (ctx.subquery() != null) { + return QueryTokenStream.group(visit(ctx.subquery())); } - return QueryTokenStream.empty(); + return super.visitEnum_expression(ctx); } @Override @@ -1817,19 +703,15 @@ public QueryTokenStream visitType_discriminator(JpqlParser.Type_discriminatorCon QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.TYPE())); - builder.append(TOKEN_OPEN_PAREN); - if (ctx.general_identification_variable() != null) { - builder.appendInline(visit(ctx.general_identification_variable())); + builder.append(visit(ctx.general_identification_variable())); } else if (ctx.single_valued_object_path_expression() != null) { - builder.appendInline(visit(ctx.single_valued_object_path_expression())); + builder.append(visit(ctx.single_valued_object_path_expression())); } else if (ctx.input_parameter() != null) { - builder.appendInline(visit(ctx.input_parameter())); + builder.append(visit(ctx.input_parameter())); } - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.TYPE(), builder); } @Override @@ -1838,101 +720,58 @@ public QueryTokenStream visitFunctions_returning_numerics(JpqlParser.Functions_r QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.LENGTH() != null) { - - builder.append(QueryTokens.token(ctx.LENGTH())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.LENGTH(), visit(ctx.string_expression(0))); } else if (ctx.LOCATE() != null) { - builder.append(QueryTokens.token(ctx.LOCATE())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.string_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.string_expression(1))); + if (ctx.arithmetic_expression() != null) { builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(0))); } - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.ABS() != null) { - builder.append(QueryTokens.token(ctx.ABS())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.LOCATE(), builder); + } else if (ctx.ABS() != null) { + return QueryTokenStream.ofFunction(ctx.ABS(), visit(ctx.arithmetic_expression(0))); } else if (ctx.CEILING() != null) { - - builder.append(QueryTokens.token(ctx.CEILING())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.CEILING(), visit(ctx.arithmetic_expression(0))); } else if (ctx.EXP() != null) { - - builder.append(QueryTokens.token(ctx.EXP())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.EXP(), visit(ctx.arithmetic_expression(0))); } else if (ctx.FLOOR() != null) { - - builder.append(QueryTokens.token(ctx.FLOOR())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.FLOOR(), visit(ctx.arithmetic_expression(0))); } else if (ctx.LN() != null) { - - builder.append(QueryTokens.token(ctx.LN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.LN(), visit(ctx.arithmetic_expression(0))); } else if (ctx.SIGN() != null) { - - builder.append(QueryTokens.token(ctx.SIGN())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.SIGN(), visit(ctx.arithmetic_expression(0))); } else if (ctx.SQRT() != null) { - - builder.append(QueryTokens.token(ctx.SQRT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.arithmetic_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.SQRT(), visit(ctx.arithmetic_expression(0))); } else if (ctx.MOD() != null) { - builder.append(QueryTokens.token(ctx.MOD())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); + + return QueryTokenStream.ofFunction(ctx.MOD(), builder); } else if (ctx.POWER() != null) { - builder.append(QueryTokens.token(ctx.POWER())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); + + return QueryTokenStream.ofFunction(ctx.POWER(), builder); } else if (ctx.ROUND() != null) { - builder.append(QueryTokens.token(ctx.ROUND())); - builder.append(TOKEN_OPEN_PAREN); builder.appendInline(visit(ctx.arithmetic_expression(0))); builder.append(TOKEN_COMMA); builder.appendInline(visit(ctx.arithmetic_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.SIZE() != null) { - builder.append(QueryTokens.token(ctx.SIZE())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.collection_valued_path_expression())); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.ROUND(), builder); + } else if (ctx.SIZE() != null) { + return QueryTokenStream.ofFunction(ctx.SIZE(), visit(ctx.collection_valued_path_expression())); } else if (ctx.INDEX() != null) { - - builder.append(QueryTokens.token(ctx.INDEX())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.identification_variable())); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.INDEX(), visit(ctx.identification_variable())); } else if (ctx.extract_datetime_field() != null) { builder.append(visit(ctx.extract_datetime_field())); } @@ -1940,139 +779,76 @@ public QueryTokenStream visitFunctions_returning_numerics(JpqlParser.Functions_r return builder; } - @Override - public QueryTokenStream visitFunctions_returning_datetime(JpqlParser.Functions_returning_datetimeContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - if (ctx.CURRENT_DATE() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT_DATE())); - } else if (ctx.CURRENT_TIME() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT_TIME())); - } else if (ctx.CURRENT_TIMESTAMP() != null) { - builder.append(QueryTokens.expression(ctx.CURRENT_TIMESTAMP())); - } else if (ctx.LOCAL() != null) { - - builder.append(QueryTokens.expression(ctx.LOCAL())); - - if (ctx.DATE() != null) { - builder.append(QueryTokens.expression(ctx.DATE())); - } else if (ctx.TIME() != null) { - builder.append(QueryTokens.expression(ctx.TIME())); - } 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; - } - @Override public QueryTokenStream visitFunctions_returning_strings(JpqlParser.Functions_returning_stringsContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); if (ctx.CONCAT() != null) { - - builder.append(QueryTokens.token(ctx.CONCAT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); + return QueryTokenStream.ofFunction(ctx.CONCAT(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); } else if (ctx.SUBSTRING() != null) { - 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) { + builder.appendInline(QueryTokenStream.concat(ctx.arithmetic_expression(), this::visit, TOKEN_COMMA)); - builder.append(QueryTokens.token(ctx.TRIM())); - builder.append(TOKEN_OPEN_PAREN); + return QueryTokenStream.ofFunction(ctx.SUBSTRING(), builder); + } else if (ctx.TRIM() != null) { - QueryRendererBuilder nested = QueryRenderer.builder(); if (ctx.trim_specification() != null) { - nested.appendExpression(visit(ctx.trim_specification())); + builder.appendExpression(visit(ctx.trim_specification())); } if (ctx.trim_character() != null) { - nested.appendExpression(visit(ctx.trim_character())); + builder.appendExpression(visit(ctx.trim_character())); } if (ctx.FROM() != null) { - nested.append(QueryTokens.expression(ctx.FROM())); + builder.append(QueryTokens.expression(ctx.FROM())); } - nested.append(visit(ctx.string_expression(0))); - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - } else if (ctx.LOWER() != null) { - builder.append(QueryTokens.token(ctx.LOWER())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.string_expression(0))); - builder.append(TOKEN_CLOSE_PAREN); + builder.append(visit(ctx.string_expression(0))); + + return QueryTokenStream.ofFunction(ctx.TRIM(), builder); + } else if (ctx.LOWER() != null) { + return QueryTokenStream.ofFunction(ctx.LOWER(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); } else if (ctx.UPPER() != null) { + return QueryTokenStream.ofFunction(ctx.UPPER(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); + } else if (ctx.LEFT() != null) { - builder.append(QueryTokens.token(ctx.UPPER())); - 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); + builder.append(visit(ctx.arithmetic_expression(0))); + + return QueryTokenStream.ofFunction(ctx.LEFT(), builder); } 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); + builder.append(visit(ctx.arithmetic_expression(0))); + + return QueryTokenStream.ofFunction(ctx.RIGHT(), builder); } 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 QueryTokenStream.ofFunction(ctx.REPLACE(), + QueryTokenStream.concat(ctx.string_expression(), this::visit, TOKEN_COMMA)); } return builder; } - @Override - public QueryTokenStream visitTrim_specification(JpqlParser.Trim_specificationContext ctx) { - - if (ctx.LEADING() != null) { - return QueryTokenStream.ofToken(ctx.LEADING()); - } else if (ctx.TRAILING() != null) { - return QueryTokenStream.ofToken(ctx.TRAILING()); - } else { - return QueryTokenStream.ofToken(ctx.BOTH()); - } - } - @Override public QueryTokenStream visitArithmetic_cast_function(JpqlParser.Arithmetic_cast_functionContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.string_expression())); if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } builder.append(QueryTokens.token(ctx.f)); - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.CAST(), builder); } @Override @@ -2080,8 +856,6 @@ public QueryTokenStream visitType_cast_function(JpqlParser.Type_cast_functionCon QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); if (ctx.AS() != null) { @@ -2096,9 +870,8 @@ public QueryTokenStream visitType_cast_function(JpqlParser.Type_cast_functionCon builder.appendInline(QueryTokenStream.concat(ctx.numeric_literal(), this::visit, TOKEN_COMMA)); builder.append(TOKEN_CLOSE_PAREN); } - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.CAST(), builder); } @Override @@ -2106,16 +879,13 @@ public QueryTokenStream visitString_cast_function(JpqlParser.String_cast_functio QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.token(ctx.CAST())); - builder.append(TOKEN_OPEN_PAREN); builder.appendExpression(visit(ctx.scalar_expression())); if (ctx.AS() != null) { builder.append(QueryTokens.expression(ctx.AS())); } builder.append(QueryTokens.token(ctx.STRING())); - builder.append(TOKEN_CLOSE_PAREN); - return builder; + return QueryTokenStream.ofFunction(ctx.CAST(), builder); } @Override @@ -2138,19 +908,13 @@ public QueryTokenStream visitFunction_invocation(JpqlParser.Function_invocationC @Override public QueryTokenStream visitExtract_datetime_field(JpqlParser.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.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; + return QueryTokenStream.ofFunction(ctx.EXTRACT(), nested); } @Override @@ -2161,208 +925,27 @@ public QueryTokenStream visitDatetime_field(JpqlParser.Datetime_fieldContext ctx @Override 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.token(ctx.EXTRACT())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(nested); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitDatetime_part(JpqlParser.Datetime_partContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitFunction_arg(JpqlParser.Function_argContext ctx) { - - if (ctx.literal() != null) { - return visit(ctx.literal()); - } else if (ctx.state_valued_path_expression() != null) { - return visit(ctx.state_valued_path_expression()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } else { - return visit(ctx.scalar_expression()); - } - } - - @Override - public QueryTokenStream visitCase_expression(JpqlParser.Case_expressionContext ctx) { - - if (ctx.general_case_expression() != null) { - return visit(ctx.general_case_expression()); - } else if (ctx.simple_case_expression() != null) { - return visit(ctx.simple_case_expression()); - } else if (ctx.coalesce_expression() != null) { - return visit(ctx.coalesce_expression()); - } else { - return visit(ctx.nullif_expression()); - } - } - - @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) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CASE())); - builder.appendExpression(QueryTokenStream.concat(ctx.when_clause(), this::visit, TOKEN_SPACE)); - - builder.append(QueryTokens.expression(ctx.ELSE())); - builder.appendExpression(visit(ctx.scalar_expression())); - builder.append(QueryTokens.expression(ctx.END())); - - return builder; - } - - @Override - public QueryTokenStream visitWhen_clause(JpqlParser.When_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.conditional_expression())); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.scalar_expression())); - - return builder; - } - - @Override - public QueryTokenStream visitSimple_case_expression(JpqlParser.Simple_case_expressionContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.CASE())); - 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.appendExpression(visit(ctx.scalar_expression())); - builder.append(QueryTokens.expression(ctx.END())); - - return builder; - } - - @Override - public QueryTokenStream visitCase_operand(JpqlParser.Case_operandContext ctx) { - - if (ctx.state_valued_path_expression() != null) { - return visit(ctx.state_valued_path_expression()); - } else { - return visit(ctx.type_discriminator()); - } - } - - @Override - public QueryTokenStream visitSimple_when_clause(JpqlParser.Simple_when_clauseContext ctx) { - - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.expression(ctx.WHEN())); - builder.appendExpression(visit(ctx.scalar_expression(0))); - builder.append(QueryTokens.expression(ctx.THEN())); - builder.appendExpression(visit(ctx.scalar_expression(1))); - - return builder; + return QueryTokenStream.ofFunction(ctx.EXTRACT(), nested); } @Override public QueryTokenStream visitCoalesce_expression(JpqlParser.Coalesce_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.COALESCE())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(QueryTokenStream.concat(ctx.scalar_expression(), this::visit, TOKEN_COMMA)); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; + return QueryTokenStream.ofFunction(ctx.COALESCE(), + QueryTokenStream.concat(ctx.scalar_expression(), this::visit, TOKEN_COMMA)); } @Override public QueryTokenStream visitNullif_expression(JpqlParser.Nullif_expressionContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); - - builder.append(QueryTokens.token(ctx.NULLIF())); - builder.append(TOKEN_OPEN_PAREN); - builder.appendInline(visit(ctx.scalar_expression(0))); - builder.append(TOKEN_COMMA); - builder.appendInline(visit(ctx.scalar_expression(1))); - builder.append(TOKEN_CLOSE_PAREN); - - return builder; - } - - @Override - public QueryTokenStream visitTrim_character(JpqlParser.Trim_characterContext ctx) { - - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.character_valued_input_parameter() != null) { - return visit(ctx.character_valued_input_parameter()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitIdentification_variable(JpqlParser.Identification_variableContext ctx) { - - if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); - } else if (ctx.type_literal() != null) { - return visit(ctx.type_literal()); - } else if (ctx.f != null) { - return QueryTokenStream.ofToken(ctx.f); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitConstructor_name(JpqlParser.Constructor_nameContext ctx) { - return visit(ctx.entity_name()); - } - - @Override - public QueryTokenStream visitLiteral(JpqlParser.LiteralContext ctx) { - - if (ctx.STRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); - } else if (ctx.JAVASTRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.JAVASTRINGLITERAL()); - } else if (ctx.INTLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTLITERAL()); - } else if (ctx.FLOATLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); - } else if (ctx.LONGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.LONGLITERAL()); - } else if (ctx.boolean_literal() != null) { - return visit(ctx.boolean_literal()); - } else if (ctx.entity_type_literal() != null) { - return visit(ctx.entity_type_literal()); - } - - return QueryTokenStream.empty(); + return QueryTokenStream.ofFunction(ctx.NULLIF(), + QueryTokenStream.concat(ctx.scalar_expression(), this::visit, TOKEN_COMMA)); } @Override @@ -2383,193 +966,25 @@ public QueryTokenStream visitInput_parameter(JpqlParser.Input_parameterContext c return builder; } - @Override - public QueryTokenStream visitPattern_value(JpqlParser.Pattern_valueContext ctx) { - return visit(ctx.string_expression()); - } - - @Override - public QueryTokenStream visitDate_time_timestamp_literal(JpqlParser.Date_time_timestamp_literalContext ctx) { - - if (ctx.STRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); - } else if (ctx.DATELITERAL() != null) { - return QueryTokenStream.ofToken(ctx.DATELITERAL()); - } else if (ctx.TIMELITERAL() != null) { - return QueryTokenStream.ofToken(ctx.TIMELITERAL()); - } else if (ctx.TIMESTAMPLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.TIMESTAMPLITERAL()); - } else { - return QueryRenderer.builder(); - } - } - - @Override - public QueryTokenStream visitEntity_type_literal(JpqlParser.Entity_type_literalContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitEscape_character(JpqlParser.Escape_characterContext ctx) { - - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.character_valued_input_parameter() != null) { - return visit(ctx.character_valued_input_parameter()); - } else if (ctx.string_literal() != null) { - return visit(ctx.string_literal()); - } - - return QueryTokenStream.empty(); - } - - @Override - public QueryTokenStream visitNumeric_literal(JpqlParser.Numeric_literalContext ctx) { - - if (ctx.INTLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.INTLITERAL()); - } else if (ctx.FLOATLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.FLOATLITERAL()); - } else if (ctx.LONGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.LONGLITERAL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitBoolean_literal(JpqlParser.Boolean_literalContext ctx) { - - if (ctx.TRUE() != null) { - return QueryTokenStream.ofToken(ctx.TRUE()); - } else if (ctx.FALSE() != null) { - return QueryTokenStream.ofToken(ctx.FALSE()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitEnum_literal(JpqlParser.Enum_literalContext ctx) { - return visit(ctx.state_field_path_expression()); - } - - @Override - public QueryTokenStream visitString_literal(JpqlParser.String_literalContext ctx) { - - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.STRINGLITERAL() != null) { - return QueryTokenStream.ofToken(ctx.STRINGLITERAL()); - } else { - return QueryTokenStream.empty(); - } - } - - @Override - public QueryTokenStream visitSingle_valued_embeddable_object_field( - JpqlParser.Single_valued_embeddable_object_fieldContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitSubtype(JpqlParser.SubtypeContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitCollection_valued_field(JpqlParser.Collection_valued_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitSingle_valued_object_field(JpqlParser.Single_valued_object_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitState_field(JpqlParser.State_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitCollection_value_field(JpqlParser.Collection_value_fieldContext ctx) { - - if (ctx.reserved_word() != null) { - return visit(ctx.reserved_word()); - } - - return visit(ctx.identification_variable()); - } - @Override public QueryTokenStream visitEntity_name(JpqlParser.Entity_nameContext ctx) { return QueryTokenStream.concat(ctx.reserved_word(), this::visit, TOKEN_DOT); } @Override - public QueryTokenStream visitResult_variable(JpqlParser.Result_variableContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitSuperquery_identification_variable( - JpqlParser.Superquery_identification_variableContext ctx) { - return visit(ctx.identification_variable()); - } - - @Override - public QueryTokenStream visitCollection_valued_input_parameter( - JpqlParser.Collection_valued_input_parameterContext ctx) { - return visit(ctx.input_parameter()); - } - - @Override - public QueryTokenStream visitSingle_valued_input_parameter(JpqlParser.Single_valued_input_parameterContext ctx) { - return visit(ctx.input_parameter()); - } - - @Override - public QueryTokenStream visitFunction_name(JpqlParser.Function_nameContext ctx) { - return visit(ctx.string_literal()); - } + public QueryTokenStream visitChildren(RuleNode node) { - @Override - public QueryTokenStream visitCharacter_valued_input_parameter( - JpqlParser.Character_valued_input_parameterContext ctx) { + int childCount = node.getChildCount(); - if (ctx.CHARACTER() != null) { - return QueryTokenStream.ofToken(ctx.CHARACTER()); - } else if (ctx.input_parameter() != null) { - return visit(ctx.input_parameter()); - } else { - return QueryTokenStream.empty(); + if (childCount == 1 && node.getChild(0) instanceof RuleContext t) { + return visit(t); } - } - @Override - public QueryTokenStream visitReserved_word(Reserved_wordContext ctx) { - if (ctx.IDENTIFICATION_VARIABLE() != null) { - return QueryTokenStream.ofToken(ctx.IDENTIFICATION_VARIABLE()); - } else if (ctx.f != null) { - return QueryTokenStream.ofToken(ctx.f); - } else { - return QueryTokenStream.empty(); + if (childCount == 1 && node.getChild(0) instanceof TerminalNode t) { + return QueryTokens.token(t); } + + return QueryTokenStream.concatExpressions(node, this::visit); } + } 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 bfbc47b395..2b7109914d 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 @@ -44,13 +44,6 @@ */ abstract class QueryRenderer implements QueryTokenStream { - /** - * Creates a QueryRenderer from a {@link QueryToken}. - */ - static QueryRenderer from(QueryToken token) { - return QueryRenderer.from(Collections.singletonList(token)); - } - /** * Creates a QueryRenderer from a collection of {@link QueryToken}. */ @@ -168,6 +161,10 @@ public String toString() { public static QueryRenderer ofExpression(QueryTokenStream tokenStream) { + if (tokenStream instanceof ExpressionRenderer er) { + return er; + } + if (tokenStream instanceof QueryRendererBuilder builder) { tokenStream = builder.current; } @@ -191,6 +188,10 @@ public static QueryRenderer inline(QueryTokenStream tokenStream) { Assert.notNull(tokenStream, "QueryTokenStream must not be null!"); + if (tokenStream instanceof InlineRenderer ilr) { + return ilr; + } + if (tokenStream instanceof QueryRendererBuilder builder) { tokenStream = builder.current; } @@ -203,10 +204,6 @@ public static QueryRenderer inline(QueryTokenStream tokenStream) { tokenStream = QueryRenderer.from(tokenStream); } - if (!tokenStream.isExpression()) { - return (QueryRenderer) tokenStream; - } - return new InlineRenderer((QueryRenderer) tokenStream); } @@ -335,11 +332,6 @@ public boolean isExpression() { return !nested.isEmpty() && nested.get(nested.size() - 1).isExpression(); } - public Stream renderers() { - return nested.stream() - .flatMap(renderer -> renderer instanceof CompositeRenderer ? ((CompositeRenderer) renderer).renderers() - : Stream.of(renderer)); - } } /** @@ -358,17 +350,6 @@ String render() { return render(tokens); } - @Override - QueryRenderer append(QueryTokenStream tokens) { - - if (tokens instanceof TokenRenderer tr) { - this.tokens.addAll(tr.tokens); - return this; - } - - return super.append(tokens); - } - @Override public Stream stream() { return tokens.stream(); @@ -409,29 +390,6 @@ public boolean isExpression() { return !tokens.isEmpty() && getRequiredLast().isExpression(); } - /** - * Render a list of {@link QueryTokens.SimpleQueryToken}s into a string. - * - * @param tokens - * @return rendered string containing either a query or some subset of that query - */ - static String render(Object tokens) { - - if (tokens instanceof Collection tpr) { - return render(tpr); - } - - if (tokens instanceof QueryRendererBuilder qrb) { - return qrb.build().render(); - } - - if (tokens instanceof QueryRenderer qr) { - return qr.render(); - } - - throw new IllegalArgumentException("Unknown token type %s".formatted(tokens)); - } - } static class QueryStreamRenderer extends QueryRenderer { @@ -486,27 +444,7 @@ static class QueryRendererBuilder implements QueryTokenStream { protected QueryRenderer current = QueryRenderer.empty(); /** - * Create and initialize a QueryRendererBuilder from a {@link QueryTokens.SimpleQueryToken}. - * - * @param token - * @return - */ - public static QueryRendererBuilder builder(QueryToken token) { - return new QueryRendererBuilder().append(token); - } - - /** - * Append a {@link QueryTokens.SimpleQueryToken}. - * - * @param token - * @return {@code this} builder. - */ - QueryRendererBuilder append(QueryToken token) { - return append(QueryRenderer.from(token)); - } - - /** - * Append a collection of {@link QueryTokens.SimpleQueryToken}. + * Append a collection of {@link QueryToken}s. * * @param tokens * @return {@code this} builder. @@ -515,16 +453,6 @@ QueryRendererBuilder append(List tokens) { return append(QueryRenderer.from(tokens)); } - /** - * Append a QueryRendererBuilder as expression. - * - * @param builder - * @return {@code this} builder. - */ - QueryRendererBuilder appendExpression(QueryRendererBuilder builder) { - return appendExpression(builder.current); - } - /** * Append a QueryRenderer. * @@ -559,6 +487,16 @@ QueryRendererBuilder appendInline(QueryTokenStream stream) { return this; } + /** + * Append a QueryRendererBuilder as expression. + * + * @param builder + * @return {@code this} builder. + */ + QueryRendererBuilder appendExpression(QueryRendererBuilder builder) { + return appendExpression(builder.current); + } + /** * Append a QueryRenderer as expression. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryToken.java index ac67b8bf0d..7cf9ccbcb3 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryToken.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryToken.java @@ -22,7 +22,7 @@ * @author Christoph Strobl * @since 3.4 */ -interface QueryToken { +interface QueryToken extends QueryTokenStream { /** * @return the token value (i.e. its content). 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 44b5c55271..c1b01daf14 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 @@ -18,13 +18,13 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.Function; -import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; +import org.antlr.v4.runtime.tree.Tree; import org.jspecify.annotations.Nullable; import org.springframework.data.util.Streamable; @@ -46,30 +46,6 @@ static QueryTokenStream empty() { return EmptyQueryTokenStream.INSTANCE; } - /** - * Creates a QueryTokenStream from a {@link QueryToken}. - * @since 4.0 - */ - static QueryTokenStream from(QueryToken token) { - return QueryRenderer.from(Collections.singletonList(token)); - } - - /** - * Creates an token QueryRenderer from an AST {@link TerminalNode}. - * @since 4.0 - */ - static QueryTokenStream ofToken(TerminalNode node) { - return from(QueryTokens.token(node)); - } - - /** - * Creates an token QueryRenderer from an AST {@link Token}. - * @since 4.0 - */ - static QueryTokenStream ofToken(Token node) { - return from(QueryTokens.token(node)); - } - /** * Compose a {@link QueryTokenStream} from a collection of inline elements. * @@ -97,22 +73,24 @@ static QueryTokenStream concatExpressions(Collection elements, Function QueryTokenStream concatExpressions(Collection elements, Function visitor) { + if (CollectionUtils.isEmpty(elements)) { + return QueryTokenStream.empty(); + } + QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); for (T child : elements) { - if (child instanceof Token t) { - builder.append(QueryTokens.expression(t)); - } else if (child instanceof TerminalNode tn) { + if (child instanceof TerminalNode tn) { builder.append(QueryTokens.expression(tn)); } else { builder.appendExpression(visitor.apply(child)); @@ -122,6 +100,39 @@ static QueryTokenStream concatExpressions(Collection elements, Function visitor) { + + int childCount = elements.getChildCount(); + if (childCount == 0) { + return QueryTokenStream.empty(); + } + + QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); + + for (int i = 0; i < childCount; i++) { + + Tree child = elements.getChild(i); + if (child instanceof TerminalNode tn) { + builder.append(QueryTokens.expression(tn)); + } else if (child instanceof ParseTree pt) { + builder.appendExpression(visitor.apply(pt)); + } else { + throw new IllegalArgumentException("Unsupported child type: " + child); + } + } + + return builder.build(); + } + /** * Compose a {@link QueryTokenStream} from a collection of elements. * @@ -168,12 +179,30 @@ static QueryTokenStream concat(Collection elements, Function iterator() { + return Collections.singleton((QueryToken) this).iterator(); + } + + @Override + public boolean isExpression() { + return false; + } + @Override public int hashCode() { return value().hashCode(); @@ -161,8 +176,10 @@ static class ExpressionToken extends SimpleQueryToken { super(token); } + @Override public boolean isExpression() { return true; } } + } From d1dc6dc3538ba20726754a63ceb54819ac869342 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Jul 2025 14:49:55 +0200 Subject: [PATCH 142/224] Add support for HQL XML functions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support the xmlelement(…), xmlforest(…), xmlpi(…), xmlquery(…), xmlexists(…), xmlagg(…), and xmltable(…) functions. Closes #3883 --- .../data/jpa/repository/query/Hql.g4 | 18 ++- .../repository/query/HqlQueryRenderer.java | 138 +++++++++++++++++- .../query/HqlQueryRendererTests.java | 70 ++++++++- 3 files changed, 215 insertions(+), 11 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 7d61f25572..c0fbf35ee0 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 @@ -1337,11 +1337,7 @@ jsonObjectAggFunction : JSON_OBJECTAGG '(' KEY? expressionOrPredicate (VALUE | ':') expressionOrPredicate jsonNullClause? jsonUniqueKeysClause? ')' filterClause?; jsonPassingClause - : PASSING jsonPassingItem (',' jsonPassingItem)* - ; - -jsonPassingItem - : expressionOrPredicate AS identifier + : PASSING aliasedExpressionOrPredicate (',' aliasedExpressionOrPredicate)* ; jsonNullClause @@ -1387,11 +1383,11 @@ xmlElementFunction ; xmlAttributesFunction - : XMLATTRIBUTES '(' expressionOrPredicate AS identifier (',' expressionOrPredicate AS identifier)* ')' + : XMLATTRIBUTES '(' aliasedExpressionOrPredicate (',' aliasedExpressionOrPredicate)* ')' ; xmlForestFunction - : XMLFOREST '(' expressionOrPredicate (AS identifier)? (',' expressionOrPredicate (AS identifier)?)* ')' + : XMLFOREST '(' potentiallyAliasedExpressionOrPredicate (',' potentiallyAliasedExpressionOrPredicate)* ')' ; xmlPiFunction @@ -1406,6 +1402,14 @@ xmlExistsFunction xmlAggFunction : XMLAGG '(' expression orderByClause? ')' filterClause? overClause?; +aliasedExpressionOrPredicate + : expressionOrPredicate AS identifier + ; + +potentiallyAliasedExpressionOrPredicate + : expressionOrPredicate (AS identifier)? + ; + xmlTableFunction : XMLTABLE '(' expression PASSING expression xmlTableColumnsClause ')'; 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 2949dfbc02..58b5a3cb3a 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 @@ -1447,7 +1447,7 @@ public QueryTokenStream visitJsonPassingClause(HqlParser.JsonPassingClauseContex QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.PASSING())); - builder.append(QueryTokenStream.concat(ctx.jsonPassingItem(), this::visit, TOKEN_COMMA)); + builder.append(QueryTokenStream.concat(ctx.aliasedExpressionOrPredicate(), this::visit, TOKEN_COMMA)); return builder; } @@ -1482,6 +1482,142 @@ public QueryTokenStream visitJsonTableColumns(HqlParser.JsonTableColumnsContext return QueryTokenStream.concat(ctx.jsonTableColumn(), this::visit, TOKEN_COMMA); } + @Override + public QueryTokenStream visitXmlElementFunction(HqlParser.XmlElementFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.NAME())); + builder.append(visit(ctx.identifier())); + + if (ctx.xmlAttributesFunction() != null) { + builder.append(TOKEN_COMMA); + builder.append(visit(ctx.xmlAttributesFunction())); + } + + if (!CollectionUtils.isEmpty(ctx.expressionOrPredicate())) { + builder.append(TOKEN_COMMA); + builder.append(QueryTokenStream.concat(ctx.expressionOrPredicate(), this::visit, TOKEN_COMMA)); + } + + return QueryTokenStream.ofFunction(ctx.XMLELEMENT(), builder); + } + + @Override + public QueryTokenStream visitXmlAttributesFunction(HqlParser.XmlAttributesFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(QueryTokenStream.concat(ctx.aliasedExpressionOrPredicate(), this::visit, TOKEN_COMMA)); + + return QueryTokenStream.ofFunction(ctx.XMLATTRIBUTES(), builder); + } + + @Override + public QueryTokenStream visitXmlForestFunction(HqlParser.XmlForestFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression( + QueryTokenStream.concat(ctx.potentiallyAliasedExpressionOrPredicate(), this::visit, TOKEN_COMMA)); + + return QueryTokenStream.ofFunction(ctx.XMLFOREST(), builder); + } + + @Override + public QueryTokenStream visitXmlPiFunction(HqlParser.XmlPiFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.NAME())); + builder.append(visit(ctx.identifier())); + + if (ctx.expression() != null) { + builder.append(TOKEN_COMMA); + builder.append(visit(ctx.expression())); + } + + return QueryTokenStream.ofFunction(ctx.XMLPI(), builder); + } + + @Override + public QueryTokenStream visitXmlQueryFunction(HqlParser.XmlQueryFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.expression(0))); + builder.append(QueryTokens.expression(ctx.PASSING())); + builder.appendExpression(visit(ctx.expression(1))); + + return QueryTokenStream.ofFunction(ctx.XMLQUERY(), builder); + } + + @Override + public QueryTokenStream visitXmlExistsFunction(HqlParser.XmlExistsFunctionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.appendExpression(visit(ctx.expression(0))); + builder.append(QueryTokens.expression(ctx.PASSING())); + builder.appendExpression(visit(ctx.expression(1))); + + return QueryTokenStream.ofFunction(ctx.XMLEXISTS(), builder); + } + + @Override + public QueryTokenStream visitXmlAggFunction(HqlParser.XmlAggFunctionContext ctx) { + + QueryRendererBuilder args = QueryRenderer.builder(); + + args.appendExpression(visit(ctx.expression())); + if (ctx.orderByClause() != null) { + args.appendExpression(visit(ctx.orderByClause())); + } + + QueryTokenStream function = QueryTokenStream.ofFunction(ctx.XMLAGG(), args); + + if (ctx.filterClause() == null && ctx.overClause() == null) { + return function; + } + + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.appendExpression(function); + + if (ctx.filterClause() != null) { + builder.appendExpression(visit(ctx.filterClause())); + } + + if (ctx.overClause() != null) { + builder.appendExpression(visit(ctx.overClause())); + } + + return builder; + } + + @Override + public QueryTokenStream visitXmlTableFunction(HqlParser.XmlTableFunctionContext ctx) { + + QueryRendererBuilder args = QueryRenderer.builder(); + + args.appendExpression(visit(ctx.expression(0))); + args.append(QueryTokens.expression(ctx.PASSING())); + args.appendExpression(visit(ctx.expression(1))); + args.appendExpression(visit(ctx.xmlTableColumnsClause())); + + return QueryTokenStream.ofFunction(ctx.XMLTABLE(), args); + } + + @Override + public QueryTokenStream visitXmlTableColumnsClause(HqlParser.XmlTableColumnsClauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.COLUMNS())); + builder.append(QueryTokenStream.concat(ctx.xmlTableColumn(), this::visit, TOKEN_COMMA)); + + return builder; + } + @Override public QueryTokenStream visitPath(HqlParser.PathContext ctx) { return QueryTokenStream.concat(ctx.children, this::visit, EMPTY_TOKEN); 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 cab9df2957..1ecbfb1e56 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 @@ -26,9 +26,10 @@ import org.junit.jupiter.params.provider.ValueSource; /** - * 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
        + * Tests built around examples of HQL found in ... and + * ...
        *
        * IMPORTANT: Purely verifies the parser without any transformations. * @@ -2757,4 +2758,67 @@ join lateral json_table(e.json, '$' columns(theInt Integer, nonExisting exists) ERROR ON ERROR) """); } + + @Test // GH-3883 + void xmlElement() { + + assertQuery("select xmlelement(name myelement)"); + assertQuery( + "select xmlelement(name `my-element`, xmlattributes(123 as attr1, '456' as `attr-2`), 'myContent', xmlelement(name empty))"); + } + + @Test // GH-3883 + void xmlForest() { + + assertQuery("select xmlforest(123 as e1)"); + assertQuery("select xmlforest(123 as e1, 'text' as e2)"); + } + + @Test // GH-3883 + void xmlPi() { + + assertQuery("select xmlpi(name php)"); + assertQuery("select xmlpi(name php, foo)"); + } + + @Test // GH-3883 + void xmlQuery() { + + assertQuery("select xmlquery('/a/val' passing 'asd')"); + assertQuery("select xmlquery('/a/val' passing e.xml) from Entity e"); + } + + @Test // GH-3883 + void xmlExists() { + + assertQuery("select xmlexists('/a/val' passing 'asd')"); + assertQuery("select xmlexists('/a/val' passing e.xml) from Entity e"); + } + + @Test // GH-3883 + void xmlAgg() { + + assertQuery("select xmlagg(xmlelement(name a, e.theString))"); + assertQuery( + "select xmlagg(xmlelement(name a, e.theString) order by e.id) FILTER (WHERE foo = bar) OVER (PARTITION BY expression) from Entity e"); + } + + @Test // GH-3883 + void xmlTable() { + + assertQuery(""" + select + t.nonExistingWithDefault + from xmltable('/root/elem' passing :xml columns theInt Integer, + theFloat Float, + theString String, + theBoolean Boolean, + theNull String, + theObject XML, + theNestedString String path 'theObject/nested', + nonExisting String, + nonExistingWithDefault String default 'none') t + """); + } + } From a53eb8b81db868ddc5823862442838f8f22dff84 Mon Sep 17 00:00:00 2001 From: Hyunsang Han Date: Fri, 30 May 2025 16:45:36 +0900 Subject: [PATCH 143/224] Enable AOT repository generation by default. Signed-off-by: Hyunsang Han Original pull request: #3904 Closes #3899 --- pom.xml | 7 ++ spring-data-jpa/pom.xml | 6 ++ .../config/JpaRepositoryConfigExtension.java | 6 +- ...toryRegistrationAotProcessorUnitTests.java | 88 +++++++++++++++++-- .../antora/modules/ROOT/pages/jpa/aot.adoc | 7 +- 5 files changed, 104 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 17ffd2313d..ee52971922 100755 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@

        2.3.232

        3.2.0 5.2 + 2.3.0 9.2.0 42.7.7 23.8.0.25.04 @@ -175,6 +176,12 @@ pom import + + org.junit-pioneer + junit-pioneer + ${junit-pioneer} + test + diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index b9c7df6084..6a1aea6fa2 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -100,6 +100,12 @@ test + + org.junit-pioneer + junit-pioneer + test + + org.springframework spring-core-test 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 6b8069418b..22fe839f5c 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 @@ -40,6 +40,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aot.AotDetector; import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; @@ -92,6 +93,7 @@ * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Hyunsang Han */ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { @@ -379,8 +381,10 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi Environment environment = repositoryContext.getEnvironment(); + String enabledByDefault = AotDetector.useGeneratedArtifacts() ? "true" : "false"; + boolean enabled = Boolean - .parseBoolean(environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + .parseBoolean(environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, enabledByDefault)); if (!enabled) { 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 82c06f9687..e3825d671e 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 @@ -30,6 +30,9 @@ import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; +import org.junitpioneer.jupiter.SetSystemProperty; +import org.springframework.aot.AotDetector; import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GenerationContext; @@ -56,14 +59,14 @@ /** * @author Christoph Strobl + * @author Hyunsang Han */ class JpaRepositoryRegistrationAotProcessorUnitTests { @Test // GH-2628 void aotProcessorMustNotRegisterDomainTypes() { - GenerationContext ctx = new DefaultGenerationContext(new ClassNameGenerator(ClassName.OBJECT), - new InMemoryGeneratedFiles()); + GenerationContext ctx = createGenerationContext(); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() .contribute(new DummyAotRepositoryContext(null) { @@ -79,8 +82,7 @@ public Set> getResolvedTypes() { @Test // GH-2628 void aotProcessorMustNotRegisterAnnotations() { - GenerationContext ctx = new DefaultGenerationContext(new ClassNameGenerator(ClassName.OBJECT), - new InMemoryGeneratedFiles()); + GenerationContext ctx = createGenerationContext(); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() .contribute(new DummyAotRepositoryContext(null) { @@ -99,8 +101,7 @@ public Set> getResolvedAnnotations() { @Test // GH-3838 void repositoryProcessorShouldConsiderPersistenceManagedTypes() { - GenerationContext ctx = new DefaultGenerationContext(new ClassNameGenerator(ClassName.OBJECT), - new InMemoryGeneratedFiles()); + GenerationContext ctx = createGenerationContext(); GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(PersistenceManagedTypes.class, () -> { @@ -126,12 +127,83 @@ public List getManagedPackages() { context.getEnvironment().getPropertySources() .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); - JpaRepositoryContributor contributor = new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(context), ctx); + JpaRepositoryContributor contributor = createContributor(new DummyAotRepositoryContext(context), ctx); assertThat(contributor.getMetamodel().managedType(Person.class)).isNotNull(); } + @Test // GH-3899 + @SetSystemProperty(key = AotDetector.AOT_ENABLED, value = "true") + void repositoryProcessorShouldEnableAotRepositoriesByDefaultWhenAotIsEnabled() { + + GenerationContext ctx = createGenerationContext(); + GenericApplicationContext context = createApplicationContext(); + + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + + assertThat(contributor).isNotNull(); + } + + @Test // GH-3899 + @ClearSystemProperty(key = AotDetector.AOT_ENABLED) + void repositoryProcessorShouldNotEnableAotRepositoriesByDefaultWhenAotIsDisabled() { + + GenerationContext ctx = createGenerationContext(); + GenericApplicationContext context = createApplicationContext(); + + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + + assertThat(contributor).isNull(); + } + + @Test // GH-3899 + @SetSystemProperty(key = AotDetector.AOT_ENABLED, value = "true") + @SetSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED, value = "false") + void repositoryProcessorShouldRespectExplicitRepositoryEnabledProperty() { + + GenerationContext ctx = createGenerationContext(); + GenericApplicationContext context = createApplicationContext(); + + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + + assertThat(contributor).isNull(); + } + + @Test // GH-3899 + @SetSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED, value = "true") + void repositoryProcessorShouldEnableWhenExplicitlySetToTrue() { + + GenerationContext ctx = createGenerationContext(); + GenericApplicationContext context = createApplicationContext(); + + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + + assertThat(contributor).isNotNull(); + } + + private GenerationContext createGenerationContext() { + return new DefaultGenerationContext(new ClassNameGenerator(ClassName.OBJECT), + new InMemoryGeneratedFiles()); + } + + private GenericApplicationContext createApplicationContext() { + return new GenericApplicationContext(); + } + + private JpaRepositoryContributor createContributor(AotRepositoryContext repositoryContext, GenerationContext ctx) { + return new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() + .contribute(repositoryContext, ctx); + } + + private JpaRepositoryContributor createContributorWithPersonTypes(GenericApplicationContext context, GenerationContext ctx) { + return createContributor(new DummyAotRepositoryContext(context) { + @Override + public Set> getResolvedTypes() { + return Collections.singleton(Person.class); + } + }, ctx); + } + @Entity static class Person { @Id Long id; diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc index e8d2d4a1a8..67fce33498 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -44,7 +44,12 @@ Do not use them directly in your code as generation and implementation details m === 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`. +When AOT is enabled (either for native compilation or by setting `spring.aot.enabled=true`), AOT repositories are automatically enabled by default. + +You can explicitly control AOT repository generation by setting the `spring.aot.repositories.enabled` property: + +* `spring.aot.repositories.enabled=true` - Explicitly enable AOT repositories +* `spring.aot.repositories.enabled=false` - Disable AOT repositories even when AOT is enabled AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment. From ccaa32b81d73d3c237d4be08405f02ff54b68072 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Jul 2025 16:21:18 +0200 Subject: [PATCH 144/224] Polishing. Refine documentation wording. Use build-managed JUnit pioneer version property. Use Commons-infrastructure for checking whether AOT repositories are enabled. Original pull request: #3904 See #3899 --- pom.xml | 7 --- spring-data-jpa/pom.xml | 1 + .../config/JpaRepositoryConfigExtension.java | 21 +++----- ...toryRegistrationAotProcessorUnitTests.java | 54 +++++++++---------- .../antora/modules/ROOT/pages/jpa/aot.adoc | 6 +-- 5 files changed, 39 insertions(+), 50 deletions(-) diff --git a/pom.xml b/pom.xml index ee52971922..17ffd2313d 100755 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,6 @@

        2.3.232

        3.2.0 5.2 - 2.3.0 9.2.0 42.7.7 23.8.0.25.04 @@ -176,12 +175,6 @@ pom import
        - - org.junit-pioneer - junit-pioneer - ${junit-pioneer} - test - diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 6a1aea6fa2..cbec8a2645 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -103,6 +103,7 @@ org.junit-pioneer junit-pioneer + ${junit-pioneer} test 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 22fe839f5c..d2db31b79a 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 @@ -40,7 +40,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.aot.AotDetector; import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; @@ -59,7 +58,6 @@ import org.springframework.core.io.ResourceLoader; 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.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.DefaultJpaContext; @@ -363,6 +361,7 @@ static boolean isActive(@Nullable ClassLoader classLoader) { return AGENT_CLASSES.stream() // .anyMatch(agentClass -> ClassUtils.isPresent(agentClass, classLoader)); } + } /** @@ -374,25 +373,19 @@ 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"; + private static final String USE_ENTITY_MANAGER = "spring.aot.jpa.repositories.use-entitymanager"; + private static final String MODULE_NAME = "jpa"; protected @Nullable JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { - Environment environment = repositoryContext.getEnvironment(); - - String enabledByDefault = AotDetector.useGeneratedArtifacts() ? "true" : "false"; - - boolean enabled = Boolean - .parseBoolean(environment.getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, enabledByDefault)); - if (!enabled) { + if (!repositoryContext.isGeneratedRepositoriesEnabled(MODULE_NAME)) { return null; } ConfigurableListableBeanFactory beanFactory = repositoryContext.getBeanFactory(); - - boolean useEntityManager = Boolean - .parseBoolean(environment.getProperty(GENERATED_REPOSITORIES_JPA_USE_ENTITY_MANAGER, "false")); + Environment environment = repositoryContext.getEnvironment(); + boolean useEntityManager = environment.getProperty(USE_ENTITY_MANAGER, Boolean.class, false); if (useEntityManager) { @@ -430,5 +423,7 @@ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegi log.debug("Using scanned types for AOT repository generation"); return new JpaRepositoryContributor(repositoryContext); } + } + } 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 e3825d671e..5f442fcf7b 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 @@ -29,9 +29,9 @@ import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; - import org.junitpioneer.jupiter.ClearSystemProperty; import org.junitpioneer.jupiter.SetSystemProperty; + import org.springframework.aot.AotDetector; import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; @@ -58,8 +58,11 @@ import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; /** + * Unit tests for {@link JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor}. + * * @author Christoph Strobl * @author Hyunsang Han + * @author Mark Paluch */ class JpaRepositoryRegistrationAotProcessorUnitTests { @@ -67,9 +70,10 @@ class JpaRepositoryRegistrationAotProcessorUnitTests { void aotProcessorMustNotRegisterDomainTypes() { GenerationContext ctx = createGenerationContext(); + GenericApplicationContext context = new GenericApplicationContext(); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(null) { + .contribute(new DummyAotRepositoryContext(context) { @Override public Set> getResolvedTypes() { return Collections.singleton(Person.class); @@ -84,8 +88,9 @@ void aotProcessorMustNotRegisterAnnotations() { GenerationContext ctx = createGenerationContext(); + GenericApplicationContext context = new GenericApplicationContext(); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(null) { + .contribute(new DummyAotRepositoryContext(context) { @Override public Set> getResolvedAnnotations() { @@ -127,7 +132,8 @@ public List getManagedPackages() { context.getEnvironment().getPropertySources() .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); - JpaRepositoryContributor contributor = createContributor(new DummyAotRepositoryContext(context), ctx); + JpaRepositoryContributor contributor = new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() + .contribute(new DummyAotRepositoryContext(context), ctx); assertThat(contributor.getMetamodel().managedType(Person.class)).isNotNull(); } @@ -137,7 +143,7 @@ public List getManagedPackages() { void repositoryProcessorShouldEnableAotRepositoriesByDefaultWhenAotIsEnabled() { GenerationContext ctx = createGenerationContext(); - GenericApplicationContext context = createApplicationContext(); + GenericApplicationContext context = new GenericApplicationContext(); JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); @@ -145,24 +151,23 @@ void repositoryProcessorShouldEnableAotRepositoriesByDefaultWhenAotIsEnabled() { } @Test // GH-3899 - @ClearSystemProperty(key = AotDetector.AOT_ENABLED) - void repositoryProcessorShouldNotEnableAotRepositoriesByDefaultWhenAotIsDisabled() { + @ClearSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED) + void shouldEnableAotRepositoriesByDefault() { GenerationContext ctx = createGenerationContext(); - GenericApplicationContext context = createApplicationContext(); + GenericApplicationContext context = new GenericApplicationContext(); JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); - assertThat(contributor).isNull(); + assertThat(contributor).isNotNull(); } @Test // GH-3899 - @SetSystemProperty(key = AotDetector.AOT_ENABLED, value = "true") @SetSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED, value = "false") - void repositoryProcessorShouldRespectExplicitRepositoryEnabledProperty() { + void shouldDisableAotRepositoriesWhenGeneratedRepositoriesIsFalse() { GenerationContext ctx = createGenerationContext(); - GenericApplicationContext context = createApplicationContext(); + GenericApplicationContext context = new GenericApplicationContext(); JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); @@ -170,15 +175,15 @@ void repositoryProcessorShouldRespectExplicitRepositoryEnabledProperty() { } @Test // GH-3899 - @SetSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED, value = "true") - void repositoryProcessorShouldEnableWhenExplicitlySetToTrue() { + @SetSystemProperty(key = "spring.aot.jpa.repositories.enabled", value = "false") + void shouldDisableAotRepositoriesWhenJpaGeneratedRepositoriesIsFalse() { GenerationContext ctx = createGenerationContext(); - GenericApplicationContext context = createApplicationContext(); + GenericApplicationContext context = new GenericApplicationContext(); JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); - assertThat(contributor).isNotNull(); + assertThat(contributor).isNull(); } private GenerationContext createGenerationContext() { @@ -186,17 +191,10 @@ private GenerationContext createGenerationContext() { new InMemoryGeneratedFiles()); } - private GenericApplicationContext createApplicationContext() { - return new GenericApplicationContext(); - } + private JpaRepositoryContributor createContributorWithPersonTypes(GenericApplicationContext context, GenerationContext ctx) { - private JpaRepositoryContributor createContributor(AotRepositoryContext repositoryContext, GenerationContext ctx) { return new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(repositoryContext, ctx); - } - - private JpaRepositoryContributor createContributorWithPersonTypes(GenericApplicationContext context, GenerationContext ctx) { - return createContributor(new DummyAotRepositoryContext(context) { + .contribute(new DummyAotRepositoryContext(context) { @Override public Set> getResolvedTypes() { return Collections.singleton(Person.class); @@ -252,12 +250,12 @@ public RepositoryInformation getRepositoryInformation() { @Override public Set> getResolvedAnnotations() { - return null; + return Set.of(); } @Override public Set> getResolvedTypes() { - return null; + return Set.of(); } @Override @@ -279,5 +277,7 @@ public TypeIntrospector introspectType(String typeName) { public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return null; } + } + } diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc index 67fce33498..5d71d15e31 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -46,10 +46,10 @@ Do not use them directly in your code as generation and implementation details m AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode. When AOT is enabled (either for native compilation or by setting `spring.aot.enabled=true`), AOT repositories are automatically enabled by default. -You can explicitly control AOT repository generation by setting the `spring.aot.repositories.enabled` property: +You can disable AOT repository generation entirely or only disable JPA AOT repositories: -* `spring.aot.repositories.enabled=true` - Explicitly enable AOT repositories -* `spring.aot.repositories.enabled=false` - Disable AOT repositories even when AOT is enabled +* Set the `spring.aot.repositories.enabled=false` property to disable generated repositories for all Spring Data modules. +* Set the `spring.aot.jpa.repositories.enabled=false` property to disable only JPA AOT repositories. AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment. From f4faeab8053b1414b09acffe0b54fad28c38c467 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 08:42:33 +0200 Subject: [PATCH 145/224] Upgrade to JSqlparser 5.3. Closes #3938 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 17ffd2313d..01031d201a 100755 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 2.7.4

        2.3.232

        3.2.0 - 5.2 + 5.3 9.2.0 42.7.7 23.8.0.25.04 From 2fbb9574d49fa36363a68d9787bf6cbed680892b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 10:37:44 +0200 Subject: [PATCH 146/224] Upgrade to Hibernate 7.0.6.Final. Closes #3933 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 01031d201a..9f1119d49a 100755 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 4.13.2 5.0.0-B07 5.0.0-SNAPSHOT - 7.0.5.Final - 7.0.6-SNAPSHOT + 7.0.6.Final + 7.0.7-SNAPSHOT 7.1.0-SNAPSHOT 2.7.4

        2.3.232

        From c1070bf0856a72885739634db91ec24f56d164cd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 10:44:27 +0200 Subject: [PATCH 147/224] Upgrade to Eclipselink 5.0.0-B09. Closes #3939 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9f1119d49a..cc81dac232 100755 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ 4.13.2 - 5.0.0-B07 + 5.0.0-B09 5.0.0-SNAPSHOT 7.0.6.Final 7.0.7-SNAPSHOT From f283b2e9cdd5df1f77b67b6cb9b7eb988a788bb8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 11:10:46 +0200 Subject: [PATCH 148/224] Polishing. Improve named query detection and query extraction for Eclipselink. See #3939 --- .../jpa/provider/PersistenceProvider.java | 32 ++++++++++++++++++- .../query/HqlOrderExpressionVisitor.java | 7 +++- .../data/jpa/repository/query/NamedQuery.java | 11 ++++--- 3 files changed, 44 insertions(+), 6 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 a0bc1606ae..7b85d0f5d0 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 @@ -35,7 +35,10 @@ import java.util.stream.Stream; import org.eclipse.persistence.config.QueryHints; +import org.eclipse.persistence.internal.queries.DatabaseQueryMechanism; +import org.eclipse.persistence.internal.queries.JPQLCallQueryMechanism; import org.eclipse.persistence.jpa.JpaQuery; +import org.eclipse.persistence.queries.DatabaseQuery; import org.eclipse.persistence.queries.ScrollableCursor; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; @@ -50,6 +53,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; /** * Enumeration representing persistence providers to be used. @@ -137,11 +141,37 @@ public long getResultCount(Query resultQuery, LongSupplier countSupplier) { @Override public String extractQueryString(Object query) { - return ((JpaQuery) query).getDatabaseQuery().getJPQLString(); + + if (query instanceof JpaQuery jpaQuery) { + + DatabaseQuery databaseQuery = jpaQuery.getDatabaseQuery(); + + if (StringUtils.hasText(databaseQuery.getJPQLString())) { + return databaseQuery.getJPQLString(); + } + + if (StringUtils.hasText(databaseQuery.getSQLString())) { + return databaseQuery.getSQLString(); + } + } + + return ""; } @Override public boolean isNativeQuery(Object query) { + + if (query instanceof JpaQuery jpaQuery) { + + DatabaseQueryMechanism call = jpaQuery.getDatabaseQuery().getQueryMechanism(); + + if (call instanceof JPQLCallQueryMechanism) { + return false; + } + + return true; + } + return false; } 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 e1ed4997f8..b9905d242c 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 @@ -210,6 +210,7 @@ public Expression visitIsBooleanPredicate(HqlParser.IsBooleanPredicateContext @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; @@ -224,8 +225,10 @@ public Expression visitStringPatternMatching(HqlParser.StringPatternMatchingC ? cb.notLike(condition, match) // : cb.notLike(condition, match, escape); } - } else { + } else if (ctx.ILIKE() != null && cb instanceof HibernateCriteriaBuilder) { + HibernateCriteriaBuilder hcb = (HibernateCriteriaBuilder) cb; + if (ctx.NOT() == null) { return escape == null // ? hcb.ilike(condition, match) // @@ -235,6 +238,8 @@ public Expression visitStringPatternMatching(HqlParser.StringPatternMatchingC ? hcb.notIlike(condition, match) // : hcb.notIlike(condition, match, escape); } + } else { + throw new UnsupportedOperationException("Unsupported string pattern: " + ctx.getText()); } } 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 125ec40c66..9bfbb750ee 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 @@ -90,17 +90,20 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, JpaQueryConfiguratio throw QueryCreationException.create(method, CANNOT_EXTRACT_QUERY); } + boolean nativeQuery = method.isNativeQuery() || extractor.isNativeQuery(namedQuery); + String queryString = extractor.extractQueryString(namedQuery); + if (parameters.hasPageableParameter()) { + LOG.warn(String.format( "Query method %s is backed by a NamedQuery but contains a Pageable parameter; Sorting delivered via this Pageable will not be applied; Use @%s(value=…) instead to apply sorting.", - method, method.isNativeQuery() ? "NativeQuery" : "Query")); + method, nativeQuery ? "NativeQuery" : "Query")); } - String queryString = extractor.extractQueryString(namedQuery); - + // || namedQuery.toString().contains("NativeQuery") DeclaredQuery declaredQuery; if (StringUtils.hasText(queryString)) { - if (method.isNativeQuery() || namedQuery.toString().contains("NativeQuery")) { + if (nativeQuery) { declaredQuery = DeclaredQuery.nativeQuery(queryString); } else { declaredQuery = DeclaredQuery.jpqlQuery(queryString); From 6b5ac2e6ddfa371b1eeccb1308228fc29fd3ca32 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 11:16:37 +0200 Subject: [PATCH 149/224] Polishing. Add missing since tag. See #3942 --- .../java/org/springframework/data/jpa/domain/Specification.java | 1 + 1 file changed, 1 insertion(+) 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 25a5fb2ce2..b32dc1fa67 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 @@ -57,6 +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}. + * @since 4.0 */ static Specification unrestricted() { return (root, query, builder) -> null; From 0b581c5d8fd7207656426861018e8543ea36113d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 17 Jul 2025 13:59:51 +0200 Subject: [PATCH 150/224] Upgrade to Maven Wrapper 3.9.11. See #3945 --- .mvn/wrapper/maven-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8eb4cb3b3d..df07464f75 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -#Thu Nov 07 09:47:17 CET 2024 -distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +#Thu Jul 17 13:59:51 CEST 2025 +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip From c5e95b28437b57863ad21d6203fe9ce252b34554 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 12:43:29 +0200 Subject: [PATCH 151/224] Add AOT fragment integration tests for `UserRepositoryTests`. Add support for properties-based named queries. Align AOT implementation with reflective repository behavior. Closes #3950 --- .../data/jpa/repository/aot/AotQueries.java | 8 + .../aot/AotRepositoryFragmentSupport.java | 60 +++++++ .../jpa/repository/aot/JpaCodeBlocks.java | 155 +++++++++++++----- .../aot/JpaRepositoryContributor.java | 31 ++-- .../jpa/repository/aot/QueriesFactory.java | 107 +++++++++--- .../jpa/repository/aot/StringAotQuery.java | 27 +++ .../config/JpaRepositoryConfigExtension.java | 2 +- .../repository/query/JpaResultConverters.java | 8 +- .../repository/query/ParameterBinding.java | 10 +- .../repository/AotUserRepositoryTests.java | 129 +++++++++++++++ .../AotFragmentTestConfigurationSupport.java | 45 ++++- .../aot/StubRepositoryInformation.java | 138 ---------------- .../aot/TestJpaAotRepositoryContext.java | 44 +++-- 13 files changed, 512 insertions(+), 252 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java delete mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java 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 51d639ea78..5c2c1ea1a6 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 @@ -101,6 +101,10 @@ public Map serialize() { serialized.put("query", sq.getQueryString()); } + if (result() instanceof StringAotQuery.NamedStringAotQuery nsq) { + serialized.put("name", nsq.getQueryName()); + } + if (paging) { if (count() instanceof NamedAotQuery nq) { @@ -112,6 +116,10 @@ public Map serialize() { if (count() instanceof StringAotQuery sq) { serialized.put("count-query", sq.getQueryString()); } + + if (count() instanceof StringAotQuery.NamedStringAotQuery nsq) { + serialized.put("count-name", nsq.getQueryName()); + } } return serialized; 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 f5c9d16edb..cb16ed8702 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 @@ -17,19 +17,27 @@ import jakarta.persistence.Tuple; +import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; +import java.util.Optional; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.domain.Slice; 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.JpaResultConverters; import org.springframework.data.jpa.repository.query.QueryEnhancer; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; import org.springframework.data.jpa.util.TupleBackedMap; @@ -50,6 +58,19 @@ */ public class AotRepositoryFragmentSupport { + private static final ConversionService CONVERSION_SERVICE; + + static { + + ConfigurableConversionService conversionService = new DefaultConversionService(); + + conversionService.addConverter(JpaResultConverters.BlobToByteArrayConverter.INSTANCE); + conversionService.removeConvertible(Collection.class, Object.class); + conversionService.removeConvertible(Object.class, Optional.class); + + CONVERSION_SERVICE = conversionService; + } + private final RepositoryMetadata repositoryMetadata; private final ValueExpressionDelegate valueExpressions; @@ -111,6 +132,41 @@ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class returnedT return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies())); } + protected @Nullable Object mapIgnoreCase(@Nullable Object source, UnaryOperator mapper) { + + if (source == null) { + return null; + } + + if (source.getClass().isArray()) { + int length = Array.getLength(source); + Collection result = new ArrayList<>(length); + + for (int i = 0; i < length; i++) { + result.add(Array.get(source, i)); + } + source = result; + } + + if (source instanceof Collection c) { + + Collection<@Nullable Object> result = new ArrayList<>(c.size()); + + for (Object o : c) { + + if (o instanceof String s) { + result.add(mapper.apply(s)); + } else { + result.add(o != null ? mapper.apply(o.toString()) : null); + } + } + + return result; + } + + return source; + } + protected @Nullable T convertOne(@Nullable Object result, boolean nativeQuery, Class projection) { if (result == null) { @@ -121,6 +177,10 @@ protected String rewriteQuery(DeclaredQuery query, Sort sort, Class returnedT return projection.cast(result); } + if (CONVERSION_SERVICE.canConvert(result.getClass(), projection)) { + return CONVERSION_SERVICE.convert(result, projection); + } + return projectionFactory.createProjection(projection, result instanceof Tuple t ? new TupleBackedMap(nativeQuery ? TupleBackedMap.underscoreAware(t) : t) : result); } 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 2cb7d332f4..b4a5430582 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 @@ -21,17 +21,21 @@ import jakarta.persistence.Tuple; import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.LongSupplier; import org.jspecify.annotations.Nullable; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.MethodParameter; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Score; import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.QueryHints; @@ -39,6 +43,7 @@ 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.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; @@ -81,6 +86,7 @@ static class QueryBlockBuilder { private final AotQueryMethodGenerationContext context; private final JpaQueryMethod queryMethod; + private final String parameterNames; private String queryVariableName; private @Nullable AotQueries queries; private MergedAnnotation queryHints = MergedAnnotation.missing(); @@ -93,6 +99,14 @@ private QueryBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMetho this.context = context; this.queryMethod = queryMethod; this.queryVariableName = context.localVariable("query"); + + String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", "); + + if (StringUtils.hasText(parameterNames)) { + this.parameterNames = ", " + parameterNames; + } else { + this.parameterNames = ""; + } } public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { @@ -180,10 +194,18 @@ public CodeBlock build() { .localVariable("count%sString".formatted(StringUtils.capitalize(queryVariableName))); builder.add(buildQueryString(sq, countQueryStringNameVariableName)); } + String pageable = context.getPageableParameterName(); + + if (pageable != null) { + String pageableVariableName = context.localVariable("pageable"); + builder.addStatement("$1T $2L = $3L != null ? $3L : $1T.unpaged()", Pageable.class, pageableVariableName, + pageable); + pageable = pageableVariableName; + } String sortParameterName = context.getSortParameterName(); - if (sortParameterName == null && context.getPageableParameterName() != null) { - sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName()); + if (sortParameterName == null && pageable != null) { + sortParameterName = "%s.getSort()".formatted(pageable); } if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType)) @@ -197,9 +219,9 @@ public CodeBlock build() { } builder.add(createQuery(false, queryVariableName, queryStringVariableName, queryRewriterName, queries.result(), - this.sqlResultSetMapping, this.queryHints, this.entityGraph, this.queryReturnType)); + this.sqlResultSetMapping, pageable, this.queryHints, this.entityGraph, this.queryReturnType)); - builder.add(applyLimits(queries.result().isExists())); + builder.add(applyLimits(queries.result().isExists(), pageable)); if (queryMethod.isPageQuery()) { @@ -208,7 +230,7 @@ public CodeBlock build() { boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting"); builder.add(createQuery(true, countQueryVariableName, countQueryStringNameVariableName, queryRewriterName, - queries.count(), null, + queries.count(), null, pageable, queryHints ? this.queryHints : MergedAnnotation.missing(), null, Long.class)); builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQueryVariableName); @@ -262,7 +284,7 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe return builder.build(); } - private CodeBlock applyLimits(boolean exists) { + private CodeBlock applyLimits(boolean exists, String pageable) { Builder builder = CodeBlock.builder(); @@ -282,8 +304,6 @@ private CodeBlock applyLimits(boolean exists) { builder.addStatement("$L.setMaxResults($L)", queryVariableName, queries.result().getLimit().max()); } - String pageable = context.getPageableParameterName(); - if (StringUtils.hasText(pageable)) { builder.beginControlFlow("if ($L.isPaged())", pageable); @@ -301,13 +321,14 @@ private CodeBlock applyLimits(boolean exists) { private CodeBlock createQuery(boolean count, String queryVariableName, @Nullable String queryStringNameVariableName, @Nullable String queryRewriterName, AotQuery query, @Nullable String sqlResultSetMapping, + @Nullable String pageable, MergedAnnotation queryHints, @Nullable AotEntityGraph entityGraph, @Nullable Class queryReturnType) { Builder builder = CodeBlock.builder(); builder.add(doCreateQuery(count, queryVariableName, queryStringNameVariableName, queryRewriterName, query, - sqlResultSetMapping, + sqlResultSetMapping, pageable, queryReturnType)); if (entityGraph != null) { @@ -325,23 +346,75 @@ private CodeBlock createQuery(boolean count, String queryVariableName, @Nullable Object parameterIdentifier = getParameterName(binding.getIdentifier()); String valueFormat = parameterIdentifier instanceof CharSequence ? "$S" : "$L"; + Object parameter = getParameter(binding.getOrigin()); + + if (parameter instanceof String parameterName) { + MethodParameter methodParameter = context.getMethodParameter(parameterName); + if (methodParameter != null) { + parameter = postProcessBindingValue(binding, methodParameter, parameterName); + } + } + if (prepare instanceof String prepared && !prepared.equals("s")) { String format = prepared.replaceAll("%", "%%").replace("s", "%s"); builder.addStatement("$L.setParameter(%s, $S.formatted($L))".formatted(valueFormat), queryVariableName, - parameterIdentifier, format, getParameter(binding.getOrigin())); + parameterIdentifier, format, parameter); } else { builder.addStatement("$L.setParameter(%s, $L)".formatted(valueFormat), queryVariableName, parameterIdentifier, - getParameter(binding.getOrigin())); + parameter); } } return builder.build(); } + private Object postProcessBindingValue(ParameterBinding binding, MethodParameter methodParameter, + String parameterName) { + + Class parameterType = methodParameter.getParameterType(); + if (Score.class.isAssignableFrom(parameterType)) { + return parameterName + ".getValue()"; + } + + if (Vector.class.isAssignableFrom(parameterType)) { + return "%1$s.getType() == Float.TYPE ? %1$s.toFloatArray() : %1$s.toDoubleArray()".formatted(parameterName); + } + + if (binding instanceof ParameterBinding.PartTreeParameterBinding treeBinding) { + + if (treeBinding.isIgnoreCase()) { + + String function = treeBinding.getTemplates() == JpqlQueryTemplates.LOWER ? "toLowerCase" : "toUpperCase"; + + if (isArray(parameterType) || Collection.class.isAssignableFrom(parameterType)) { + return CodeBlock.builder().add("mapIgnoreCase($L, $T::$L)", parameterName, String.class, function).build(); + } + + if (String.class.isAssignableFrom(parameterType)) { + return "%1$s != null ? %1$s.%2$s() : %1$s".formatted(parameterName, function); + } + + return "%1$s != null ? %1$s.toString().%2$s() : %1$s".formatted(parameterName, function); + } + } + + if (isArray(parameterType)) { + return CodeBlock.builder().add("$T.asList($L)", Arrays.class, parameterName).build(); + } + + return parameterName; + } + + private static boolean isArray(Class parameterType) { + return parameterType.isArray() && !parameterType.getComponentType().equals(byte.class) + && !parameterType.getComponentType().equals(Byte.class); + } + private CodeBlock doCreateQuery(boolean count, String queryVariableName, @Nullable String queryStringName, @Nullable String queryRewriterName, AotQuery query, @Nullable String sqlResultSetMapping, + @Nullable String pageable, @Nullable Class queryReturnType) { ReturnedType returnedType = context.getReturnedType(); @@ -354,9 +427,9 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, queryStringNameToUse = queryStringName + "Rewritten"; - if (StringUtils.hasText(context.getPageableParameterName())) { + if (StringUtils.hasText(pageable)) { builder.addStatement("$T $L = $L.rewrite($L, $L)", String.class, queryStringNameToUse, queryRewriterName, - queryStringName, context.getPageableParameterName()); + queryStringName, pageable); } else if (StringUtils.hasText(context.getSortParameterName())) { builder.addStatement("$T $L = $L.rewrite($L, $L)", String.class, queryStringNameToUse, queryRewriterName, queryStringName, context.getSortParameterName()); @@ -452,8 +525,6 @@ private Object getParameter(ParameterBinding.ParameterOrigin origin) { if (origin.isExpression() && origin instanceof ParameterBinding.Expression expr) { Builder builder = CodeBlock.builder(); - ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); - var parameterNames = discoverer.getParameterNames(context.getMethod()); String expressionString = expr.expression().getExpressionString(); // re-wrap expression @@ -461,8 +532,8 @@ private Object getParameter(ParameterBinding.ParameterOrigin origin) { expressionString = "#{" + expressionString + "}"; } - builder.add("evaluateExpression(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", expressionString, - StringUtils.arrayToCommaDelimitedString(parameterNames)); + builder.add("evaluateExpression(ExpressionMarker.class.getEnclosingMethod(), $S$L)", expressionString, + parameterNames); return builder.build(); } @@ -531,6 +602,7 @@ static class QueryExecutionBlockBuilder { private final JpaQueryMethod queryMethod; private @Nullable AotQuery aotQuery; private String queryVariableName; + private @Nullable String pageable; private MergedAnnotation modifying = MergedAnnotation.missing(); private QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { @@ -538,6 +610,7 @@ private QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, JpaQ this.context = context; this.queryMethod = queryMethod; this.queryVariableName = context.localVariable("query"); + this.pageable = context.getPageableParameterName() != null ? context.localVariable("pageable") : null; } public QueryExecutionBlockBuilder referencing(String queryVariableName) { @@ -552,6 +625,12 @@ public QueryExecutionBlockBuilder query(AotQuery aotQuery) { return this; } + public QueryExecutionBlockBuilder query(String pageable) { + + this.pageable = pageable; + return this; + } + public QueryExecutionBlockBuilder modifying(MergedAnnotation modifying) { this.modifying = modifying; @@ -598,22 +677,24 @@ public CodeBlock build() { return builder.build(); } + TypeName queryResultType = TypeName.get(context.getActualReturnType().toClass()); + if (aotQuery != null && aotQuery.isDelete()) { - builder.addStatement("$T<$T> $L = $L.getResultList()", List.class, actualReturnType, + builder.addStatement("$T $L = $L.getResultList()", List.class, context.localVariable("resultList"), queryVariableName); builder.addStatement("$L.forEach($L::remove)", context.localVariable("resultList"), context.fieldNameOf(EntityManager.class)); - if (!context.getReturnType().isAssignableFrom(List.class)) { + if (!Collection.class.isAssignableFrom(context.getReturnType().toClass())) { if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { builder.addStatement("return $T.valueOf($L.size())", context.getMethod().getReturnType(), context.localVariable("resultList")); } else { - builder.addStatement("return $L.isEmpty() ? null : $L.iterator().next()", + builder.addStatement("return ($T) ($L.isEmpty() ? null : $L.iterator().next())", actualReturnType, context.localVariable("resultList"), context.localVariable("resultList")); } } else { - builder.addStatement("return $L", context.localVariable("resultList")); + builder.addStatement("return ($T) $L", List.class, context.localVariable("resultList")); } } else if (aotQuery != null && aotQuery.isExists()) { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); @@ -621,8 +702,6 @@ public CodeBlock build() { if (context.getReturnedType().isProjecting()) { - TypeName queryResultType = TypeName.get(context.getActualReturnType().toClass()); - if (queryMethod.isCollectionQuery()) { builder.addStatement("return ($T) convertMany($L.getResultList(), $L, $T.class)", context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType); @@ -633,20 +712,18 @@ public CodeBlock build() { builder.addStatement( "return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, $L)", PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, aotQuery.isNative(), - queryResultType, context.getPageableParameterName(), context.localVariable("countAll")); + queryResultType, pageable, context.localVariable("countAll")); } else if (queryMethod.isSliceQuery()) { 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 $L = $L.isPaged() && $L.size() > $L.getPageSize()", - context.localVariable("hasNext"), context.getPageableParameterName(), - context.localVariable("resultList"), context.getPageableParameterName()); + context.localVariable("hasNext"), pageable, context.localVariable("resultList"), pageable); builder.addStatement( "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")); + pageable, context.localVariable("resultList"), pageable, context.localVariable("hasNext")); } else { if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { @@ -667,26 +744,26 @@ public CodeBlock build() { } else if (queryMethod.isPageQuery()) { builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, $L)", PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, - context.getPageableParameterName(), context.localVariable("countAll")); + pageable, context.localVariable("countAll")); } else if (queryMethod.isSliceQuery()) { 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()); + context.localVariable("hasNext"), pageable, context.localVariable("resultList"), pageable); builder.addStatement( "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")); + pageable, context.localVariable("resultList"), pageable, context.localVariable("hasNext")); } else { if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) { - builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, - actualReturnType, queryVariableName); + builder.addStatement("return $T.ofNullable(($T) convertOne($L.getSingleResultOrNull(), $L, $T.class))", + Optional.class, actualReturnType, queryVariableName, aotQuery.isNative(), + context.getActualReturnType().toClass()); } else { - builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnTypeName(), - queryVariableName); + builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)", + context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), + 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 564b5a8093..8ae68ee411 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 @@ -87,26 +87,29 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext, Persiste } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { - - super(repositoryContext); - - this.context = repositoryContext; - this.metamodel = entityManagerFactory.getMetamodel(); - this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); - this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory); - this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory); + this(repositoryContext, entityManagerFactory.getMetamodel(), + PersistenceProvider.fromEntityManagerFactory(entityManagerFactory), + new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory, + repositoryContext.getRequiredClassLoader()), + new EntityGraphLookup(entityManagerFactory)); } private JpaRepositoryContributor(AotRepositoryContext repositoryContext, AotMetamodel metamodel) { + this(repositoryContext, metamodel, + PersistenceProvider.fromEntityManagerFactory(metamodel.getEntityManagerFactory()), + new QueriesFactory(repositoryContext.getConfigurationSource(), metamodel.getEntityManagerFactory(), + repositoryContext.getRequiredClassLoader()), + new EntityGraphLookup(metamodel.getEntityManagerFactory())); + } + private JpaRepositoryContributor(AotRepositoryContext repositoryContext, Metamodel metamodel, + PersistenceProvider persistenceProvider, QueriesFactory queriesFactory, EntityGraphLookup entityGraphLookup) { super(repositoryContext); - - this.context = repositoryContext; this.metamodel = metamodel; - this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(metamodel.getEntityManagerFactory()); - this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), - metamodel.getEntityManagerFactory(), metamodel); - this.entityGraphLookup = new EntityGraphLookup(metamodel.getEntityManagerFactory()); + this.persistenceProvider = persistenceProvider; + this.queriesFactory = queriesFactory; + this.entityGraphLookup = entityGraphLookup; + this.context = repositoryContext; } @Override 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 299dc47412..210a02a4fa 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 @@ -20,26 +20,33 @@ import jakarta.persistence.TypedQueryReference; import jakarta.persistence.metamodel.Metamodel; +import java.io.IOException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.function.Function; import java.util.function.UnaryOperator; import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 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.config.JpaRepositoryConfigExtension; 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.config.PropertiesBasedNamedQueriesFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; @@ -54,28 +61,57 @@ class QueriesFactory { private final EntityManagerFactory entityManagerFactory; + private final NamedQueries namedQueries; private final Metamodel metamodel; private final EscapeCharacter escapeCharacter; private final JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER; - public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory) { - this(configurationSource, entityManagerFactory, entityManagerFactory.getMetamodel()); + public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory, + ClassLoader classLoader) { + this(configurationSource, entityManagerFactory, entityManagerFactory.getMetamodel(), classLoader); } public QueriesFactory(RepositoryConfigurationSource configurationSource, EntityManagerFactory entityManagerFactory, - Metamodel metamodel) { + Metamodel metamodel, ClassLoader classLoader) { this.metamodel = metamodel; + this.namedQueries = getNamedQueries(configurationSource, classLoader); this.entityManagerFactory = entityManagerFactory; Optional escapeCharacter = configurationSource.getAttribute("escapeCharacter", Character.class); this.escapeCharacter = escapeCharacter.map(EscapeCharacter::of).orElse(EscapeCharacter.DEFAULT); } + private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource configSource, ClassLoader classLoader) { + + String location = configSource != null ? configSource.getNamedQueryLocation().orElse(null) : null; + + if (location == null) { + location = new JpaRepositoryConfigExtension().getDefaultNamedQueryLocation(); + } + + if (StringUtils.hasText(location)) { + + try { + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader); + + PropertiesBasedNamedQueriesFactoryBean factoryBean = new PropertiesBasedNamedQueriesFactoryBean(); + factoryBean.setLocations(resolver.getResources(location)); + factoryBean.afterPropertiesSet(); + return factoryBean.getObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return new PropertiesBasedNamedQueries(new Properties()); + } + /** * Creates the {@link AotQueries} used within a specific {@link JpaQueryMethod}. * - * @param context + * @param repositoryInformation * @param query * @param selector * @param queryMethod @@ -89,14 +125,18 @@ public AotQueries createQueries(RepositoryInformation repositoryInformation, Mer return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, queryMethod); } - TypedQueryReference namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName()); - if (namedQuery != null) { - return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod); + String queryName = queryMethod.getNamedQueryName(); + if (hasNamedQuery(queryName, returnedType)) { + return buildNamedQuery(returnedType, selector, queryName, query, queryMethod); } return buildPartTreeQuery(returnedType, repositoryInformation, query, queryMethod); } + private boolean hasNamedQuery(String queryName, ReturnedType returnedType) { + return namedQueries.hasQuery(queryName) || getNamedQuery(returnedType, queryName) != null; + } + private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, MergedAnnotation query, JpaQueryMethod queryMethod) { @@ -137,10 +177,9 @@ public ReturnedType getReturnedType() { 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)); + if (hasNamedQuery(queryMethod.getNamedCountQueryName(), returnedType)) { + return AotQueries.from(aotStringQuery, + createNamedAotQuery(returnedType, queryMethod.getNamedCountQueryName(), queryMethod, isNative)); } String countProjection = query.getString("countProjection"); @@ -148,30 +187,52 @@ public ReturnedType getReturnedType() { } private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector, - TypedQueryReference namedQuery, MergedAnnotation query, JpaQueryMethod queryMethod) { + String queryName, MergedAnnotation query, JpaQueryMethod queryMethod) { - NamedAotQuery aotQuery = buildNamedAotQuery(namedQuery, queryMethod, - query.isPresent() && query.getBoolean("nativeQuery")); + boolean nativeQuery = query.isPresent() && query.getBoolean("nativeQuery"); + AotQuery aotQuery = createNamedAotQuery(returnedType, queryName, queryMethod, 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())); + if (hasNamedQuery(queryMethod.getNamedCountQueryName(), returnedType)) { + return AotQueries.from(aotQuery, + createNamedAotQuery(returnedType, queryMethod.getNamedCountQueryName(), queryMethod, nativeQuery)); } String countProjection = query.isPresent() ? query.getString("countProjection") : null; return AotQueries.from(aotQuery, it -> { - return StringAotQuery.of(aotQuery.getQuery()).getQuery(); + + if (it instanceof StringAotQuery sq) { + return sq.getQuery(); + } + + return ((NamedAotQuery) aotQuery).getQuery(); }, countProjection, selector); } - private NamedAotQuery buildNamedAotQuery(TypedQueryReference namedQuery, JpaQueryMethod queryMethod, + private AotQuery createNamedAotQuery(ReturnedType returnedType, String queryName, JpaQueryMethod queryMethod, + boolean isNative) { + + if (namedQueries.hasQuery(queryName)) { + + String queryString = namedQueries.getQuery(queryName); + return StringAotQuery.named(queryName, + isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString)); + } + + TypedQueryReference namedQuery = getNamedQuery(returnedType, queryName); + + Assert.state(namedQuery != null, "Native named query must not be null"); + + return createNamedAotQuery(namedQuery, queryMethod, isNative); + } + + private AotQuery createNamedAotQuery(TypedQueryReference namedQuery, JpaQueryMethod queryMethod, boolean isNative) { QueryExtractor queryExtractor = queryMethod.getQueryExtractor(); @@ -215,9 +276,9 @@ private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInfor 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)); + if (hasNamedQuery(queryMethod.getNamedCountQueryName(), returnedType)) { + return AotQueries.from(aotQuery, + createNamedAotQuery(returnedType, queryMethod.getNamedCountQueryName(), queryMethod, false)); } AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates); 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 b30f0118c7..ba7c49bafc 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 @@ -47,6 +47,19 @@ static StringAotQuery of(DeclaredQuery query) { return new DeclaredAotQuery(PreprocessedQuery.parse(query), false); } + /** + * Creates a new named (via {@link org.springframework.data.repository.core.NamedQueries}) {@code StringAotQuery} from + * a {@link DeclaredQuery}. Parses the query into {@link PreprocessedQuery}. + */ + static StringAotQuery named(String queryName, DeclaredQuery query) { + + if (query instanceof PreprocessedQuery pq) { + return new NamedStringAotQuery(queryName, pq, false); + } + + return new NamedStringAotQuery(queryName, PreprocessedQuery.parse(query), false); + } + /** * Creates a new {@code StringAotQuery} from a JPQL {@code queryString}. Parses the query into * {@link PreprocessedQuery}. @@ -146,6 +159,20 @@ public StringAotQuery rewrite(QueryProvider rewritten) { } + static class NamedStringAotQuery extends DeclaredAotQuery { + + private final String queryName; + + NamedStringAotQuery(String queryName, PreprocessedQuery query, boolean constructorExpressionOrDefaultProjection) { + super(query, constructorExpressionOrDefaultProjection); + this.queryName = queryName; + } + + public String getQueryName() { + return queryName; + } + } + /** * PartTree (derived) Query with a limit associated. * 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 d2db31b79a..28b75ff3f9 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 @@ -373,7 +373,7 @@ static boolean isActive(@Nullable ClassLoader classLoader) { */ public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor { - private static final String USE_ENTITY_MANAGER = "spring.aot.jpa.repositories.use-entitymanager"; + public static final String USE_ENTITY_MANAGER = "spring.aot.jpa.repositories.use-entitymanager"; private static final String MODULE_NAME = "jpa"; protected @Nullable JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, 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 9ec1c5f1e5..42aa4bf93f 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 @@ -21,9 +21,9 @@ import java.sql.Blob; import java.sql.SQLException; -import org.springframework.core.convert.converter.Converter; - import org.jspecify.annotations.Nullable; + +import org.springframework.core.convert.converter.Converter; import org.springframework.dao.CleanupFailureDataAccessException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.util.StreamUtils; @@ -35,7 +35,7 @@ * @author Mark Paluch * @since 1.6 */ -final class JpaResultConverters { +public final class JpaResultConverters { /** * {@code private} to prevent instantiation. @@ -47,7 +47,7 @@ private JpaResultConverters() {} * * @author Thomas Darimont */ - enum BlobToByteArrayConverter implements Converter { + public enum BlobToByteArrayConverter implements Converter { INSTANCE; 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 acc22fedbd..0a9a03b6b4 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 @@ -220,7 +220,7 @@ public boolean isCompatibleWith(ParameterBinding other) { * @author Thomas Darimont * @author Mark Paluch */ - static class PartTreeParameterBinding extends ParameterBinding { + public static class PartTreeParameterBinding extends ParameterBinding { private final Class parameterType; private final JpqlQueryTemplates templates; @@ -254,6 +254,14 @@ public boolean isIsNullParameter() { return Type.IS_NULL.equals(type); } + public boolean isIgnoreCase() { + return ignoreCase; + } + + public JpqlQueryTemplates getTemplates() { + return templates; + } + public @Nullable Object getValue() { return value; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java new file mode 100644 index 0000000000..fa64f36fc6 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-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 jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.aot.AotFragmentTestConfigurationSupport; +import org.springframework.data.jpa.repository.sample.SampleConfig; +import org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.data.jpa.repository.sample.UserRepositoryImpl; +import org.springframework.data.jpa.repository.support.DefaultJpaContext; +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.core.support.RepositoryComposition; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.test.context.ContextConfiguration; + +/** + * Integration test for {@link UserRepository} using JavaConfig with mounted AOT-generated repository methods. + * + * @author Mark Paluch + */ +@ContextConfiguration(classes = AotUserRepositoryTests.Config.class, inheritLocations = false) +class AotUserRepositoryTests extends UserRepositoryTests { + + @Configuration + @ImportResource("classpath:/infrastructure.xml") + static class Config { + + @PersistenceContext EntityManager entityManager; + @Autowired ApplicationContext applicationContext; + + @Bean + public EvaluationContextExtension sampleEvaluationContextExtension() { + return new SampleEvaluationContextExtension(); + } + + @Bean + static AotFragmentTestConfigurationSupport aot() { + return new AotFragmentTestConfigurationSupport(UserRepository.class, SampleConfig.class, false, + UserRepositoryImpl.class); + } + + @Bean + public UserRepository userRepository(BeanFactory beanFactory) throws Exception { + + ExtensionAwareEvaluationContextProvider evaluationContextProvider = new ExtensionAwareEvaluationContextProvider( + applicationContext); + + JpaRepositoryFactoryBean factory = new JpaRepositoryFactoryBean<>( + UserRepository.class); + factory.setEntityManager(entityManager); + factory.setBeanFactory(applicationContext); + factory + .setCustomImplementation(new UserRepositoryImpl(new DefaultJpaContext(Collections.singleton(entityManager)))); + + factory.setRepositoryFragments(RepositoryComposition.RepositoryFragments.just(beanFactory.getBean("fragment"))); + + factory.setNamedQueries(namedQueries()); + factory.setEvaluationContextProvider(evaluationContextProvider); + factory.afterPropertiesSet(); + + return factory.getObject(); + } + + @Bean + public GreetingsFrom greetingsFrom() { + return new GreetingsFrom(); + } + + private NamedQueries namedQueries() throws IOException { + + PropertiesFactoryBean factory = new PropertiesFactoryBean(); + factory.setLocation(new ClassPathResource("META-INF/jpa-named-queries.properties")); + factory.afterPropertiesSet(); + + return new PropertiesBasedNamedQueries(factory.getObject()); + } + } + + @Test + @Disabled("ConversionFailedException: Failed to convert from type [java.lang.Object[]] to type [org.springframework.data.jpa.repository.sample.UserRepository$NameOnly] for value [{...}]") + void bindsNativeQueryResultsToProjectionByName() {} + + @Test + @Disabled + void shouldFindUsersInNativeQueryWithPagination() {} + + @Test + @Disabled + void find2YoungestUsersPageableWithPageSize3() {} + + @Test + @Disabled + void find2YoungestUsersPageableWithPageSize3Sliced() {} + +} 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 ddf2cf9eb4..de30244d67 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 @@ -19,11 +19,14 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.List; import org.mockito.Mockito; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -33,17 +36,22 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.context.annotation.ImportResource; +import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.sample.SampleConfig; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; 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.core.support.RepositoryFragment; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.ReflectionUtils; @@ -57,21 +65,31 @@ * @author Mark Paluch */ @ImportResource("classpath:/infrastructure.xml") -class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { +public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { private final Class repositoryInterface; + private final boolean registerFragmentFacade; private final TestJpaAotRepositoryContext repositoryContext; public AotFragmentTestConfigurationSupport(Class repositoryInterface) { - this(repositoryInterface, SampleConfig.class); + this(repositoryInterface, SampleConfig.class, true); } public AotFragmentTestConfigurationSupport(Class repositoryInterface, Class configClass) { + this(repositoryInterface, configClass, true); + } + + public AotFragmentTestConfigurationSupport(Class repositoryInterface, Class configClass, + boolean registerFragmentFacade, Class... additionalFragments) { this.repositoryInterface = repositoryInterface; - this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, null, + + RepositoryComposition composition = RepositoryComposition + .of((List) Arrays.stream(additionalFragments).map(RepositoryFragment::structural).toList()); + this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, composition, new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(configClass), EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); + this.registerFragmentFacade = registerFragmentFacade; } @Override @@ -79,27 +97,33 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface); + repositoryContext.setBeanFactory(beanFactory); + new JpaRepositoryContributor(repositoryContext).contribute(generationContext); AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder .genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") .addConstructorArgValue(new RuntimeBeanReference(EntityManager.class)) - .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); + .addConstructorArgValue( + getCreationContext(repositoryContext, beanFactory.getBean(Environment.class), beanFactory)) + .getBeanDefinition(); TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { beanFactory.setBeanClassLoader(compiled.getClassLoader()); ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository); }); - BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> { + if (registerFragmentFacade) { + + 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); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade); + } } private Object getFragmentFacadeProxy(Object fragment) { @@ -124,7 +148,7 @@ private Object getFragmentFacadeProxy(Object fragment) { } private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( - TestJpaAotRepositoryContext repositoryContext) { + TestJpaAotRepositoryContext repositoryContext, Environment environment, ListableBeanFactory beanFactory) { RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() { @Override @@ -134,7 +158,10 @@ public RepositoryMetadata getRepositoryMetadata() { @Override public ValueExpressionDelegate getValueExpressionDelegate() { - return ValueExpressionDelegate.create(); + + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(environment, + beanFactory); + return new ValueExpressionDelegate(accessor, ValueExpressionParser.create()); } @Override 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 deleted file mode 100644 index 589b95a5f7..0000000000 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/StubRepositoryInformation.java +++ /dev/null @@ -1,138 +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; - -import java.lang.reflect.Method; -import java.util.List; -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; -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.TypeInformation; - -/** - * @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) { - - if (isBaseClassMethod(method)) { - return false; - } - - return true; - } - - @Override - public List getQueryMethods() { - return null; - } - - @Override - public Class getRepositoryBaseClass() { - return SimpleJpaRepository.class; - } - - @Override - 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/TestJpaAotRepositoryContext.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/TestJpaAotRepositoryContext.java index b46528c4a8..f2bda1d158 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 @@ -18,24 +18,27 @@ 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.jspecify.annotations.Nullable; + 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; +import org.springframework.data.jpa.domain.sample.SpecialUser; import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.support.JpaRepositoryFragmentsContributor; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.AotRepositoryInformation; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; -import org.springframework.lang.Nullable; /** * Test {@link AotRepositoryContext} implementation for JPA repositories. @@ -44,15 +47,22 @@ */ public class TestJpaAotRepositoryContext implements AotRepositoryContext { - private final StubRepositoryInformation repositoryInformation; + private final AotRepositoryInformation repositoryInformation; private final Class repositoryInterface; private final RepositoryConfigurationSource configurationSource; + private @Nullable ConfigurableListableBeanFactory beanFactory; public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition, RepositoryConfigurationSource configurationSource) { this.repositoryInterface = repositoryInterface; this.configurationSource = configurationSource; - this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + + RepositoryMetadata metadata = AnnotationRepositoryMetadata.getMetadata(repositoryInterface); + + RepositoryComposition.RepositoryFragments fragments = JpaRepositoryFragmentsContributor.DEFAULT.describe(metadata); + + this.repositoryInformation = new AotRepositoryInformation(metadata, SimpleJpaRepository.class, + composition.append(fragments).getFragments().stream().toList()); } public Class getRepositoryInterface() { @@ -61,7 +71,7 @@ public Class getRepositoryInterface() { @Override public ConfigurableListableBeanFactory getBeanFactory() { - return null; + return beanFactory; } @Override @@ -116,22 +126,10 @@ public Set> getResolvedAnnotations() { @Override public Set> getResolvedTypes() { - return Set.of(User.class, Role.class); + return Set.of(User.class, SpecialUser.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())); - } + public void setBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; } } From c236c7195d4102c25a03e017285f73344abc0a6c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 13:12:11 +0200 Subject: [PATCH 152/224] Prepare 4.0 M4 (2025.1.0). See #3892 --- pom.xml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index cc81dac232..b4c06fee1f 100755 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,4 @@ - + 4.0.0 @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M4 @@ -40,7 +40,7 @@ 9.2.0 42.7.7 23.8.0.25.04 - 4.0.0-SNAPSHOT + 4.0.0-M4 0.10.3 org.hibernate @@ -210,20 +210,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 7b7a4daf9e511b67000adfaf8026aed8554759d2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 13:12:28 +0200 Subject: [PATCH 153/224] Release version 4.0 M4 (2025.1.0). See #3892 --- 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 b4c06fee1f..32f93e324a 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-M4 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..2475b0dde7 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-M4 org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M4 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..5105f8114e 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-M4 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index cbec8a2645..0c090b5644 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-M4 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-M4 ../pom.xml From dcc5c620af8bb5e1c0b8adf9f96b13b58b12d4a7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 13:14:46 +0200 Subject: [PATCH 154/224] Prepare next development iteration. See #3892 --- 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 32f93e324a..b4c06fee1f 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M4 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 2475b0dde7..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-M4 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-M4 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 5105f8114e..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-M4 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 0c090b5644..cbec8a2645 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-M4 + 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-M4 + 4.0.0-SNAPSHOT ../pom.xml From 2e5e513a9d434bff37db37393f042dc239d21507 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 13:14:47 +0200 Subject: [PATCH 155/224] After release cleanups. See #3892 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index b4c06fee1f..79787558fc 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M4 + 4.0.0-SNAPSHOT @@ -40,7 +40,7 @@ 9.2.0 42.7.7 23.8.0.25.04 - 4.0.0-M4 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -210,8 +210,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From 714d67353ce8f517d163937b56059e9a53a0a506 Mon Sep 17 00:00:00 2001 From: shchae04 <94516539+shchae04@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:38:09 +0900 Subject: [PATCH 156/224] Fix typo in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request fixes a small typo in the README file: - `datatabase` → `database` It's a minor change, but helps improve the clarity and quality of the documentation. Signed-off-by: shchae04 <94516539+shchae04@users.noreply.github.com> Original pull request #3953 --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 3bd2b4da00..82e05e71d2 100644 --- a/README.adoc +++ b/README.adoc @@ -143,7 +143,7 @@ https://github.com/spring-projects/spring-data-jpa/issues[issue tracker] to see * If the issue doesn’t exist already, https://github.com/spring-projects/spring-data-jpa/issues[create a new issue]. * Please provide as much information as possible with the issue report, we like to know the version of Spring Data that you are using and JVM version, complete stack traces and any relevant configuration information. * If you need to paste code, or include a stack trace format it as code using triple backtick. -* If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code. Use an in-memory datatabase if possible or set the database up using https://github.com/testcontainers[Testcontainers]. +* If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code. Use an in-memory database if possible or set the database up using https://github.com/testcontainers[Testcontainers]. == Building from Source From 9a80b0ce79aa6273f0287bb9c472171d0108e0ac Mon Sep 17 00:00:00 2001 From: Now Date: Thu, 24 Jul 2025 03:17:15 +0900 Subject: [PATCH 157/224] Remove dead code in JSqlParserQueryEnhancer Signed-off-by: Now Original pull request #3954 --- .../data/jpa/repository/query/JSqlParserQueryEnhancer.java | 2 -- 1 file changed, 2 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 b340d49ce4..1711386349 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,8 +98,6 @@ public JSqlParserQueryEnhancer(QueryProvider 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); } From 257ece17a5c0a64f22b7611ab2aa45b311fb6eaa Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 24 Jul 2025 07:56:45 +0200 Subject: [PATCH 158/224] Polishing. Formatting. Reduced scope of field to variable. Original pull request #3954 --- .../query/JSqlParserQueryEnhancer.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 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 1711386349..bf3f70df59 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -15,9 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlCount; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlLower; -import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression; +import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*; +import static org.springframework.data.jpa.repository.query.QueryUtils.*; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; @@ -75,7 +74,6 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { private final QueryProvider query; - private final Statement statement; private final ParsedType parsedType; private final boolean hasConstructorExpression; private final @Nullable String primaryAlias; @@ -90,15 +88,15 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { public JSqlParserQueryEnhancer(QueryProvider query) { this.query = query; - this.statement = parseStatement(query.getQueryString(), Statement.class); + Statement statement = parseStatement(query.getQueryString(), Statement.class); this.parsedType = detectParsedType(statement); this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); - this.primaryAlias = detectAlias(this.parsedType, this.statement); - this.projection = detectProjection(this.statement); - this.selectAliases = Collections.unmodifiableSet(getSelectionAliases(this.statement)); - this.joinAliases = Collections.unmodifiableSet(getJoinAliases(this.statement)); - this.serialized = SerializationUtils.serialize(this.statement); + this.primaryAlias = detectAlias(this.parsedType, statement); + this.projection = detectProjection(statement); + this.selectAliases = Collections.unmodifiableSet(getSelectionAliases(statement)); + this.joinAliases = Collections.unmodifiableSet(getJoinAliases(statement)); + this.serialized = SerializationUtils.serialize(statement); } /** @@ -217,8 +215,8 @@ private static Set getJoinAliases(Statement statement) { * @param statement * @param mapper * @param fallback - * @return * @param + * @return */ private static T doWithPlainSelect(Statement statement, java.util.function.Function mapper, Supplier fallback) { @@ -238,8 +236,8 @@ private static T doWithPlainSelect(Statement statement, java.util.function.F * @param skipIf * @param mapper * @param fallback - * @return * @param + * @return */ private static T doWithPlainSelect(Statement statement, Predicate skipIf, java.util.function.Function mapper, Supplier fallback) { From 529eb72ffb4ec00a518c52785c0e5fdc2ade82e7 Mon Sep 17 00:00:00 2001 From: Now Date: Thu, 24 Jul 2025 03:26:06 +0900 Subject: [PATCH 159/224] Fix typo in Jpa21Utils javadoc. Signed-off-by: Now Original pull request #3955 --- .../springframework/data/jpa/repository/query/Jpa21Utils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ff1ce50b54..b3c1407279 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 @@ -64,7 +64,7 @@ public static QueryHints getFetchGraphHint(EntityManager em, JpaEntityGraph enti * Adds a JPA 2.1 fetch-graph or load-graph hint to the given {@link Query} if running under JPA 2.1. * * @see Jakarta - * Persistence Specfication - Use of Entity Graphs in find and query operations + * Persistence Specification - Use of Entity Graphs in find and query operations * @param em must not be {@literal null}. * @param jpaEntityGraph must not be {@literal null}. * @param entityType must not be {@literal null}. From ae2ff8f6c191fc05903999814218d541e6337931 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 4 Aug 2025 12:20:15 +0200 Subject: [PATCH 160/224] Fix AOT vs reflective behavior failures in `AotUserRepositoryTests`. Closes #3951 --- .../data/jpa/repository/aot/AotQueries.java | 13 +-- .../data/jpa/repository/aot/AotQuery.java | 11 +++ .../jpa/repository/aot/JpaCodeBlocks.java | 32 +++++-- .../aot/JpaRepositoryContributor.java | 4 +- .../jpa/repository/aot/NamedAotQuery.java | 24 ++--- .../jpa/repository/aot/QueriesFactory.java | 87 ++++++++++--------- .../jpa/repository/aot/StringAotQuery.java | 65 +++++++------- .../repository/query/DefaultEntityQuery.java | 5 ++ .../query/EmptyIntrospectedQuery.java | 6 ++ .../jpa/repository/query/EntityQuery.java | 2 + .../repository/query/PreprocessedQuery.java | 2 +- .../repository/AotUserRepositoryTests.java | 19 ---- 12 files changed, 141 insertions(+), 129 deletions(-) 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 5c2c1ea1a6..9ef9705bf2 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 @@ -39,15 +39,7 @@ record AotQueries(AotQuery result, AotQuery count) { /** * Derive a count query from the given query. */ - public static AotQueries from(StringAotQuery query, @Nullable 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, + public static AotQueries withDerivedCountQuery(T query, Function queryMapper, @Nullable String countProjection, QueryEnhancerSelector selector) { DeclaredQuery underlyingQuery = queryMapper.apply(query); @@ -56,8 +48,7 @@ public static AotQueries from(T query, Function queryRewriter = QueryRewriter.IdentityQueryRewriter.class; private QueryBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMethod queryMethod) { + this.context = context; this.queryMethod = queryMethod; this.queryVariableName = context.localVariable("query"); @@ -294,14 +295,16 @@ private CodeBlock applyLimits(boolean exists, String pageable) { return builder.build(); } + if (queries != null && queries.result() instanceof StringAotQuery sq && sq.hasPagingExpression()) { + return builder.build(); + } + String limit = context.getLimitParameterName(); if (StringUtils.hasText(limit)) { builder.beginControlFlow("if ($L.isLimited())", limit); builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limit); builder.endControlFlow(); - } else if (queries != null && queries.result().isLimited()) { - builder.addStatement("$L.setMaxResults($L)", queryVariableName, queries.result().getLimit().max()); } if (StringUtils.hasText(pageable)) { @@ -316,6 +319,20 @@ private CodeBlock applyLimits(boolean exists, String pageable) { builder.endControlFlow(); } + if (queries.result().isLimited()) { + + int max = queries.result().getLimit().max(); + + builder.beginControlFlow("if ($L.getMaxResults() != $T.MAX_VALUE)", queryVariableName, Integer.class); + builder.beginControlFlow("if ($1L.getMaxResults() > $2L && $1L.getFirstResult() > 0)", queryVariableName, max); + builder.addStatement("$1L.setFirstResult($1L.getFirstResult() - ($1L.getMaxResults() - $2L))", + queryVariableName, max); + builder.endControlFlow(); + builder.endControlFlow(); + + builder.addStatement("$1L.setMaxResults($2L)", queryVariableName, max); + } + return builder.build(); } @@ -484,11 +501,12 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, if (query instanceof NamedAotQuery nq) { - 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) { + if (!count && !nq.hasConstructorExpressionOrDefaultProjection() && returnedType.isProjecting() + && returnedType.getReturnedType().isInterface()) { + queryReturnType = Tuple.class; + } + + if (queryReturnType != null) { builder.addStatement("$T $L = this.$L.createNamedQuery($S, $T.class)", Query.class, queryVariableName, context.fieldNameOf(EntityManager.class), nq.getName(), queryReturnType); 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 8ae68ee411..5712b38893 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 @@ -183,8 +183,8 @@ private Optional> getQueryEnhancerSelectorClass() { MergedAnnotation query = MergedAnnotations.from(method).get(Query.class); - AotQueries aotQueries = queriesFactory.createQueries(getRepositoryInformation(), query, selector, queryMethod, - returnedType); + AotQueries aotQueries = queriesFactory.createQueries(getRepositoryInformation(), returnedType, selector, query, + queryMethod); // no KeysetScrolling for now. if (parameters.hasScrollPositionParameter()) { 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 e3813ce137..deb6c21f02 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 @@ -15,11 +15,8 @@ */ package org.springframework.data.jpa.repository.aot; -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; +import org.springframework.data.jpa.repository.query.EntityQuery; /** * Value object to describe a named AOT query. @@ -31,20 +28,20 @@ class NamedAotQuery extends AotQuery { private final String name; private final DeclaredQuery query; + private final boolean constructorExpressionOrDefaultProjection; - private NamedAotQuery(String name, DeclaredQuery queryString, List parameterBindings) { - super(parameterBindings); + public NamedAotQuery(String name, EntityQuery entityQuery) { + super(entityQuery.getParameterBindings()); this.name = name; - this.query = queryString; + this.query = entityQuery.getQuery(); + this.constructorExpressionOrDefaultProjection = AotQuery.hasConstructorExpressionOrDefaultProjection(entityQuery); } /** * 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 static NamedAotQuery named(String namedQuery, EntityQuery query) { + return new NamedAotQuery(namedQuery, query); } public String getName() { @@ -64,4 +61,9 @@ public boolean isNative() { return query.isNative(); } + @Override + public boolean hasConstructorExpressionOrDefaultProjection() { + return constructorExpressionOrDefaultProjection; + } + } 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 210a02a4fa..8bf3c9b4e2 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 @@ -112,28 +112,28 @@ private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource con * Creates the {@link AotQueries} used within a specific {@link JpaQueryMethod}. * * @param repositoryInformation - * @param query + * @param returnedType * @param selector + * @param query * @param queryMethod - * @param returnedType * @return */ - public AotQueries createQueries(RepositoryInformation repositoryInformation, MergedAnnotation query, - QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) { + public AotQueries createQueries(RepositoryInformation repositoryInformation, ReturnedType returnedType, + QueryEnhancerSelector selector, MergedAnnotation query, JpaQueryMethod queryMethod) { if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, queryMethod); } String queryName = queryMethod.getNamedQueryName(); - if (hasNamedQuery(queryName, returnedType)) { + if (hasNamedQuery(returnedType, queryName)) { return buildNamedQuery(returnedType, selector, queryName, query, queryMethod); } - return buildPartTreeQuery(returnedType, repositoryInformation, query, queryMethod); + return buildPartTreeQuery(repositoryInformation, returnedType, selector, query, queryMethod); } - private boolean hasNamedQuery(String queryName, ReturnedType returnedType) { + private boolean hasNamedQuery(ReturnedType returnedType, String queryName) { return namedQueries.hasQuery(queryName) || getNamedQuery(returnedType, queryName) != null; } @@ -142,19 +142,15 @@ 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 ? DeclaredQuery::nativeQuery : DeclaredQuery::jpqlQuery; queryFunction = operator.andThen(queryFunction); String queryString = query.getString("value"); - StringAotQuery aotStringQuery = queryFunction.apply(queryString); + EntityQuery entityQuery = EntityQuery.create(queryFunction.apply(queryString), selector); + StringAotQuery aotStringQuery = StringAotQuery.of(entityQuery); 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()) { @@ -174,38 +170,38 @@ public ReturnedType getReturnedType() { } if (StringUtils.hasText(countQuery)) { - return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery)); + return AotQueries.from(aotStringQuery, StringAotQuery.of(queryFunction.apply(countQuery))); } - if (hasNamedQuery(queryMethod.getNamedCountQueryName(), returnedType)) { + if (hasNamedQuery(returnedType, queryMethod.getNamedCountQueryName())) { return AotQueries.from(aotStringQuery, - createNamedAotQuery(returnedType, queryMethod.getNamedCountQueryName(), queryMethod, isNative)); + createNamedAotQuery(returnedType, selector, queryMethod.getNamedCountQueryName(), queryMethod, isNative)); } String countProjection = query.getString("countProjection"); - return AotQueries.from(aotStringQuery, countProjection, selector); + return AotQueries.withDerivedCountQuery(aotStringQuery, StringAotQuery::getQuery, countProjection, selector); } - private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector, - String queryName, MergedAnnotation query, JpaQueryMethod queryMethod) { + private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector, String queryName, + MergedAnnotation query, JpaQueryMethod queryMethod) { boolean nativeQuery = query.isPresent() && query.getBoolean("nativeQuery"); - AotQuery aotQuery = createNamedAotQuery(returnedType, queryName, queryMethod, nativeQuery); - + AotQuery aotQuery = createNamedAotQuery(returnedType, selector, queryName, queryMethod, 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)); + StringAotQuery + .of(aotQuery.isNative() ? DeclaredQuery.nativeQuery(countQuery) : DeclaredQuery.jpqlQuery(countQuery))); } - if (hasNamedQuery(queryMethod.getNamedCountQueryName(), returnedType)) { + if (hasNamedQuery(returnedType, queryMethod.getNamedCountQueryName())) { return AotQueries.from(aotQuery, - createNamedAotQuery(returnedType, queryMethod.getNamedCountQueryName(), queryMethod, nativeQuery)); + createNamedAotQuery(returnedType, selector, queryMethod.getNamedCountQueryName(), queryMethod, nativeQuery)); } String countProjection = query.isPresent() ? query.getString("countProjection") : null; - return AotQueries.from(aotQuery, it -> { + return AotQueries.withDerivedCountQuery(aotQuery, it -> { if (it instanceof StringAotQuery sq) { return sq.getQuery(); @@ -215,25 +211,26 @@ private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelec }, countProjection, selector); } - private AotQuery createNamedAotQuery(ReturnedType returnedType, String queryName, JpaQueryMethod queryMethod, - boolean isNative) { + private AotQuery createNamedAotQuery(ReturnedType returnedType, QueryEnhancerSelector selector, String queryName, + JpaQueryMethod queryMethod, boolean isNative) { if (namedQueries.hasQuery(queryName)) { String queryString = namedQueries.getQuery(queryName); - return StringAotQuery.named(queryName, - isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString)); + + DeclaredQuery query = isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString); + return StringAotQuery.named(queryName, EntityQuery.create(query, selector)); } TypedQueryReference namedQuery = getNamedQuery(returnedType, queryName); Assert.state(namedQuery != null, "Native named query must not be null"); - return createNamedAotQuery(namedQuery, queryMethod, isNative); + return createNamedAotQuery(namedQuery, selector, isNative, queryMethod); } - private AotQuery createNamedAotQuery(TypedQueryReference namedQuery, JpaQueryMethod queryMethod, - boolean isNative) { + private AotQuery createNamedAotQuery(TypedQueryReference namedQuery, QueryEnhancerSelector selector, + boolean isNative, JpaQueryMethod queryMethod) { QueryExtractor queryExtractor = queryMethod.getQueryExtractor(); String queryString = queryExtractor.extractQueryString(namedQuery); @@ -244,8 +241,9 @@ private AotQuery createNamedAotQuery(TypedQueryReference namedQuery, JpaQuery 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)); + DeclaredQuery query = isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString); + + return NamedAotQuery.named(namedQuery.getName(), EntityQuery.create(query, selector)); } private @Nullable TypedQueryReference getNamedQuery(ReturnedType returnedType, String queryName) { @@ -266,19 +264,20 @@ private AotQuery createNamedAotQuery(TypedQueryReference namedQuery, JpaQuery return null; } - private AotQueries buildPartTreeQuery(ReturnedType returnedType, RepositoryInformation repositoryInformation, + private AotQueries buildPartTreeQuery(RepositoryInformation repositoryInformation, ReturnedType returnedType, + QueryEnhancerSelector selector, MergedAnnotation query, JpaQueryMethod queryMethod) { PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); 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"))); + return AotQueries.from(aotQuery, StringAotQuery.of(DeclaredQuery.jpqlQuery(query.getString("countQuery")))); } - if (hasNamedQuery(queryMethod.getNamedCountQueryName(), returnedType)) { + if (hasNamedQuery(returnedType, queryMethod.getNamedCountQueryName())) { return AotQueries.from(aotQuery, - createNamedAotQuery(returnedType, queryMethod.getNamedCountQueryName(), queryMethod, false)); + createNamedAotQuery(returnedType, selector, queryMethod.getNamedCountQueryName(), queryMethod, false)); } AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates); @@ -318,19 +317,21 @@ private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, Class result = queryForEntity ? returnedType.getDomainType() : null; - if (query instanceof StringAotQuery sq && sq.hasConstructorExpressionOrDefaultProjection()) { - return result; - } - if (returnedType.isProjecting()) { if (returnedType.getReturnedType().isInterface()) { + + if (query.hasConstructorExpressionOrDefaultProjection()) { + return result; + } + 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 ba7c49bafc..46555c5504 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 @@ -19,6 +19,7 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.query.DeclaredQuery; +import org.springframework.data.jpa.repository.query.EntityQuery; import org.springframework.data.jpa.repository.query.ParameterBinding; import org.springframework.data.jpa.repository.query.PreprocessedQuery; import org.springframework.data.jpa.repository.query.QueryProvider; @@ -48,24 +49,18 @@ static StringAotQuery of(DeclaredQuery query) { } /** - * Creates a new named (via {@link org.springframework.data.repository.core.NamedQueries}) {@code StringAotQuery} from - * a {@link DeclaredQuery}. Parses the query into {@link PreprocessedQuery}. + * Creates a new {@code StringAotQuery} from a {@link EntityQuery}. Parses the query into {@link PreprocessedQuery}. */ - static StringAotQuery named(String queryName, DeclaredQuery query) { - - if (query instanceof PreprocessedQuery pq) { - return new NamedStringAotQuery(queryName, pq, false); - } - - return new NamedStringAotQuery(queryName, PreprocessedQuery.parse(query), false); + static StringAotQuery of(EntityQuery query) { + return new DeclaredAotQuery(query); } /** - * Creates a new {@code StringAotQuery} from a JPQL {@code queryString}. Parses the query into - * {@link PreprocessedQuery}. + * Creates a new named (via {@link org.springframework.data.repository.core.NamedQueries}) {@code StringAotQuery} from + * a {@link EntityQuery}. Parses the query into {@link PreprocessedQuery}. */ - static StringAotQuery jpqlQuery(String queryString) { - return of(DeclaredQuery.jpqlQuery(queryString)); + static StringAotQuery named(String queryName, EntityQuery query) { + return new NamedStringAotQuery(queryName, query); } /** @@ -76,14 +71,6 @@ public static StringAotQuery jpqlQuery(String queryString, List"; 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 0e22efa28a..5753731a6e 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 @@ -81,6 +81,8 @@ default boolean usesPaging() { return false; } + PreprocessedQuery getQuery(); + /** * 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 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 b32a2b1ae3..c4dd4a2ac5 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 @@ -119,7 +119,7 @@ boolean hasNamedBindings() { return this.hasNamedBindings; } - boolean containsPageableInSpel() { + public boolean containsPageableInSpel() { return containsPageableInSpel; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java index fa64f36fc6..c35bafb0e3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/AotUserRepositoryTests.java @@ -21,9 +21,6 @@ import java.io.IOException; import java.util.Collections; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; @@ -110,20 +107,4 @@ private NamedQueries namedQueries() throws IOException { } } - @Test - @Disabled("ConversionFailedException: Failed to convert from type [java.lang.Object[]] to type [org.springframework.data.jpa.repository.sample.UserRepository$NameOnly] for value [{...}]") - void bindsNativeQueryResultsToProjectionByName() {} - - @Test - @Disabled - void shouldFindUsersInNativeQueryWithPagination() {} - - @Test - @Disabled - void find2YoungestUsersPageableWithPageSize3() {} - - @Test - @Disabled - void find2YoungestUsersPageableWithPageSize3Sliced() {} - } From 2704a580d951e55a1d37584c434474580e5f30a1 Mon Sep 17 00:00:00 2001 From: Choi Wang Gyu Date: Mon, 4 Aug 2025 22:07:54 +0900 Subject: [PATCH 161/224] Avoid double parentheses in IN predicate rendering. Prevent duplicate parentheses when rendering IN/NOT IN predicates with expressions that already contain parentheses. Added logic to check if predicate string starts and ends with parentheses before wrapping with additional parentheses. This change improves JPQL query readability by avoiding patterns like "field IN (('value1', 'value2'))" and ensures proper syntax for subqueries and already-parenthesized expressions. Added comprehensive unit tests to verify the fix handles various scenarios including regular expressions, pre-parenthesized expressions, and subquery expressions. Signed-off-by: Choi Wang Gyu Closes #3961 Original pull request: #3962 --- .../repository/query/JpqlQueryBuilder.java | 11 ++++++-- .../query/JpqlQueryBuilderUnitTests.java | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 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 124df50346..f9e9fb76f6 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 @@ -44,6 +44,7 @@ * A Domain-Specific Language to build JPQL queries using Java code. * * @author Mark Paluch + * @author Choi Wang Gyu */ @SuppressWarnings("JavadocDeclaration") public final class JpqlQueryBuilder { @@ -1422,8 +1423,14 @@ 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)); + String predicateStr = predicate.render(context); + + // Avoid double parentheses if predicate string already starts and ends with parentheses + if (predicateStr.startsWith("(") && predicateStr.endsWith(")")) { + return "%s %s %s".formatted(path.render(context), operator, predicateStr); + } + + return "%s %s (%s)".formatted(path.render(context), operator, predicateStr); } @Override 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 46952dee71..376b116cf0 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 @@ -34,6 +34,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Choi Wang Gyu */ class JpqlQueryBuilderUnitTests { @@ -136,6 +137,31 @@ void predicateRendering() { assertThat(where.neq(expression("'AT'")).render(context)).isEqualTo("o.country != 'AT'"); } + @Test // GH-3961 - Nested predicate parentheses handling + void inPredicateWithNestedExpression() { + + Entity entity = JpqlQueryBuilder.entity(Order.class); + WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); + RenderContext context = ctx(entity); + + // Test regular IN predicate with parentheses + assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')"); + + // Test IN predicate with already parenthesized expression - should avoid double parentheses + Expression parenthesizedExpression = expression("('AT', 'DE')"); + assertThat(where.in(parenthesizedExpression).render(context)) + .isEqualTo("o.country IN ('AT', 'DE')"); + + // Test NOT IN predicate with already parenthesized expression + assertThat(where.notIn(parenthesizedExpression).render(context)) + .isEqualTo("o.country NOT IN ('AT', 'DE')"); + + // Test IN with subquery (already parenthesized) + Expression subqueryExpression = expression("(SELECT c.code FROM Country c WHERE c.active = true)"); + assertThat(where.in(subqueryExpression).render(context)) + .isEqualTo("o.country IN (SELECT c.code FROM Country c WHERE c.active = true)"); + } + @Test // GH-3588 void selectRendering() { From 9445cdc6906381890eb980542aa2cd254899c403 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 5 Aug 2025 13:05:26 +0200 Subject: [PATCH 162/224] Polishing. Apply consistent formatting. Extract superinterface for rendering. Refine tests. See #3961 Original pull request: #3962 --- .../repository/query/JpqlQueryBuilder.java | 95 +++++++++++++----- .../query/JpqlQueryBuilderUnitTests.java | 97 +++++++++++-------- 2 files changed, 128 insertions(+), 64 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 f9e9fb76f6..04c4f97892 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 @@ -142,6 +142,7 @@ public Select select(Selection selection) { Selection postProcess(Selection selection) { return distinct ? new DistinctSelection(selection) : selection; } + }; } @@ -299,6 +300,7 @@ public static WhereStep where(Origin source, PropertyPath path) { public static WhereStep where(Expression rhs) { return new WhereStep() { + @Override public Predicate between(Expression lower, Expression upper) { return new BetweenPredicate(rhs, lower, upper); @@ -393,6 +395,7 @@ public Predicate eq(Expression value) { public Predicate neq(Expression value) { return new OperatorPredicate(rhs, "!=", value); } + }; } @@ -506,7 +509,9 @@ default Select select(JpqlQueryBuilder.PathExpression path) { } public interface Selection { + String render(RenderContext context); + } /** @@ -525,6 +530,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } static PathAndOrigin path(Origin origin, String path) { @@ -538,29 +544,28 @@ static PathAndOrigin path(Origin origin, String path) { 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); + if (parent instanceof Join parentJoin) { + parent = parentJoin.source; + segments.add(parentJoin.path); } else { parent = null; } } - if (parent instanceof Entity) { - Collections.reverse(segments); - segments.add(path); - PathAndOrigin path1 = path(parent, StringUtils.collectionToDelimitedString(segments, ".")); - return new PathAndOrigin(path1.path().getLeafProperty(), origin, false); - } + Collections.reverse(segments); + segments.add(path); + PathAndOrigin joinedPath = path(parent, StringUtils.collectionToDelimitedString(segments, ".")); + return new PathAndOrigin(joinedPath.path().getLeafProperty(), origin, false); } - throw new IllegalStateException(" oh no "); + throw new IllegalStateException("🙈 Unsupported origin type: " + origin); } /** @@ -579,6 +584,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } /** @@ -598,6 +604,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } /** @@ -618,6 +625,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } /** @@ -652,21 +660,29 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } /** - * {@code WHERE} predicate. + * Interface specifying a predicate or expression that can be rendered to {@code String}. */ - public interface Predicate { + public interface Renderable { /** - * Render the predicate given {@link RenderContext}. + * Render the predicate or expression given {@link RenderContext}. * * @param context * @return */ String render(RenderContext context); + } + + /** + * {@code WHERE} predicate. + */ + public interface Predicate extends Renderable { + /** * {@code OR}-concatenate this predicate with {@code other}. * @@ -701,21 +717,21 @@ default Predicate and(Predicate other) { // don't like the structuring of this a default Predicate nest() { return new NestedPredicate(this); } + } /** * Interface specifying an expression that can be rendered to {@code String}. */ - public interface Expression { + public interface Expression extends Renderable { /** - * Render the expression given {@link RenderContext}. + * Create an {@link AliasedExpression} with the given {@code alias}. If the expression is already aliased, the + * previous alias is discarded and replaced with the new one. * - * @param context + * @param alias * @return */ - String render(RenderContext context); - default AliasedExpression as(String alias) { if (this instanceof DefaultAliasedExpression de) { @@ -724,6 +740,7 @@ default AliasedExpression as(String alias) { return new DefaultAliasedExpression(this, alias); } + } /** @@ -756,6 +773,7 @@ public String getAlias() { public String toString() { return render(RenderContext.EMPTY); } + } /** @@ -768,6 +786,7 @@ public interface PathExpression extends Expression { * @return the associated {@link PropertyPath}. */ PropertyPath getPropertyPath(); + } /** @@ -865,6 +884,7 @@ String render() { return result.toString(); } + } /** @@ -889,6 +909,7 @@ public AbstractJpqlQuery where(Predicate predicate) { public String toString() { return render(); } + } record OrderExpression(Expression sortExpression, @org.springframework.lang.Nullable Sort.Direction direction, @@ -915,6 +936,7 @@ public String render(RenderContext context) { return builder.toString(); } + } /** @@ -966,6 +988,7 @@ public String prefixWithAlias(Origin source, String fragment) { public boolean isConstructorContext() { return false; } + } static class ConstructorContext extends RenderContext { @@ -978,6 +1001,7 @@ static class ConstructorContext extends RenderContext { public boolean isConstructorContext() { return true; } + } /** @@ -992,6 +1016,7 @@ public interface Origin { * @return the simple name of the origin (e.g. {@link Class#getSimpleName()}) */ String getName(); + } /** @@ -1001,6 +1026,7 @@ public interface Origin { public interface Bindable { boolean isRoot(); + } /** @@ -1283,6 +1309,7 @@ default Predicate like(String value, String escape) { * @return */ Predicate neq(Expression value); + } record LiteralExpression(String expression) implements Expression { @@ -1296,6 +1323,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record StringLiteralExpression(String literal) implements Expression { @@ -1313,6 +1341,7 @@ public String raw() { public String toString() { return render(RenderContext.EMPTY); } + } record ParameterExpression(ParameterPlaceholder parameter) implements Expression { @@ -1326,6 +1355,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record FunctionExpression(String function, List arguments) implements Expression { @@ -1351,6 +1381,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record OperatorPredicate(Expression path, String operator, Expression predicate) implements Predicate { @@ -1364,6 +1395,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record MemberOfPredicate(Expression path, String operator, Expression predicate) implements Predicate { @@ -1377,6 +1409,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record LhsPredicate(Expression path, String predicate) implements Predicate { @@ -1390,6 +1423,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record BetweenPredicate(Expression path, Expression lower, Expression upper) implements Predicate { @@ -1403,6 +1437,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record LikePredicate(Expression left, String operator, Expression right, String escape) implements Predicate { @@ -1416,6 +1451,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record InPredicate(Expression path, String operator, Expression predicate) implements Predicate { @@ -1423,20 +1459,21 @@ record InPredicate(Expression path, String operator, Expression predicate) imple @Override public String render(RenderContext context) { - String predicateStr = predicate.render(context); - - // Avoid double parentheses if predicate string already starts and ends with parentheses - if (predicateStr.startsWith("(") && predicateStr.endsWith(")")) { - return "%s %s %s".formatted(path.render(context), operator, predicateStr); - } - - return "%s %s (%s)".formatted(path.render(context), operator, predicateStr); + Expression predicate = this.predicate; + String rendered = predicate.render(context); + + return (hasParenthesis(rendered) ? "%s %s %s" : "%s %s (%s)").formatted(path.render(context), operator, rendered); } @Override public String toString() { return render(RenderContext.EMPTY); } + + private static boolean hasParenthesis(String str) { + return str.startsWith("(") && str.endsWith(")"); + } + } record AndPredicate(Predicate left, Predicate right) implements Predicate { @@ -1450,6 +1487,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record OrPredicate(Predicate left, Predicate right) implements Predicate { @@ -1463,6 +1501,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } record NestedPredicate(Predicate delegate) implements Predicate { @@ -1476,6 +1515,7 @@ public String render(RenderContext context) { public String toString() { return render(RenderContext.EMPTY); } + } /** @@ -1507,6 +1547,7 @@ public String render(RenderContext context) { public String getAlias() { return path().getSegment(); } + } /** @@ -1541,5 +1582,7 @@ 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/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 376b116cf0..fde8dd493d 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 @@ -27,6 +27,8 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; /** @@ -41,10 +43,10 @@ class JpqlQueryBuilderUnitTests { @Test // GH-3588 void placeholdersRenderCorrectly() { - assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1)).render(RenderContext.EMPTY)).isEqualTo("?1"); - assertThat(JpqlQueryBuilder.parameter(ParameterPlaceholder.named("arg1")).render(RenderContext.EMPTY)) + assertThatRendered(JpqlQueryBuilder.parameter(ParameterPlaceholder.indexed(1))).isEqualTo("?1"); + assertThatRendered(JpqlQueryBuilder.parameter(ParameterPlaceholder.named("arg1"))) .isEqualTo(":arg1"); - assertThat(JpqlQueryBuilder.parameter("?1").render(RenderContext.EMPTY)).isEqualTo("?1"); + assertThatRendered(JpqlQueryBuilder.parameter("?1")).isEqualTo("?1"); } @Test // GH-3588 @@ -57,17 +59,18 @@ void placeholdersErrorOnInvalidInput() { @Test // GH-3588 void stringLiteralRendersAsQuotedString() { - assertThat(literal("literal").render(RenderContext.EMPTY)).isEqualTo("'literal'"); + assertThatRendered(literal("literal")).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(literal("literal's").render(RenderContext.EMPTY)).isEqualTo("'literal''s'"); + assertThatRendered(literal("literal's")).isEqualTo("'literal''s'"); } @Test // GH-3588 void entity() { Entity entity = JpqlQueryBuilder.entity(Order.class); + assertThat(entity.getAlias()).isEqualTo("o"); assertThat(entity.getEntity()).isEqualTo(Order.class.getName()); assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); @@ -76,20 +79,20 @@ void entity() { @Test // GH-3588 void literalExpressionRendersAsIs() { Expression expression = expression("CONCAT(person.lastName, ‘, ’, person.firstName))"); - assertThat(expression.render(RenderContext.EMPTY)).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))"); + assertThatRendered(expression).isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName))"); } - @Test // GH- + @Test // GH-3961 void aliasedExpression() { // aliasing is contextual and happens during selection rendering. E.g. constructor expressions don't use aliases. Expression expression = expression("CONCAT(person.lastName, ‘, ’, person.firstName)").as("concatted"); - assertThat(expression.render(RenderContext.EMPTY)) + assertThatRendered(expression) .isEqualTo("CONCAT(person.lastName, ‘, ’, person.firstName)"); } - @Test // GH-3588 - void xxx() { + @Test // GH-3961 + void shouldRenderDateAsJpqlLiteral() { Entity entity = JpqlQueryBuilder.entity(Order.class); PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date"); @@ -104,61 +107,60 @@ void predicateRendering() { Entity entity = JpqlQueryBuilder.entity(Order.class); WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); - RenderContext context = ctx(entity); + ContextualAssert ctx = contextual(ctx(entity)); - assertThat(where.between(expression("'AT'"), expression("'DE'")).render(context)) + ctx.assertThat(where.between(expression("'AT'"), expression("'DE'"))) .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'"); + ctx.assertThat(where.eq(expression("'AT'"))).isEqualTo("o.country = 'AT'"); + ctx.assertThat(where.eq(literal("AT"))).isEqualTo("o.country = 'AT'"); + ctx.assertThat(where.gt(expression("'AT'"))).isEqualTo("o.country > 'AT'"); + ctx.assertThat(where.gte(expression("'AT'"))).isEqualTo("o.country >= 'AT'"); - // TODO: that is really really bad - // lange namen - assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')"); + ctx.assertThat(where.in(expression("'AT', 'DE'"))).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(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)) + ctx.assertThat(where.isEmpty()).isEqualTo("o.country IS EMPTY"); + ctx.assertThat(where.isNotEmpty()).isEqualTo("o.country IS NOT EMPTY"); + ctx.assertThat(where.isTrue()).isEqualTo("o.country = TRUE"); + ctx.assertThat(where.isFalse()).isEqualTo("o.country = FALSE"); + ctx.assertThat(where.isNull()).isEqualTo("o.country IS NULL"); + ctx.assertThat(where.isNotNull()).isEqualTo("o.country IS NOT NULL"); + ctx.assertThat(where.like("'\\_%'", "" + EscapeCharacter.DEFAULT.getEscapeCharacter())) .isEqualTo("o.country LIKE '\\_%' ESCAPE '\\'"); - assertThat(where.notLike(expression("'\\_%'"), "" + EscapeCharacter.DEFAULT.getEscapeCharacter()).render(context)) + ctx.assertThat(where.notLike(expression("'\\_%'"), "" + EscapeCharacter.DEFAULT.getEscapeCharacter())) .isEqualTo("o.country NOT LIKE '\\_%' ESCAPE '\\'"); - 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"); + ctx.assertThat(where.lt(expression("'AT'"))).isEqualTo("o.country < 'AT'"); + ctx.assertThat(where.lte(expression("'AT'"))).isEqualTo("o.country <= 'AT'"); + ctx.assertThat(where.memberOf(expression("'AT'"))).isEqualTo("'AT' MEMBER OF o.country"); + // TODO: can we have this where.value(foo).memberOf(pathAndOrigin); - 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'"); + ctx.assertThat(where.notMemberOf(expression("'AT'"))).isEqualTo("'AT' NOT MEMBER OF o.country"); + ctx.assertThat(where.neq(expression("'AT'"))).isEqualTo("o.country != 'AT'"); } - @Test // GH-3961 - Nested predicate parentheses handling + @Test // GH-3961 void inPredicateWithNestedExpression() { Entity entity = JpqlQueryBuilder.entity(Order.class); WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); - RenderContext context = ctx(entity); + ContextualAssert ctx = contextual(ctx(entity)); // Test regular IN predicate with parentheses - assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')"); + ctx.assertThat(where.in(expression("'AT', 'DE'"))).isEqualTo("o.country IN ('AT', 'DE')"); // Test IN predicate with already parenthesized expression - should avoid double parentheses Expression parenthesizedExpression = expression("('AT', 'DE')"); - assertThat(where.in(parenthesizedExpression).render(context)) + ctx.assertThat(where.in(parenthesizedExpression)) .isEqualTo("o.country IN ('AT', 'DE')"); // Test NOT IN predicate with already parenthesized expression - assertThat(where.notIn(parenthesizedExpression).render(context)) + ctx.assertThat(where.notIn(parenthesizedExpression)) .isEqualTo("o.country NOT IN ('AT', 'DE')"); // Test IN with subquery (already parenthesized) Expression subqueryExpression = expression("(SELECT c.code FROM Country c WHERE c.active = true)"); - assertThat(where.in(subqueryExpression).render(context)) + ctx.assertThat(where.in(subqueryExpression)) .isEqualTo("o.country IN (SELECT c.code FROM Country c WHERE c.active = true)"); } @@ -208,6 +210,25 @@ void joinOnPaths() { assertThat(fragment).isEqualTo("p.name = 'ex30' AND join_0.name = 'cstrobl'"); } + static ContextualAssert contextual(RenderContext context) { + return new ContextualAssert(context); + } + + static AbstractStringAssert assertThatRendered(Renderable renderable) { + return contextual(RenderContext.EMPTY).assertThat(renderable); + } + + static AbstractStringAssert assertThat(String actual) { + return Assertions.assertThat(actual); + } + + record ContextualAssert(RenderContext context) { + + public AbstractStringAssert assertThat(Renderable renderable) { + return Assertions.assertThat(renderable.render(context)); + } + } + static RenderContext ctx(Entity... entities) { Map aliases = new LinkedHashMap<>(entities.length); From 9c9818c3f98398fed8fa9b3d332a6ecc968d94f6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 6 Aug 2025 14:15:42 +0200 Subject: [PATCH 163/224] Upgrade to Hibernate 7.1.0.CR2. Closes #3964 --- pom.xml | 2 +- .../HqlOrderExpressionVisitorUnitTests.java | 187 +++++++++--------- 2 files changed, 100 insertions(+), 89 deletions(-) diff --git a/pom.xml b/pom.xml index 79787558fc..0990dd996b 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.2 5.0.0-B09 5.0.0-SNAPSHOT - 7.0.6.Final + 7.1.0.CR2 7.0.7-SNAPSHOT 7.1.0-SNAPSHOT 2.7.4 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 5c0eb36bc3..adf3890db0 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,10 +15,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.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.*; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -35,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.JpaSort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.ContextConfiguration; @@ -57,186 +55,199 @@ class HqlOrderExpressionVisitorUnitTests { @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("LENGTH(firstname)"), "var_1")) + .startsWithIgnoringCase("order by character_length(var_1.firstname) asc"); + assertThat(renderOrderBy(JpaSort.unsafe("char_length(firstname)"), "var_1")) + .startsWithIgnoringCase("order by char_length(var_1.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')"); + assertThat(renderOrderBy(JpaSort.unsafe("nlssort(firstname, 'NLS_SORT = XGERMAN_DIN_AI')"), "var_1")) + .startsWithIgnoringCase("order by nlssort(var_1.firstname, 'NLS_SORT = XGERMAN_DIN_AI')"); } @Test // GH-3172 void cast() { assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> renderOrderBy(JpaSort.unsafe("cast(emailAddress as date)"), "u")); + .isThrownBy(() -> renderOrderBy(JpaSort.unsafe("cast(emailAddress as date)"), "var_1")); } @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("EXTRACT(DAY FROM createdAt)"), "var_1")) + .startsWithIgnoringCase("order by extract(day from var_1.createdAt)"); - assertThat(renderOrderBy(JpaSort.unsafe("WEEK(createdAt)"), "u")) - .startsWithIgnoringCase("order by extract(week from u.createdAt)"); + assertThat(renderOrderBy(JpaSort.unsafe("WEEK(createdAt)"), "var_1")) + .startsWithIgnoringCase("order by extract(week from var_1.createdAt)"); } @Test // GH-3172 void trunc() { - assertThat(renderOrderBy(JpaSort.unsafe("TRUNC(age)"), "u")).startsWithIgnoringCase("order by trunc(u.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("TRUNC(age)"), "var_1")) + .startsWithIgnoringCase("order by trunc(var_1.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)"); + assertThat(renderOrderBy(JpaSort.unsafe("upper(firstname)"), "var_1")) + .startsWithIgnoringCase("order by upper(var_1.firstname)"); + assertThat(renderOrderBy(JpaSort.unsafe("lower(firstname)"), "var_1")) + .startsWithIgnoringCase("order by lower(var_1.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"); + assertThat(renderOrderBy(JpaSort.unsafe("substring(emailAddress, 0, 3)"), "var_1")) + .startsWithIgnoringCase("order by substring(var_1.emailAddress, 0, 3) asc"); + assertThat(renderOrderBy(JpaSort.unsafe("substring(emailAddress, 0)"), "var_1")) + .startsWithIgnoringCase("order by substring(var_1.emailAddress, 0) asc"); } @Test // GH-3172 void repeat() { - assertThat(renderOrderBy(JpaSort.unsafe("repeat('a', 5)"), "u")) + assertThat(renderOrderBy(JpaSort.unsafe("repeat('a', 5)"), "var_1")) .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"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1"), "var_1")).startsWithIgnoringCase("order by var_1.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1l"), "var_1")).startsWithIgnoringCase("order by var_1.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1L"), "var_1")).startsWithIgnoringCase("order by var_1.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1"), "var_1")).startsWithIgnoringCase("order by var_1.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1f"), "var_1")).startsWithIgnoringCase("order by var_1.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1bi"), "var_1")) + .startsWithIgnoringCase("order by var_1.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1.1bd"), "var_1")) + .startsWithIgnoringCase("order by var_1.age + 1.1"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 0x12"), "var_1")).startsWithIgnoringCase("order by var_1.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 '2024-01-01 12:34:56'}"), "var_1")) + .startsWithIgnoringCase("order by var_1.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'"); + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {ts '2012-01-03 09:00:00.000000001'}"), "var_1")) + .startsWithIgnoringCase("order by var_1.createdAt + '2012-01-03T09:00:00.000000001'"); // Hibernate NPE - assertThatIllegalArgumentException().isThrownBy(() -> renderOrderBy(JpaSort.unsafe("createdAt + {t '12:34:56'}"), "u")); + assertThatIllegalArgumentException() + .isThrownBy(() -> renderOrderBy(JpaSort.unsafe("createdAt + {t '12:34:56'}"), "var_1")); - assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d '2024-01-01'}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + '2024-01-01'"); + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d '2024-01-01'}"), "var_1")) + .startsWithIgnoringCase("order by var_1.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 + {ts 2024-01-01 12:34:56}"), "var_1")) + .startsWithIgnoringCase("order by var_1.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 + {t 12:34:56}"), "var_1")) + .startsWithIgnoringCase("order by var_1.createdAt + '12:34:56'"); - assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d 2024-01-01}"), "u")) - .startsWithIgnoringCase("order by u.createdAt + '2024-01-01'"); + assertThat(renderOrderBy(JpaSort.unsafe("createdAt + {d 2024-01-01}"), "var_1")) + .startsWithIgnoringCase("order by var_1.createdAt + '2024-01-01'"); } @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)"); + // Hibernate representation bugs, should be sum(var_1.age) + assertThat(renderOrderBy(JpaSort.unsafe("sum(age)"), "var_1")).startsWithIgnoringCase("order by sum()"); + assertThat(renderOrderBy(JpaSort.unsafe("min(age)"), "var_1")).startsWithIgnoringCase("order by min()"); + assertThat(renderOrderBy(JpaSort.unsafe("max(age)"), "var_1")).startsWithIgnoringCase("order by max()"); + + assertThat(renderOrderBy(JpaSort.unsafe("age"), "var_1")).startsWithIgnoringCase("order by var_1.age"); + assertThat(renderOrderBy(JpaSort.unsafe("age + 1"), "var_1")).startsWithIgnoringCase("order by var_1.age + 1"); + assertThat(renderOrderBy(JpaSort.unsafe("ABS(age) + 1"), "var_1")) + .startsWithIgnoringCase("order by abs(var_1.age) + 1"); + + assertThat(renderOrderBy(JpaSort.unsafe("neg(active)"), "var_1")) + .startsWithIgnoringCase("order by neg(var_1.active)"); + assertThat(renderOrderBy(JpaSort.unsafe("abs(age)"), "var_1")).startsWithIgnoringCase("order by abs(var_1.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("ceiling(age)"), "var_1")) + .startsWithIgnoringCase("order by ceiling(var_1.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("floor(age)"), "var_1")) + .startsWithIgnoringCase("order by floor(var_1.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("round(age)"), "var_1")) + .startsWithIgnoringCase("order by round(var_1.age)"); + + assertThat(renderOrderBy(JpaSort.unsafe("prod(age, 1)"), "var_1")) + .startsWithIgnoringCase("order by prod(var_1.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("prod(age, age)"), "var_1")) + .startsWithIgnoringCase("order by prod(var_1.age, var_1.age)"); + + assertThat(renderOrderBy(JpaSort.unsafe("diff(age, 1)"), "var_1")) + .startsWithIgnoringCase("order by diff(var_1.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("quot(age, 1)"), "var_1")) + .startsWithIgnoringCase("order by quot(var_1.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("mod(age, 1)"), "var_1")) + .startsWithIgnoringCase("order by mod(var_1.age, 1)"); + assertThat(renderOrderBy(JpaSort.unsafe("sqrt(age)"), "var_1")).startsWithIgnoringCase("order by sqrt(var_1.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("exp(age)"), "var_1")).startsWithIgnoringCase("order by exp(var_1.age)"); + assertThat(renderOrderBy(JpaSort.unsafe("ln(age)"), "var_1")).startsWithIgnoringCase("order by ln(var_1.age)"); } @Test // GH-3172 @Disabled("HHH-19075") void trim() { - assertThat(renderOrderBy(JpaSort.unsafe("trim(leading '.' from lastname)"), "u")) + assertThat(renderOrderBy(JpaSort.unsafe("trim(leading '.' from lastname)"), "var_1")) .startsWithIgnoringCase("order by repeat('a', 5) asc"); } @Test // GH-3172 void groupedExpression() { - assertThat(renderOrderBy(JpaSort.unsafe("(lastname)"), "u")).startsWithIgnoringCase("order by u.lastname"); + assertThat(renderOrderBy(JpaSort.unsafe("(lastname)"), "var_1")).startsWithIgnoringCase("order by var_1.lastname"); } @Test // GH-3172 void tupleExpression() { - assertThat(renderOrderBy(JpaSort.unsafe("(firstname, lastname)"), "u")) - .startsWithIgnoringCase("order by u.firstname, u.lastname"); + assertThat(renderOrderBy(JpaSort.unsafe("(firstname, lastname)"), "var_1")) + .startsWithIgnoringCase("order by var_1.firstname, var_1.lastname"); } @Test // GH-3172 void concat() { - assertThat(renderOrderBy(JpaSort.unsafe("firstname || lastname"), "u")) - .startsWithIgnoringCase("order by concat(u.firstname, u.lastname)"); + assertThat(renderOrderBy(JpaSort.unsafe("firstname || lastname"), "var_1")) + .startsWithIgnoringCase("order by concat(var_1.firstname, var_1.lastname)"); } @Test // GH-3172 void pathBased() { - String query = renderQuery(JpaSort.unsafe("manager.firstname"), "u"); + String query = renderQuery(JpaSort.unsafe("manager.firstname"), "var_1"); - assertThat(query).contains("from org.springframework.data.jpa.domain.sample.User u left join u.manager"); + assertThat(query).contains("from org.springframework.data.jpa.domain.sample.User var_1 left join var_1.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' else firstname end"), "var_1")) + .startsWithIgnoringCase("order by case var_1.firstname when 'Oliver' then 'A' else var_1.firstname end"); assertThat(renderOrderBy( - JpaSort.unsafe("case firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else firstname end"), "u")) + JpaSort.unsafe("case firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else firstname end"), "var_1")) .startsWithIgnoringCase( - "order by case u.firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else u.firstname end"); + "order by case var_1.firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else var_1.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 age < 31 then 'A' else firstname end"), "var_1")) + .startsWithIgnoringCase("order by case when var_1.age < 31 then 'A' else var_1.firstname end"); assertThat( - renderOrderBy(JpaSort.unsafe("case when firstname not in ('Oliver', 'Dave') then 'A' else firstname end"), "u")) + renderOrderBy(JpaSort.unsafe("case when firstname not in ('Oliver', 'Dave') then 'A' else firstname end"), + "var_1")) .startsWithIgnoringCase( - "order by case when u.firstname not in ('Oliver', 'Dave') then 'A' else u.firstname end"); + "order by case when var_1.firstname not in ('Oliver', 'Dave') then 'A' else var_1.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"); From 4b083eae283280dcb74583bb7e33a6d7bde1e59e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 6 Aug 2025 14:39:39 +0200 Subject: [PATCH 164/224] Use AOT ExpressionMarker support. Closes #3965 --- .../data/jpa/repository/aot/JpaCodeBlocks.java | 8 ++------ 1 file changed, 2 insertions(+), 6 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 4aad5699c9..ab8a73ae1f 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 @@ -215,10 +215,6 @@ public CodeBlock build() { builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringVariableName, actualReturnType)); } - if (queries.result().hasExpression() || queries.count().hasExpression()) { - builder.addStatement("class ExpressionMarker{}"); - } - builder.add(createQuery(false, queryVariableName, queryStringVariableName, queryRewriterName, queries.result(), this.sqlResultSetMapping, pageable, this.queryHints, this.entityGraph, this.queryReturnType)); @@ -550,13 +546,13 @@ private Object getParameter(ParameterBinding.ParameterOrigin origin) { expressionString = "#{" + expressionString + "}"; } - builder.add("evaluateExpression(ExpressionMarker.class.getEnclosingMethod(), $S$L)", expressionString, + builder.add("evaluateExpression($L, $S$L)", context.getExpressionMarker().enclosingMethod(), expressionString, parameterNames); return builder.build(); } - throw new UnsupportedOperationException("Not supported yet"); + throw new UnsupportedOperationException("Not supported yet for: " + origin); } private CodeBlock applyEntityGraph(AotEntityGraph entityGraph, String queryVariableName) { From d5d933212abbddb70e0dda8339b9c32ce03820d8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 8 Aug 2025 15:23:36 +0200 Subject: [PATCH 165/224] Upgrade to Hibernate 7.1.0.Final. Closes #3969 --- pom.xml | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index 0990dd996b..16eaf699f7 100755 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,8 @@ 4.13.2 5.0.0-B09 5.0.0-SNAPSHOT - 7.1.0.CR2 - 7.0.7-SNAPSHOT - 7.1.0-SNAPSHOT + 7.1.0.Final + 7.1.1-SNAPSHOT 2.7.4

        2.3.232

        3.2.0 @@ -65,22 +64,6 @@ - - hibernate-70-snapshots - - ${hibernate-70-snapshots} - 3.2.0 - - - - sonatype-oss - https://oss.sonatype.org/content/repositories/snapshots - - false - - - - hibernate-71-snapshots From fd3137cb9bdfb481525560afc863c9152209f283 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 12 Aug 2025 14:59:05 +0200 Subject: [PATCH 166/224] Follow AOT repository contributor changes in data-commons. Closes #3972 --- .../jpa/repository/aot/AotContributionIntegrationTests.java | 1 + .../repository/aot/AotFragmentTestConfigurationSupport.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) 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 7f9cd170ec..60ba9c23df 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 @@ -78,6 +78,7 @@ private static TestGenerationContext generate(Class... configurationClasses) TestGenerationContext generationContext = new TestGenerationContext(); generator.processAheadOfTime(context, generationContext); + generationContext.writeGeneratedContent(); return generationContext; } 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 de30244d67..317cdcd9c6 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 @@ -102,12 +102,14 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) new JpaRepositoryContributor(repositoryContext).contribute(generationContext); AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder - .genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") + .genericBeanDefinition(repositoryInterface.getName() + "Impl__AotRepository") .addConstructorArgValue(new RuntimeBeanReference(EntityManager.class)) .addConstructorArgValue( getCreationContext(repositoryContext, beanFactory.getBean(Environment.class), beanFactory)) .getBeanDefinition(); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { beanFactory.setBeanClassLoader(compiled.getClassLoader()); ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository); From 28933e4a4c7bcea7061ffaade0d2a281ff7a0d74 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Aug 2025 14:19:27 +0200 Subject: [PATCH 167/224] =?UTF-8?q?Add=20missing=20`@Nullable`=20annotatio?= =?UTF-8?q?ns=20to=20`JpaSpecificationExecutor.findBy(=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3974 --- .../data/jpa/repository/JpaSpecificationExecutor.java | 3 ++- 1 file changed, 2 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 536ff5bca2..7f11a8500a 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 @@ -240,7 +240,8 @@ default R findBy(PredicateSpecification spec, * @since 3.0 * @throws InvalidDataAccessApiUsageException if the query function returns the {@link FluentQuery} instance. */ - R findBy(Specification spec, Function, R> queryFunction); + R findBy(Specification spec, + Function, R> queryFunction); /** * Extension to {@link FetchableFluentQuery} allowing slice results and pagination with a custom count From 1f4cec8629607f0aa58c237940e31702421abc50 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Aug 2025 14:27:53 +0200 Subject: [PATCH 168/224] Polishing. Fix nullability arrangements. See #3974 --- .../data/jpa/provider/PersistenceProvider.java | 5 +++++ .../data/jpa/repository/aot/AotMetamodel.java | 4 ++-- .../data/jpa/repository/aot/JpaCodeBlocks.java | 4 +++- .../data/jpa/repository/aot/QueriesFactory.java | 3 ++- .../data/jpa/repository/query/EmptyIntrospectedQuery.java | 2 +- .../data/jpa/repository/query/HqlSortedQueryTransformer.java | 2 +- .../data/jpa/repository/query/JpaQueryCreator.java | 2 +- 7 files changed, 15 insertions(+), 7 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 7b85d0f5d0..62905fec3c 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 @@ -332,6 +332,11 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory } else if (AopUtils.isAopProxy(unwrapped)) { unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped); } + + if (unwrapped == null) { + throw new IllegalStateException( + "Unwrapping EntityManagerFactory from '%s' failed resulting in null".formatted(emf)); + } } Class entityManagerType = unwrapped.getClass(); 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 a19191eb1c..a7d8a1377c 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 @@ -91,7 +91,7 @@ public void addTransformer(ClassTransformer classTransformer) { this.entityManagerFactory = init(() -> { - managedTypes.stream().forEach(persistenceUnitInfo::addManagedClassName); + managedTypes.forEach(persistenceUnitInfo::addManagedClassName); persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName()); @@ -104,7 +104,7 @@ public List getManagedClassNames() { @Override public URL getPersistenceUnitRootUrl() { - return persistenceUnitRootUrl; + return persistenceUnitRootUrl != null ? persistenceUnitRootUrl : super.getPersistenceUnitRootUrl(); } }; 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 ab8a73ae1f..aae52c1329 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 @@ -281,7 +281,9 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe return builder.build(); } - private CodeBlock applyLimits(boolean exists, String pageable) { + private CodeBlock applyLimits(boolean exists, @Nullable String pageable) { + + Assert.notNull(queries, "Queries must not be null"); Builder builder = CodeBlock.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 8bf3c9b4e2..df59aa8b46 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 @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.function.Function; @@ -99,7 +100,7 @@ private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource con PropertiesBasedNamedQueriesFactoryBean factoryBean = new PropertiesBasedNamedQueriesFactoryBean(); factoryBean.setLocations(resolver.getResources(location)); factoryBean.afterPropertiesSet(); - return factoryBean.getObject(); + return Objects.requireNonNull(factoryBean.getObject()); } catch (IOException e) { throw new RuntimeException(e); } 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 09cc0241ce..bfb18a5c8d 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 @@ -97,7 +97,7 @@ public QueryProvider rewrite(QueryEnhancer.QueryRewriteInformation rewriteInform @Override public PreprocessedQuery getQuery() { - return null; + throw new UnsupportedOperationException(); } @Override 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 99a677a41f..46a22d1ecb 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 @@ -142,7 +142,7 @@ public QueryTokenStream visitJoinFunctionCall(HqlParser.JoinFunctionCallContext QueryTokenStream tokens = super.visitJoinFunctionCall(ctx); if (ctx.variable() != null && !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/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 27a146e14b..a154585d3c 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 @@ -357,7 +357,7 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) { } } - private JpqlQueryBuilder.Expression getDistanceExpression() { + private JpqlQueryBuilder.@Nullable Expression getDistanceExpression() { DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction()); From 054b7f4a88a79b8cae94d4f37e61b5aea6e8fffe Mon Sep 17 00:00:00 2001 From: Jakub Soltys Date: Wed, 18 Jun 2025 22:15:30 +0200 Subject: [PATCH 169/224] Remove unnecessary join when filtering on relationship id. We now no longer create a join for query property paths that point to an identifier of referenced entities to optimize query creation. Closes #3349 Original pull request: #3922 See also: #3970 Signed-off-by: Jakub Soltys --- .../data/jpa/repository/query/QueryUtils.java | 63 +++++--- .../ReferencingEmbeddedIdExampleEmployee.java | 44 +++++ .../ReferencingIdClassExampleEmployee.java | 44 +++++ .../RepositoryWithCompositeKeyTests.java | 150 ++++++++++++++++++ ...EclipseLinkQueryUtilsIntegrationTests.java | 22 +++ .../query/QueryUtilsIntegrationTests.java | 43 ++++- .../sample/EmployeeRepositoryWithIdClass.java | 3 + ...yeeRepositoryWithEmbeddedIdRepository.java | 35 ++++ ...ployeeRepositoryWithIdClassRepository.java | 34 ++++ .../test/resources/META-INF/persistence.xml | 2 + 10 files changed, 420 insertions(+), 20 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithEmbeddedIdRepository.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithIdClassRepository.java 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 d250c89d7a..21bb552f12 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 @@ -42,6 +42,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Member; import java.util.*; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -88,6 +89,7 @@ * @author Eduard Dudar * @author Yanming Zhou * @author Alim Naizabek + * @author Jakub Soltys */ public abstract class QueryUtils { @@ -773,11 +775,17 @@ static Expression toExpressionRecursively(From from, PropertyPath p boolean isLeafProperty = !property.hasNext(); - boolean requiresOuterJoin = requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin); + boolean isRelationshipId = isRelationshipId(from, property); + boolean requiresOuterJoin = requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin, isLeafProperty, isRelationshipId); - // if it does not require an outer join and is a leaf, simply get the segment - if (!requiresOuterJoin && isLeafProperty) { - return from.get(segment); + // if it does not require an outer join and is a leaf or relationship id, simply get rest of the segment path + if (!requiresOuterJoin && (isLeafProperty || isRelationshipId)) { + Path trailingPath = from.get(segment); + while (property.hasNext()) { + property = property.next(); + trailingPath = trailingPath.get(property.getSegment()); + } + return trailingPath; } // get or create the join @@ -806,10 +814,12 @@ static Expression toExpressionRecursively(From from, PropertyPath p * to generate an explicit outer join in order to prevent Hibernate to use an inner join instead. see * https://hibernate.atlassian.net/browse/HHH-12999 * @param hasRequiredOuterJoin has a parent already required an outer join? + * @param isLeafProperty is leaf property + * @param isRelationshipId whether property path refers to relationship id * @return whether an outer join is to be used for integrating this attribute in a query. */ static boolean requiresOuterJoin(From from, PropertyPath property, boolean isForSelection, - boolean hasRequiredOuterJoin) { + boolean hasRequiredOuterJoin, boolean isLeafProperty, boolean isRelationshipId) { // already inner joined so outer join is useless if (isAlreadyInnerJoined(from, property.getSegment())) { @@ -818,14 +828,7 @@ static boolean requiresOuterJoin(From from, PropertyPath property, boolean Bindable model = from.getModel(); ManagedType managedType = getManagedTypeForModel(model); - Bindable propertyPathModel = getModelForPath(property, managedType, from); - - // is the attribute of Collection type? - boolean isPluralAttribute = model instanceof PluralAttribute; - - if (propertyPathModel == null && isPluralAttribute) { - return true; - } + Bindable propertyPathModel = getModelForPath(property, managedType, () -> from); if (!(propertyPathModel instanceof Attribute attribute)) { return false; @@ -843,14 +846,36 @@ static boolean requiresOuterJoin(From from, PropertyPath property, boolean boolean isInverseOptionalOneToOne = ONE_TO_ONE == attribute.getPersistentAttributeType() && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", "")); - boolean isLeafProperty = !property.hasNext(); - if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) { + if ((isLeafProperty || isRelationshipId) && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) { return false; } return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); } + /** + * Checks if this property path is referencing to relationship id. + * + * @param from the {@link From} to check for attribute model. + * @param property the property path + * @return whether in a query is relationship id. + */ + static boolean isRelationshipId(From from, PropertyPath property) { + if (!property.hasNext()) { + return false; + } + + Bindable model = from.getModel(); + ManagedType managedType = getManagedTypeForModel(model); + Bindable propertyPathModel = getModelForPath(property, managedType, () -> from); + ManagedType propertyPathManagedType = getManagedTypeForModel(propertyPathModel); + Bindable nextPropertyPathModel = getModelForPath(property.next(), propertyPathManagedType, () -> from.get(property.getSegment())); + if (nextPropertyPathModel instanceof SingularAttribute) { + return ((SingularAttribute) nextPropertyPathModel).isId(); + } + return false; + } + @SuppressWarnings("unchecked") static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { @@ -954,14 +979,14 @@ static void checkSortExpression(Order order) { * @param path the current {@link PropertyPath} segment. * @param managedType primary source for the resulting {@link Bindable}. Can be {@literal null}. * @param fallback must not be {@literal null}. - * @return the corresponding {@link Bindable} of {@literal null}. + * @return the corresponding {@link Bindable}. * @see https://hibernate.atlassian.net/browse/HHH-16144 * @see https://github.com/jakartaee/persistence/issues/562 */ - private static @Nullable Bindable getModelForPath(PropertyPath path, @Nullable ManagedType managedType, - Path fallback) { + private static Bindable getModelForPath(PropertyPath path, @Nullable ManagedType managedType, + Supplier> fallback) { String segment = path.getSegment(); if (managedType != null) { @@ -972,7 +997,7 @@ static void checkSortExpression(Order order) { } } - return fallback.get(segment).getModel(); + return fallback.get().get(segment).getModel(); } /** diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java new file mode 100644 index 0000000000..818032d7a0 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java @@ -0,0 +1,44 @@ +/* + * 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.domain.sample; + + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +@Entity +public class ReferencingEmbeddedIdExampleEmployee { + + @Id private long id; + @ManyToOne private EmbeddedIdExampleEmployee employee; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public EmbeddedIdExampleEmployee getEmployee() { + return employee; + } + + public void setEmployee(EmbeddedIdExampleEmployee employee) { + this.employee = employee; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java new file mode 100644 index 0000000000..83544066e6 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java @@ -0,0 +1,44 @@ +/* + * 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.domain.sample; + + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +@Entity +public class ReferencingIdClassExampleEmployee { + + @Id private long id; + @ManyToOne private IdClassExampleEmployee employee; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public IdClassExampleEmployee getEmployee() { + return employee; + } + + public void setEmployee(IdClassExampleEmployee employee) { + this.employee = employee; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java index 99665ecbfd..fa42bedb21 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java @@ -39,6 +39,8 @@ import org.springframework.data.jpa.domain.sample.QIdClassExampleEmployee; import org.springframework.data.jpa.repository.sample.EmployeeRepositoryWithEmbeddedId; import org.springframework.data.jpa.repository.sample.EmployeeRepositoryWithIdClass; +import org.springframework.data.jpa.repository.sample.ReferencingEmployeeRepositoryWithEmbeddedIdRepository; +import org.springframework.data.jpa.repository.sample.ReferencingEmployeeRepositoryWithIdClassRepository; import org.springframework.data.jpa.repository.sample.SampleConfig; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -61,6 +63,10 @@ class RepositoryWithCompositeKeyTests { @Autowired EmployeeRepositoryWithIdClass employeeRepositoryWithIdClass; @Autowired EmployeeRepositoryWithEmbeddedId employeeRepositoryWithEmbeddedId; + @Autowired + ReferencingEmployeeRepositoryWithEmbeddedIdRepository referencingEmployeeRepositoryWithEmbeddedIdRepository; + @Autowired + ReferencingEmployeeRepositoryWithIdClassRepository referencingEmployeeRepositoryWithIdClassRepository; @Autowired EntityManager em; /** @@ -360,4 +366,148 @@ void shouldExecuteExistsQueryForEntitiesWithCompoundIdClassKeys() { assertThat(employeeRepositoryWithIdClass.existsByName(emp1.getName())).isTrue(); assertThat(employeeRepositoryWithIdClass.existsByName("Walter")).isFalse(); } + + @Test // GH-3349 + void findByRelationshipPartialEmbeddedId() { + + EmbeddedIdExampleDepartment dep1 = new EmbeddedIdExampleDepartment(); + dep1.setDepartmentId(1L); + dep1.setName("Dep1"); + + EmbeddedIdExampleDepartment dep2 = new EmbeddedIdExampleDepartment(); + dep2.setDepartmentId(2L); + dep2.setName("Dep2"); + + EmbeddedIdExampleEmployee emp1 = new EmbeddedIdExampleEmployee(); + emp1.setEmployeePk(new EmbeddedIdExampleEmployeePK(1L, 1L)); + emp1.setDepartment(dep1); + emp1 = employeeRepositoryWithEmbeddedId.save(emp1); + + EmbeddedIdExampleEmployee emp2 = new EmbeddedIdExampleEmployee(); + emp2.setEmployeePk(new EmbeddedIdExampleEmployeePK(1L, 2L)); + emp2.setDepartment(dep2); + emp2 = employeeRepositoryWithEmbeddedId.save(emp2); + + ReferencingEmbeddedIdExampleEmployee refEmp1 = new ReferencingEmbeddedIdExampleEmployee(); + refEmp1.setId(1L); + refEmp1.setEmployee(emp1); + refEmp1 = referencingEmployeeRepositoryWithEmbeddedIdRepository.save(refEmp1); + + ReferencingEmbeddedIdExampleEmployee refEmp2 = new ReferencingEmbeddedIdExampleEmployee(); + refEmp2.setId(2L); + refEmp2.setEmployee(emp2); + refEmp2 = referencingEmployeeRepositoryWithEmbeddedIdRepository.save(refEmp2); + + List result = referencingEmployeeRepositoryWithEmbeddedIdRepository.findByEmployee_EmployeePk_employeeId(1L); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result).containsOnly(refEmp1, refEmp2); + + List result2 = referencingEmployeeRepositoryWithEmbeddedIdRepository.findByEmployee_EmployeePk_DepartmentId(2L); + + assertThat(result2).isNotNull(); + assertThat(result2).hasSize(1); + assertThat(result2).containsOnly(refEmp2); + } + + @Test // GH-3349 + void findByRelationshipPartialIdClass() { + + IdClassExampleDepartment dep1 = new IdClassExampleDepartment(); + dep1.setDepartmentId(1L); + dep1.setName("Dep1"); + + IdClassExampleDepartment dep2 = new IdClassExampleDepartment(); + dep2.setDepartmentId(2L); + dep2.setName("Dep2"); + + IdClassExampleEmployee emp1 = new IdClassExampleEmployee(); + emp1.setEmpId(1L); + emp1.setDepartment(dep1); + emp1 = employeeRepositoryWithIdClass.save(emp1); + + IdClassExampleEmployee emp2 = new IdClassExampleEmployee(); + emp2.setEmpId(1L); + emp2.setDepartment(dep2); + emp2 = employeeRepositoryWithIdClass.save(emp2); + + ReferencingIdClassExampleEmployee refEmp1 = new ReferencingIdClassExampleEmployee(); + refEmp1.setId(1L); + refEmp1.setEmployee(emp1); + refEmp1 = referencingEmployeeRepositoryWithIdClassRepository.save(refEmp1); + + ReferencingIdClassExampleEmployee refEmp2 = new ReferencingIdClassExampleEmployee(); + refEmp2.setId(2L); + refEmp2.setEmployee(emp2); + refEmp2 = referencingEmployeeRepositoryWithIdClassRepository.save(refEmp2); + + List result = referencingEmployeeRepositoryWithIdClassRepository.findByEmployee_EmpId(1L); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result).containsOnly(refEmp1, refEmp2); + + List result2 = referencingEmployeeRepositoryWithIdClassRepository.findByEmployee_Department_DepartmentId(2L); + + assertThat(result2).isNotNull(); + assertThat(result2).hasSize(1); + assertThat(result2).containsOnly(refEmp2); + } + + @Test + void findByPartialRelationshipIdClass() { + + IdClassExampleDepartment dep1 = new IdClassExampleDepartment(); + dep1.setDepartmentId(1L); + dep1.setName("Dep1"); + + IdClassExampleDepartment dep2 = new IdClassExampleDepartment(); + dep2.setDepartmentId(2L); + dep2.setName("Dep2"); + + IdClassExampleEmployee emp1 = new IdClassExampleEmployee(); + emp1.setEmpId(1L); + emp1.setDepartment(dep1); + emp1 = employeeRepositoryWithIdClass.save(emp1); + + IdClassExampleEmployee emp2 = new IdClassExampleEmployee(); + emp2.setEmpId(1L); + emp2.setDepartment(dep2); + employeeRepositoryWithIdClass.save(emp2); + + List result = employeeRepositoryWithIdClass.findAllByDepartment_DepartmentId(1L); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result).containsOnly(emp1); + } + + @Test + void findByPartialDirectIdClass() { + + IdClassExampleDepartment dep1 = new IdClassExampleDepartment(); + dep1.setDepartmentId(1L); + dep1.setName("Dep1"); + + IdClassExampleDepartment dep2 = new IdClassExampleDepartment(); + dep2.setDepartmentId(2L); + dep2.setName("Dep2"); + + IdClassExampleEmployee emp1 = new IdClassExampleEmployee(); + emp1.setEmpId(1L); + emp1.setDepartment(dep1); + emp1 = employeeRepositoryWithIdClass.save(emp1); + + IdClassExampleEmployee emp2 = new IdClassExampleEmployee(); + emp2.setEmpId(1L); + emp2.setDepartment(dep2); + emp2 = employeeRepositoryWithIdClass.save(emp2); + + List result = employeeRepositoryWithIdClass.findAllByEmpId(1L); + + assertThat(result).isNotNull(); + assertThat(result).hasSize(2); + assertThat(result).containsOnly(emp1, emp2); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java index ce1b95d90e..1840c07c99 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java @@ -22,6 +22,7 @@ import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.data.jpa.domain.sample.User; @@ -63,4 +64,25 @@ void prefersFetchOverJoin() { assertThat(from.getJoins()).hasSize(1); } + + @Test // GH-3349 + @Disabled + @Override + void doesNotCreateJoinForRelationshipSimpleId() { + //eclipse link produces join for path.get(relationship) + } + + @Test // GH-3349 + @Disabled + @Override + void doesNotCreateJoinForRelationshipEmbeddedId() { + //eclipse link produces join for path.get(relationship) + } + + @Test // GH-3349 + @Disabled + @Override + void doesNotCreateJoinForRelationshipIdClass() { + //eclipse link produces join for path.get(relationship) + } } 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 a7aecc36a7..702c65199b 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,7 +34,6 @@ 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; @@ -56,6 +55,8 @@ import org.springframework.data.jpa.domain.sample.Invoice; import org.springframework.data.jpa.domain.sample.InvoiceItem; import org.springframework.data.jpa.domain.sample.Order; +import org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee; +import org.springframework.data.jpa.domain.sample.ReferencingIdClassExampleEmployee; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.infrastructure.HibernateTestUtils; import org.springframework.data.mapping.PropertyPath; @@ -71,6 +72,7 @@ * @author Patrice Blanchardie * @author Diego Krupitza * @author Krzysztof Krason + * @author Jakub Soltys */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -387,6 +389,45 @@ void demonstrateDifferentBehaviorOfGetJoin() { assertThat(root.getJoins()).hasSize(getNumberOfJoinsAfterCreatingAPath()); } + @Test // GH-3349 + void doesNotCreateJoinForRelationshipSimpleId() { + + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(User.class); + Root from = query.from(User.class); + + QueryUtils.toExpressionRecursively(from, PropertyPath.from("manager.id", User.class)); + + assertThat(from.getFetches()).hasSize(0); + assertThat(from.getJoins()).hasSize(0); + } + + @Test // GH-3349 + void doesNotCreateJoinForRelationshipEmbeddedId() { + + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(ReferencingEmbeddedIdExampleEmployee.class); + Root from = query.from(ReferencingEmbeddedIdExampleEmployee.class); + + QueryUtils.toExpressionRecursively(from, PropertyPath.from("employee.employeePk.employeeId", ReferencingEmbeddedIdExampleEmployee.class)); + + assertThat(from.getFetches()).hasSize(0); + assertThat(from.getJoins()).hasSize(0); + } + + @Test // GH-3349 + void doesNotCreateJoinForRelationshipIdClass() { + + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(ReferencingIdClassExampleEmployee.class); + Root from = query.from(ReferencingIdClassExampleEmployee.class); + + QueryUtils.toExpressionRecursively(from, PropertyPath.from("employee.empId", ReferencingIdClassExampleEmployee.class)); + + assertThat(from.getFetches()).hasSize(0); + assertThat(from.getJoins()).hasSize(0); + } + int getNumberOfJoinsAfterCreatingAPath() { return 0; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithIdClass.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithIdClass.java index e3413fed97..644cc00ded 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithIdClass.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/EmployeeRepositoryWithIdClass.java @@ -41,4 +41,7 @@ public interface EmployeeRepositoryWithIdClass extends JpaRepository findAllByDepartment_DepartmentId(long departmentId); + List findAllByEmpId(long empId); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithEmbeddedIdRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithEmbeddedIdRepository.java new file mode 100644 index 0000000000..3945a78447 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithEmbeddedIdRepository.java @@ -0,0 +1,35 @@ +/* + * 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.sample; + +import org.springframework.context.annotation.Lazy; +import org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee; +import org.springframework.data.jpa.domain.sample.ReferencingIdClassExampleEmployee; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** + * Demonstrates the support for composite primary keys with {@code @IdClass}. + * + * @author Jakub Soltys + */ +@Lazy +public interface ReferencingEmployeeRepositoryWithEmbeddedIdRepository extends JpaRepository { + + List findByEmployee_EmployeePk_employeeId(Long employeeId); + List findByEmployee_EmployeePk_DepartmentId(Long departementId); +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithIdClassRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithIdClassRepository.java new file mode 100644 index 0000000000..d8ab661b82 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/ReferencingEmployeeRepositoryWithIdClassRepository.java @@ -0,0 +1,34 @@ +/* + * 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.sample; + +import org.springframework.context.annotation.Lazy; +import org.springframework.data.jpa.domain.sample.ReferencingIdClassExampleEmployee; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** + * Demonstrates the support for composite primary keys with {@code @IdClass}. + * + * @author Jakub Soltys + */ +@Lazy +public interface ReferencingEmployeeRepositoryWithIdClassRepository extends JpaRepository { + + List findByEmployee_EmpId(Long employeeId); + List findByEmployee_Department_DepartmentId(Long departementId); +} 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 e45c453b10..44bbc1a702 100644 --- a/spring-data-jpa/src/test/resources/META-INF/persistence.xml +++ b/spring-data-jpa/src/test/resources/META-INF/persistence.xml @@ -28,9 +28,11 @@ org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee org.springframework.data.jpa.domain.sample.EmbeddedIdExampleDepartment + org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee org.springframework.data.jpa.domain.sample.EmployeeWithName org.springframework.data.jpa.domain.sample.IdClassExampleEmployee org.springframework.data.jpa.domain.sample.IdClassExampleDepartment + org.springframework.data.jpa.domain.sample.ReferencingIdClassExampleEmployee org.springframework.data.jpa.domain.sample.Invoice org.springframework.data.jpa.domain.sample.InvoiceItem org.springframework.data.jpa.domain.sample.Item From 1e4967584f015197a17c4f8c150724534ed7aef1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Aug 2025 15:37:12 +0200 Subject: [PATCH 170/224] Polishing. Introduce ExpressionFactory to reduce code duplications. Unify JpqlUtils and QueryUtils expression creation to reduce code duplications. Add Eclipselink tests. Many thanks to @academey for design ideas. See #3349 Original pull request: #3922 See also: #3970 --- .../query/ExpressionFactorySupport.java | 196 ++++++++ .../repository/query/JpqlQueryBuilder.java | 10 - .../data/jpa/repository/query/JpqlUtils.java | 188 ++++---- .../data/jpa/repository/query/QueryUtils.java | 432 ++++++++---------- .../ReferencingEmbeddedIdExampleEmployee.java | 6 +- .../ReferencingIdClassExampleEmployee.java | 6 +- ...selinkRepositoryWithCompositeKeyTests.java | 45 ++ .../RepositoryWithCompositeKeyTests.java | 3 + .../query/JpaQueryCreatorTests.java | 31 ++ .../query/QueryUtilsIntegrationTests.java | 12 +- 10 files changed, 564 insertions(+), 365 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionFactorySupport.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipselinkRepositoryWithCompositeKeyTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionFactorySupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionFactorySupport.java new file mode 100644 index 0000000000..aa167eca54 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ExpressionFactorySupport.java @@ -0,0 +1,196 @@ +/* + * 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 jakarta.persistence.metamodel.Attribute.PersistentAttributeType.*; + +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.Bindable; +import jakarta.persistence.metamodel.ManagedType; +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 org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.util.StringUtils; + +/** + * Support class to build expression factories for JPA query creation. + * + * @author Mark Paluch + * @since 4.0 + */ +class ExpressionFactorySupport { + + 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); + } + + /** + * 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 resolver the {@link ModelPathResolver} to check for the model. + * @param property the property path + * @param isForSelection is the property navigated for the selection or ordering part of the query? if true, we need + * to generate an explicit outer join in order to prevent Hibernate to use an inner join instead. see + * https://hibernate.atlassian.net/browse/HHH-12999 + * @param hasRequiredOuterJoin has a parent already required an outer join? + * @param isLeafProperty is leaf property + * @param isRelationshipId whether property path refers to relationship id + * @return whether an outer join is to be used for integrating this attribute in a query. + */ + public boolean requiresOuterJoin(ModelPathResolver resolver, PropertyPath property, boolean isForSelection, + boolean hasRequiredOuterJoin, boolean isLeafProperty, boolean isRelationshipId) { + + Bindable propertyPathModel = resolver.resolve(property); + + if (!(propertyPathModel instanceof Attribute attribute)) { + return false; + } + + // not a persistent attribute type association (@OneToOne, @ManyToOne) + 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 = ONE_TO_ONE == attribute.getPersistentAttributeType() + && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", "")); + + if ((isLeafProperty || isRelationshipId) && !isForSelection && !isCollection && !isInverseOptionalOneToOne + && !hasRequiredOuterJoin) { + return false; + } + + return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); + } + + /** + * Checks if this property path is referencing to relationship id. + * + * @param resolver the {@link ModelPathResolver resolver}. + * @param property the property path. + * @return whether in a query is relationship id. + */ + public boolean isRelationshipId(ModelPathResolver resolver, PropertyPath property) { + + if (!property.hasNext()) { + return false; + } + + Bindable bindable = resolver.resolveNext(property); + return bindable instanceof SingularAttribute sa && sa.isId(); + } + + @SuppressWarnings("unchecked") + 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); + if (annotation == null) { + return defaultValue; + } + + T value = (T) AnnotationUtils.getValue(annotation, propertyName); + return value != null ? value : defaultValue; + } + + /** + * Required for EclipseLink: we try to avoid using from.get as EclipseLink produces an inner join regardless of which + * join operation is specified next + * + * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892 + * @param model + * @return + */ + static @Nullable ManagedType getManagedTypeForModel(@Nullable Object model) { + + if (model instanceof ManagedType managedType) { + return managedType; + } + + if (model instanceof PluralAttribute pa) { + return pa.getElementType() instanceof ManagedType managedType ? managedType : null; + } + + if (!(model instanceof SingularAttribute singularAttribute)) { + return null; + } + + return singularAttribute.getType() instanceof ManagedType managedType ? managedType : null; + } + + public interface ModelPathResolver { + + /** + * Resolve the {@link Bindable} for the given {@link PropertyPath}. + * + * @param propertyPath + * @return + */ + @Nullable + Bindable resolve(PropertyPath propertyPath); + + /** + * Resolve the next {@link Bindable} for the given {@link PropertyPath}. Requires the {@link PropertyPath#hasNext() + * to have a next item}. + * + * @param propertyPath + * @return + */ + @Nullable + Bindable resolveNext(PropertyPath propertyPath); + + } + +} 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 04c4f97892..c41bb8d25b 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 @@ -1019,16 +1019,6 @@ public interface Origin { } - /** - * 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. */ 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 298b095915..7f7028c2a3 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,20 +15,17 @@ */ package org.springframework.data.jpa.repository.query; -import jakarta.persistence.criteria.From; 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 java.util.Objects; import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PropertyPath; -import org.springframework.util.StringUtils; +import org.springframework.util.Assert; /** * Utilities to create JPQL expressions, derived from {@link QueryUtils}. @@ -37,126 +34,129 @@ */ class JpqlUtils { - static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable Metamodel metamodel, - JpqlQueryBuilder.Origin source, Bindable from, PropertyPath property) { + static JpqlQueryBuilder.PathExpression toExpressionRecursively(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) { - return toExpressionRecursively(metamodel, source, from, property, isForSelection, false); + static JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + Bindable from, PropertyPath property, boolean isForSelection) { + return JpqlExpressionFactory.INSTANCE.toExpressionRecursively(metamodel, 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? - * @return the expression + * Expression Factory for JPQL queries that operate on String-based queries. */ - static JpqlQueryBuilder.PathExpression toExpressionRecursively(@Nullable 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, from, property, isForSelection, hasRequiredOuterJoin); + static class JpqlExpressionFactory extends ExpressionFactorySupport { + + private static final JpqlExpressionFactory INSTANCE = new JpqlExpressionFactory(); + + /** + * Creates an expression with proper inner and left joins by recursively navigating the path + * + * @param metamodel the JPA {@link Metamodel} used to resolve attribute types to {@link ManagedType}. + * @param source the {@link org.springframework.data.jpa.repository.query.JpqlQueryBuilder.Origin} + * @param from bindable from which the property is navigated. + * @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? + * @return the expression + */ + public JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamodel, JpqlQueryBuilder.Origin source, + Bindable from, PropertyPath property, boolean isForSelection, boolean hasRequiredOuterJoin) { + + String segment = property.getSegment(); + + boolean isLeafProperty = !property.hasNext(); + BindablePathResolver resolver = new BindablePathResolver(metamodel, from); + boolean isRelationshipId = isRelationshipId(resolver, property); + boolean requiresOuterJoin = requiresOuterJoin(resolver, property, isForSelection, hasRequiredOuterJoin, + isLeafProperty, isRelationshipId); + + // if it does not require an outer join and is a leaf, simply get the segment + if (!requiresOuterJoin && (isLeafProperty || isRelationshipId)) { + return new JpqlQueryBuilder.PathAndOrigin(property, source, false); + } - // 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); - // get or create the join - JpqlQueryBuilder.Join joinSource = requiresOuterJoin ? JpqlQueryBuilder.leftJoin(source, segment) - : JpqlQueryBuilder.innerJoin(source, segment); + // if it's a leaf, return the join + if (isLeafProperty) { + return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true); + } - // 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"); - PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null"); + ManagedType managedTypeForModel = getManagedTypeForModel(from); + Attribute nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from); - ManagedType managedTypeForModel = QueryUtils.getManagedTypeForModel(from); - Attribute nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from); + if (nextAttribute == null) { + throw new IllegalStateException("Binding property is null"); + } - if (nextAttribute == null) { - throw new IllegalStateException("Binding property is null"); + return toExpressionRecursively(metamodel, joinSource, (Bindable) nextAttribute, nextProperty, isForSelection, + requiresOuterJoin); } - return toExpressionRecursively(metamodel, joinSource, (Bindable) nextAttribute, nextProperty, isForSelection, - requiresOuterJoin); - } + private static @Nullable Attribute getModelForPath(@Nullable Metamodel metamodel, PropertyPath path, + @Nullable ManagedType managedType, @Nullable Bindable fallback) { - /** - * 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 bindable - * @param propertyPath - * @param isForSelection - * @param hasRequiredOuterJoin - * @return - */ - static boolean requiresOuterJoin(@Nullable Metamodel metamodel, Bindable bindable, PropertyPath propertyPath, - boolean isForSelection, boolean hasRequiredOuterJoin) { + 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 + } + } - ManagedType managedType = QueryUtils.getManagedTypeForModel(bindable); - Attribute attribute = getModelForPath(metamodel, propertyPath, managedType, bindable); + if (metamodel != null && fallback != null) { - boolean isPluralAttribute = bindable instanceof PluralAttribute; - if (attribute == null) { - return isPluralAttribute; - } + Class fallbackType = fallback.getBindableJavaType(); + try { + return metamodel.managedType(fallbackType).getAttribute(segment); + } catch (IllegalArgumentException e) { + // nothing to do here + } + } - if (!QueryUtils.ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) { - return false; + return null; } - boolean isCollection = attribute.isCollection(); + record BindablePathResolver(Metamodel metamodel, + Bindable bindable) implements ExpressionFactorySupport.ModelPathResolver { - // 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(QueryUtils.getAnnotationProperty(attribute, "mappedBy", "")); + @Override + public @Nullable Bindable resolve(PropertyPath propertyPath) { - boolean isLeafProperty = !propertyPath.hasNext(); - if (isLeafProperty && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) { - return false; - } + Attribute attribute = resolveAttribute(propertyPath); + return attribute instanceof Bindable b ? b : null; + } - return hasRequiredOuterJoin || QueryUtils.getAnnotationProperty(attribute, "optional", true); - } + private @Nullable Attribute resolveAttribute(PropertyPath propertyPath) { + ManagedType managedType = getManagedTypeForModel(bindable); + return getModelForPath(metamodel, propertyPath, managedType, bindable); + } - private static @Nullable Attribute getModelForPath(@Nullable Metamodel metamodel, PropertyPath path, - @Nullable ManagedType managedType, Bindable fallback) { + @Override + @SuppressWarnings("NullAway") + public @Nullable Bindable resolveNext(PropertyPath propertyPath) { - 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 - } - } + Assert.state(propertyPath.hasNext(), "PropertyPath must contain at least one element"); - if (metamodel != null) { + Attribute propertyPathModel = resolveAttribute(propertyPath); + ManagedType propertyPathManagedType = getManagedTypeForModel(propertyPathModel); + Attribute next = getModelForPath(metamodel, Objects.requireNonNull(propertyPath.next()), + propertyPathManagedType, null); - Class fallbackType = fallback.getBindableJavaType(); - try { - return metamodel.managedType(fallbackType).getAttribute(segment); - } catch (IllegalArgumentException e) { - // nothing to do here + return next instanceof Bindable b ? b : null; } + } - 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 21bb552f12..0ef35d2b9d 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 @@ -15,12 +15,9 @@ */ package org.springframework.data.jpa.repository.query; -import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.*; import static java.util.regex.Pattern.*; import jakarta.persistence.EntityManager; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; import jakarta.persistence.Parameter; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; @@ -32,16 +29,18 @@ import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Path; 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.PluralAttribute; -import jakarta.persistence.metamodel.SingularAttribute; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Member; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,7 +48,6 @@ 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; @@ -134,8 +132,6 @@ public abstract class QueryUtils { private static final Pattern CONSTRUCTOR_EXPRESSION; - static final Map> ASSOCIATION_TYPES; - private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3; private static final int VARIABLE_NAME_GROUP_INDEX = 4; private static final int COMPLEX_COUNT_FIRST_INDEX = 3; @@ -170,15 +166,6 @@ public abstract class QueryUtils { COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE | DOTALL); - 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); - builder = new StringBuilder(); builder.append("select"); builder.append("\\s+"); // at least one space separating @@ -578,7 +565,8 @@ 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 */ - public 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"); @@ -748,277 +736,223 @@ private static Nulls toNulls(Sort.NullHandling nullHandling) { }; } - static Expression toExpressionRecursively(From from, PropertyPath property) { - return toExpressionRecursively(from, property, false); - } - - public static Expression toExpressionRecursively(From from, PropertyPath property, - boolean isForSelection) { - return toExpressionRecursively(from, property, isForSelection, false); - } - /** - * Creates an expression with proper inner and left joins by recursively navigating the path + * Check any given {@link JpaOrder#isUnsafe()} order for presence of at least one property offending the + * {@link #PUNCTATION_PATTERN} and throw an {@link Exception} indicating potential unsafe order by expression. * - * @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 + * @param order */ - @SuppressWarnings("unchecked") - static Expression toExpressionRecursively(From from, PropertyPath property, boolean isForSelection, - boolean hasRequiredOuterJoin) { - - String segment = property.getSegment(); - - boolean isLeafProperty = !property.hasNext(); - - boolean isRelationshipId = isRelationshipId(from, property); - boolean requiresOuterJoin = requiresOuterJoin(from, property, isForSelection, hasRequiredOuterJoin, isLeafProperty, isRelationshipId); + static void checkSortExpression(Order order) { - // if it does not require an outer join and is a leaf or relationship id, simply get rest of the segment path - if (!requiresOuterJoin && (isLeafProperty || isRelationshipId)) { - Path trailingPath = from.get(segment); - while (property.hasNext()) { - property = property.next(); - trailingPath = trailingPath.get(property.getSegment()); - } - return trailingPath; + if (order instanceof JpaOrder jpaOrder && jpaOrder.isUnsafe()) { + return; } - // get or create the join - JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER; - Join join = getOrCreateJoin(from, segment, joinType); - - // if it's a leaf, return the join - if (isLeafProperty) { - return (Expression) join; + if (PUNCTATION_PATTERN.matcher(order.getProperty()).find()) { + throw new InvalidDataAccessApiUsageException(String.format(UNSAFE_PROPERTY_REFERENCE, order)); } - - PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null"); - - // recurse with the next property - return toExpressionRecursively(join, 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 from the {@link From} to check for fetches. - * @param property the property path - * @param isForSelection is the property navigated for the selection or ordering part of the query? if true, we need - * to generate an explicit outer join in order to prevent Hibernate to use an inner join instead. see - * https://hibernate.atlassian.net/browse/HHH-12999 - * @param hasRequiredOuterJoin has a parent already required an outer join? - * @param isLeafProperty is leaf property - * @param isRelationshipId whether property path refers to relationship id - * @return whether an outer join is to be used for integrating this attribute in a query. - */ - static boolean requiresOuterJoin(From from, PropertyPath property, boolean isForSelection, - boolean hasRequiredOuterJoin, boolean isLeafProperty, boolean isRelationshipId) { - - // already inner joined so outer join is useless - if (isAlreadyInnerJoined(from, property.getSegment())) { - return false; - } - - Bindable model = from.getModel(); - ManagedType managedType = getManagedTypeForModel(model); - Bindable propertyPathModel = getModelForPath(property, managedType, () -> from); - - if (!(propertyPathModel instanceof Attribute attribute)) { - return false; - } - - // not a persistent attribute type association (@OneToOne, @ManyToOne) - 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 = ONE_TO_ONE == attribute.getPersistentAttributeType() - && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", "")); - - if ((isLeafProperty || isRelationshipId) && !isForSelection && !isCollection && !isInverseOptionalOneToOne && !hasRequiredOuterJoin) { - return false; - } + static Expression toExpressionRecursively(From from, PropertyPath property) { + return toExpressionRecursively(from, property, false); + } - return hasRequiredOuterJoin || getAnnotationProperty(attribute, "optional", true); + public static Expression toExpressionRecursively(From from, PropertyPath property, + boolean isForSelection) { + return FromExpressionFactory.INSTANCE.toExpressionRecursively(from, property, isForSelection, false); } /** - * Checks if this property path is referencing to relationship id. + * Expression factory to create {@link Expression}s from a CriteriaBuilder {@link From}. * - * @param from the {@link From} to check for attribute model. - * @param property the property path - * @return whether in a query is relationship id. + * @since 4.0 */ - static boolean isRelationshipId(From from, PropertyPath property) { - if (!property.hasNext()) { - return false; - } - - Bindable model = from.getModel(); - ManagedType managedType = getManagedTypeForModel(model); - Bindable propertyPathModel = getModelForPath(property, managedType, () -> from); - ManagedType propertyPathManagedType = getManagedTypeForModel(propertyPathModel); - Bindable nextPropertyPathModel = getModelForPath(property.next(), propertyPathManagedType, () -> from.get(property.getSegment())); - if (nextPropertyPathModel instanceof SingularAttribute) { - return ((SingularAttribute) nextPropertyPathModel).isId(); - } - return false; - } - - @SuppressWarnings("unchecked") - static T getAnnotationProperty(Attribute attribute, String propertyName, T defaultValue) { - - Class associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType()); + static class FromExpressionFactory extends ExpressionFactorySupport { + + private static final FromExpressionFactory INSTANCE = new FromExpressionFactory(); + + /** + * 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") + Expression toExpressionRecursively(From from, PropertyPath property, boolean isForSelection, + boolean hasRequiredOuterJoin) { + + String segment = property.getSegment(); + + boolean isLeafProperty = !property.hasNext(); + + FromPathResolver resolver = new FromPathResolver(from); + boolean isRelationshipId = isRelationshipId(resolver, property); + boolean requiresOuterJoin = requiresOuterJoin(resolver, property, isForSelection, hasRequiredOuterJoin, + isLeafProperty, isRelationshipId); + + // if it does not require an outer join and is a leaf or relationship id, simply get rest of the segment path + if (!requiresOuterJoin && (isLeafProperty || isRelationshipId)) { + Path trailingPath = from.get(segment); + while (property.hasNext()) { + property = property.next(); + trailingPath = trailingPath.get(property.getSegment()); + } + return trailingPath; + } - if (associationAnnotation == null) { - return defaultValue; - } + // get or create the join + JoinType joinType = requiresOuterJoin ? JoinType.LEFT : JoinType.INNER; + Join join = getOrCreateJoin(from, segment, joinType); - Member member = attribute.getJavaMember(); + // if it's a leaf, return the join + if (isLeafProperty) { + return (Expression) join; + } - if (!(member instanceof AnnotatedElement annotatedMember)) { - return defaultValue; - } + PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null"); + + // recurse with the next property + return toExpressionRecursively(join, 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 from the {@link From} to check for fetches. + * @param property the property path + * @param isForSelection is the property navigated for the selection or ordering part of the query? if true, we need + * to generate an explicit outer join in order to prevent Hibernate to use an inner join instead. see + * https://hibernate.atlassian.net/browse/HHH-12999 + * @param hasRequiredOuterJoin has a parent already required an outer join? + * @param isLeafProperty is leaf property + * @param isRelationshipId whether property path refers to relationship id + * @return whether an outer join is to be used for integrating this attribute in a query. + */ + private boolean requiresOuterJoin(FromPathResolver resolver, PropertyPath property, boolean isForSelection, + boolean hasRequiredOuterJoin, boolean isLeafProperty, boolean isRelationshipId) { + + // already inner joined so outer join is useless + if (isAlreadyInnerJoined(resolver.from(), property.getSegment())) { + return false; + } - Annotation annotation = AnnotationUtils.getAnnotation(annotatedMember, associationAnnotation); - if (annotation == null) { - return defaultValue; + return super.requiresOuterJoin(resolver, property, isForSelection, hasRequiredOuterJoin, isLeafProperty, + isRelationshipId); } - T value = (T) AnnotationUtils.getValue(annotation, propertyName); - return value != null ? value : defaultValue; - } + /** + * Returns an existing (fetch) join for the given attribute if one already exists or creates a new one if not. + * + * @param from the {@link From} to get the current joins from. + * @param attribute the {@link Attribute} to look for in the current joins. + * @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) { - /** - * Returns an existing (fetch) join for the given attribute if one already exists or creates a new one if not. - * - * @param from the {@link From} to get the current joins from. - * @param attribute the {@link Attribute} to look for in the current joins. - * @param joinType the join type to create if none was found - * @return will never be {@literal null}. - */ - static Join getOrCreateJoin(From from, String attribute, JoinType joinType) { + for (Fetch fetch : from.getFetches()) { - for (Fetch fetch : from.getFetches()) { - - if (fetch instanceof Join join && join.getAttribute().getName().equals(attribute)) { - return join; + if (fetch instanceof Join join && join.getAttribute().getName().equals(attribute)) { + return join; + } } - } - for (Join join : from.getJoins()) { + for (Join join : from.getJoins()) { - if (join.getAttribute().getName().equals(attribute)) { - return join; + if (join.getAttribute().getName().equals(attribute)) { + return join; + } } + return from.join(attribute, joinType); } - return from.join(attribute, joinType); - } - /** - * Return whether the given {@link From} contains an inner join for the attribute with the given name. - * - * @param from the {@link From} to check for joins. - * @param attribute the attribute name to check. - * @return true if the attribute has already been inner joined - */ - static boolean isAlreadyInnerJoined(From from, String attribute) { + /** + * Return whether the given {@link From} contains an inner join for the attribute with the given name. + * + * @param from the {@link From} to check for joins. + * @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) { - for (Fetch fetch : from.getFetches()) { + for (Fetch fetch : from.getFetches()) { - if (fetch.getAttribute().getName().equals(attribute) // - && fetch.getJoinType().equals(JoinType.INNER)) { - return true; + if (fetch.getAttribute().getName().equals(attribute) // + && fetch.getJoinType().equals(JoinType.INNER)) { + return true; + } } - } - for (Join join : from.getJoins()) { + for (Join join : from.getJoins()) { - if (join.getAttribute().getName().equals(attribute) // - && join.getJoinType().equals(JoinType.INNER)) { - return true; + if (join.getAttribute().getName().equals(attribute) // + && join.getJoinType().equals(JoinType.INNER)) { + return true; + } } + + return false; } - return false; - } + record FromPathResolver(From from) implements ModelPathResolver { - /** - * Check any given {@link JpaOrder#isUnsafe()} order for presence of at least one property offending the - * {@link #PUNCTATION_PATTERN} and throw an {@link Exception} indicating potential unsafe order by expression. - * - * @param order - */ - static void checkSortExpression(Order order) { + @Override + public @Nullable Bindable resolve(PropertyPath propertyPath) { - if (order instanceof JpaOrder jpaOrder && jpaOrder.isUnsafe()) { - return; - } + Bindable model = from.getModel(); + ManagedType managedType = getManagedTypeForModel(model); + return getModelForPath(propertyPath, managedType, () -> from); + } - if (PUNCTATION_PATTERN.matcher(order.getProperty()).find()) { - throw new InvalidDataAccessApiUsageException(String.format(UNSAFE_PROPERTY_REFERENCE, order)); - } - } + @Override + @SuppressWarnings("NullAway") + public @Nullable Bindable resolveNext(PropertyPath propertyPath) { - /** - * Get the {@link Bindable model} that corresponds to the given path utilizing the given {@link ManagedType} if - * present or resolving the model from the {@link Path#getModel() path} by creating it via {@link From#get(String)} in - * case where the type signature may be erased by some vendors if the attribute contains generics. - * - * @param path the current {@link PropertyPath} segment. - * @param managedType primary source for the resulting {@link Bindable}. Can be {@literal null}. - * @param fallback must not be {@literal null}. - * @return the corresponding {@link Bindable}. - * @see https://hibernate.atlassian.net/browse/HHH-16144 - * @see https://github.com/jakartaee/persistence/issues/562 - */ - private static Bindable getModelForPath(PropertyPath path, @Nullable ManagedType managedType, - Supplier> fallback) { - - String segment = path.getSegment(); - if (managedType != null) { - try { - return (Bindable) managedType.getAttribute(segment); - } catch (IllegalArgumentException ex) { - // ManagedType may be erased for some vendor if the attribute is declared as generic - } - } + Assert.state(propertyPath.hasNext(), "PropertyPath must contain at least one element"); - return fallback.get().get(segment).getModel(); - } + Bindable propertyPathModel = resolve(propertyPath); - /** - * Required for EclipseLink: we try to avoid using from.get as EclipseLink produces an inner join regardless of which - * join operation is specified next - * - * @see https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892 - * @param model - * @return - */ - static @Nullable ManagedType getManagedTypeForModel(Bindable model) { + ManagedType propertyPathManagedType = getManagedTypeForModel(propertyPathModel); + return getModelForPath(propertyPath.next(), propertyPathManagedType, + () -> from.get(propertyPath.next().getSegment())); + } - if (model instanceof ManagedType managedType) { - return managedType; - } + /** + * Get the {@link Bindable model} that corresponds to the given path utilizing the given {@link ManagedType} if + * present or resolving the model from the {@link Path#getModel() path} by creating it via + * {@link From#get(String)} in case where the type signature may be erased by some vendors if the attribute + * contains generics. + * + * @param path the current {@link PropertyPath} segment. + * @param managedType primary source for the resulting {@link Bindable}. Can be {@literal null}. + * @param fallback must not be {@literal null}. + * @return the corresponding {@link Bindable}. + * @see https://hibernate.atlassian.net/browse/HHH-16144 + * @see https://github.com/jakartaee/persistence/issues/562 + */ + private static Bindable getModelForPath(PropertyPath path, @Nullable ManagedType managedType, + Supplier> fallback) { + + String segment = path.getSegment(); + if (managedType != null) { + try { + return (Bindable) managedType.getAttribute(segment); + } catch (IllegalArgumentException ex) { + // ManagedType may be erased for some vendor if the attribute is declared as generic + } + } - if (!(model instanceof SingularAttribute singularAttribute)) { - return null; + return (Bindable) fallback.get().get(segment); + } } - - return singularAttribute.getType() instanceof ManagedType managedType ? managedType : null; } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java index 818032d7a0..7675699c9d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingEmbeddedIdExampleEmployee.java @@ -23,14 +23,14 @@ @Entity public class ReferencingEmbeddedIdExampleEmployee { - @Id private long id; + @Id private Long id; @ManyToOne private EmbeddedIdExampleEmployee employee; - public long getId() { + public Long getId() { return id; } - public void setId(long id) { + public void setId(Long id) { this.id = id; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java index 83544066e6..301f42b36e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/ReferencingIdClassExampleEmployee.java @@ -23,14 +23,14 @@ @Entity public class ReferencingIdClassExampleEmployee { - @Id private long id; + @Id private Long id; @ManyToOne private IdClassExampleEmployee employee; - public long getId() { + public Long getId() { return id; } - public void setId(long id) { + public void setId(Long id) { this.id = id; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipselinkRepositoryWithCompositeKeyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipselinkRepositoryWithCompositeKeyTests.java new file mode 100644 index 0000000000..5eb94caf89 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipselinkRepositoryWithCompositeKeyTests.java @@ -0,0 +1,45 @@ +/* + * 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; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.ImportResource; +import org.springframework.test.context.ContextConfiguration; + +/** + * Testcase to run {@link RepositoryWithCompositeKeyTests} integration tests on top of EclipseLink. + * + * @author Mark Paluch + */ +@ContextConfiguration +class EclipselinkRepositoryWithCompositeKeyTests extends RepositoryWithCompositeKeyTests { + + @ImportResource({ "classpath:infrastructure.xml", "classpath:eclipselink.xml" }) + static class TestConfig extends RepositoryWithIdClassKeyTests.Config {} + + @Override + @Test + @Disabled("Eclipselink doesn't support batch delete by id with IdClass") + void shouldSupportDeleteAllByIdInBatchWithIdClass() {} + + @Override + @Test + @Disabled("Eclipselink doesn't support derived identities with IdClass") + void shouldSupportSavingEntitiesWithCompositeKeyClassesWithEmbeddedIdsAndDerivedIdentities() {} + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java index fa42bedb21..1a56a77875 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java @@ -37,6 +37,8 @@ import org.springframework.data.jpa.domain.sample.IdClassExampleEmployeePK; import org.springframework.data.jpa.domain.sample.QEmbeddedIdExampleEmployee; import org.springframework.data.jpa.domain.sample.QIdClassExampleEmployee; +import org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee; +import org.springframework.data.jpa.domain.sample.ReferencingIdClassExampleEmployee; import org.springframework.data.jpa.repository.sample.EmployeeRepositoryWithEmbeddedId; import org.springframework.data.jpa.repository.sample.EmployeeRepositoryWithIdClass; import org.springframework.data.jpa.repository.sample.ReferencingEmployeeRepositoryWithEmbeddedIdRepository; @@ -55,6 +57,7 @@ * @author Ernst-Jan van der Laan * @author Krzysztof Krason * @author Aleksei Elin + * @author Jakub Soltys */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = SampleConfig.class) 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 582670223a..a64c4f9fd1 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 @@ -45,6 +45,10 @@ import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Vector; +import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleDepartment; +import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployee; +import org.springframework.data.jpa.domain.sample.EmbeddedIdExampleEmployeePK; +import org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.jpa.util.TestMetaModel; import org.springframework.data.projection.ProjectionFactory; @@ -63,6 +67,9 @@ class JpaQueryCreatorTests { private static final TestMetaModel ORDER = TestMetaModel.hibernateModel(Order.class, LineItem.class, Product.class); private static final TestMetaModel PERSON = TestMetaModel.hibernateModel(Person.class); + private static final TestMetaModel REFERENCE_IDS = TestMetaModel.hibernateModel( + ReferencingEmbeddedIdExampleEmployee.class, EmbeddedIdExampleEmployee.class, EmbeddedIdExampleEmployeePK.class, + EmbeddedIdExampleDepartment.class); static List ignoreCaseTemplates = List.of(JpqlQueryTemplates.LOWER, JpqlQueryTemplates.UPPER); @@ -714,6 +721,30 @@ void exists() { .validateQuery(); } + @Test // GH-3588 + void doesNotCreateJoinForRelationshipEmbeddedId() { + + queryCreator(REFERENCE_IDS) // + .forTree(ReferencingEmbeddedIdExampleEmployee.class, "findByEmployee_EmployeePk_EmployeeId") // + .withParameters(1L) // + .as(QueryCreatorTester::create) // + .expectJpql( + "SELECT r FROM org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee r WHERE r.employee.employeePk.employeeId = ?1") // + .validateQuery(); + } + + @Test // GH-3588 + void createsJoinForReferenceName() { + + queryCreator(REFERENCE_IDS) // + .forTree(ReferencingEmbeddedIdExampleEmployee.class, "findByEmployee_Department_Name") // + .withParameters("foo") // + .as(QueryCreatorTester::create) // + .expectJpql( + "SELECT r FROM org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee r LEFT JOIN r.employee e LEFT JOIN e.department d WHERE d.name = ?1") // + .validateQuery(); + } + QueryCreatorBuilder queryCreator(Metamodel metamodel) { return new DefaultCreatorBuilder(metamodel); } 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 702c65199b..f85973efbf 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 @@ -398,8 +398,8 @@ void doesNotCreateJoinForRelationshipSimpleId() { QueryUtils.toExpressionRecursively(from, PropertyPath.from("manager.id", User.class)); - assertThat(from.getFetches()).hasSize(0); - assertThat(from.getJoins()).hasSize(0); + assertThat(from.getFetches()).isEmpty(); + assertThat(from.getJoins()).isEmpty(); } @Test // GH-3349 @@ -411,8 +411,8 @@ void doesNotCreateJoinForRelationshipEmbeddedId() { QueryUtils.toExpressionRecursively(from, PropertyPath.from("employee.employeePk.employeeId", ReferencingEmbeddedIdExampleEmployee.class)); - assertThat(from.getFetches()).hasSize(0); - assertThat(from.getJoins()).hasSize(0); + assertThat(from.getFetches()).isEmpty(); + assertThat(from.getJoins()).isEmpty(); } @Test // GH-3349 @@ -424,8 +424,8 @@ void doesNotCreateJoinForRelationshipIdClass() { QueryUtils.toExpressionRecursively(from, PropertyPath.from("employee.empId", ReferencingIdClassExampleEmployee.class)); - assertThat(from.getFetches()).hasSize(0); - assertThat(from.getJoins()).hasSize(0); + assertThat(from.getFetches()).isEmpty(); + assertThat(from.getJoins()).isEmpty(); } int getNumberOfJoinsAfterCreatingAPath() { From 66669fa3dcccecdd1e45c967c2a98589f8a0258e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 14 Aug 2025 16:40:47 +0200 Subject: [PATCH 171/224] Polishing. Refine antora documentation keys. See #3952 --- .../modules/ROOT/pages/jpa/entity-persistence.adoc | 3 ++- src/main/antora/resources/antora-resources/antora.yml | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc b/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc index 990c959441..ac8fcd4a75 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc @@ -19,7 +19,8 @@ Spring Data JPA offers the following strategies to detect whether an entity is n If the identifier property is `null`, then the entity is assumed to be new. Otherwise, it is assumed to be not new. In contrast to other Spring Data modules, JPA considers `0` (zero) as the first inserted version of an entity and therefore, a primitive version property cannot be used to determine whether an entity is new or not. -2. Implementing `Persistable`: If an entity implements `Persistable`, Spring Data JPA delegates the new detection to the `isNew(…)` method of the entity. See the link:$$https://docs.spring.io/spring-data/data-commons/docs/current/api/index.html?org/springframework/data/domain/Persistable.html$$[JavaDoc] for details. +2. Implementing `Persistable`: If an entity implements `Persistable`, Spring Data JPA delegates the new detection to the `isNew(…)` method of the entity. +See the link:{spring-data-commons-javadoc-base}/org/springframework/data/domain/Persistable.html[JavaDoc] for details. 3. Implementing `EntityInformation`: You can customize the `EntityInformation` abstraction used in the `SimpleJpaRepository` implementation by creating a subclass of `JpaRepositoryFactory` and overriding the `getEntityInformation(…)` method accordingly. You then have to register the custom implementation of `JpaRepositoryFactory` as a Spring bean. Note that this should be rarely necessary. See the javadoc:org.springframework.data.jpa.repository.support.JpaRepositoryFactory[JavaDoc] for details. Option 1 is not an option for entities that use manually assigned identifiers and no version attribute as with those the identifier will always be non-`null`. diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index eedc4999e3..a037a43a83 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -3,18 +3,19 @@ prerelease: ${antora-component.prerelease} asciidoc: attributes: + attribute-missing: 'warn' + chomp: 'all' version: ${project.version} copyright-year: ${current.year} springversionshort: ${spring.short} springversion: ${spring} - attribute-missing: 'warn' commons: ${springdata.commons.docs} include-xml-namespaces: false - spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference - spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/ + spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference/{commons} + spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java' springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api spring-framework-docs: '{springdocsurl}' + springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api spring-framework-javadoc: '{springjavadocurl}' springhateoasversion: ${spring-hateoas} hibernatejavadocurl: https://docs.jboss.org/hibernate/orm/6.6/javadocs/ From 6e0ed025ff6774085f41e052ca7c4ebfa57de3fe Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:44:52 +0200 Subject: [PATCH 172/224] Prepare 4.0 M5 (2025.1.0). See #3952 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 16eaf699f7..978e0f8e21 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-M5 @@ -39,7 +39,7 @@ 9.2.0 42.7.7 23.8.0.25.04 - 4.0.0-SNAPSHOT + 4.0.0-M5 0.10.3 org.hibernate @@ -193,20 +193,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From a90271b8b7bfb159a9452c042b5a1da19d3f0234 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:45:10 +0200 Subject: [PATCH 173/224] Release version 4.0 M5 (2025.1.0). See #3952 --- 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 978e0f8e21..cea3dbe500 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-M5 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..e1f1fde2fb 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-M5 org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M5 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..6e55f3677f 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-M5 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index cbec8a2645..513a2887b7 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-M5 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-M5 ../pom.xml From ab07a5c36353611ce44ec118c43f78f725c34be1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:47:28 +0200 Subject: [PATCH 174/224] Prepare next development iteration. See #3952 --- 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 cea3dbe500..978e0f8e21 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M5 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index e1f1fde2fb..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-M5 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-M5 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 6e55f3677f..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-M5 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 513a2887b7..cbec8a2645 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-M5 + 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-M5 + 4.0.0-SNAPSHOT ../pom.xml From 5ed83deb114c62c612409afa8bf3b5736af971e1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:47:29 +0200 Subject: [PATCH 175/224] After release cleanups. See #3952 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 978e0f8e21..16eaf699f7 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M5 + 4.0.0-SNAPSHOT @@ -39,7 +39,7 @@ 9.2.0 42.7.7 23.8.0.25.04 - 4.0.0-M5 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -193,8 +193,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From db92dc0b41b43dd4fd3a79e545979f808a5e8280 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 14:13:08 +0200 Subject: [PATCH 176/224] Refine version properties for documentation build. See spring-projects/spring-data-build#2638 --- src/main/antora/antora-playbook.yml | 2 +- .../resources/antora-resources/antora.yml | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml index 5465baed5d..22b7c06465 100644 --- a/src/main/antora/antora-playbook.yml +++ b/src/main/antora/antora-playbook.yml @@ -17,7 +17,7 @@ content: - url: https://github.com/spring-projects/spring-data-commons # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: [ 4.0.x ] + branches: [ main ] start_path: src/main/antora asciidoc: attributes: diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index a037a43a83..b0a1b58fdb 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -5,19 +5,19 @@ asciidoc: attributes: attribute-missing: 'warn' chomp: 'all' - version: ${project.version} - copyright-year: ${current.year} - springversionshort: ${spring.short} - springversion: ${spring} - commons: ${springdata.commons.docs} + version: '${project.version}' + copyright-year: '${current.year}' + springversionshort: '${spring.short}' + springversion: '${spring}' + commons: '${springdata.commons.docs}' include-xml-namespaces: false - spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference/{commons} + spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}' spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java' - springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} + springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}' spring-framework-docs: '{springdocsurl}' - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api + springjavadocurl: '${documentation.spring-javadoc-url}' spring-framework-javadoc: '{springjavadocurl}' - springhateoasversion: ${spring-hateoas} + springhateoasversion: '${spring-hateoas}' hibernatejavadocurl: https://docs.jboss.org/hibernate/orm/6.6/javadocs/ - releasetrainversion: ${releasetrain} + releasetrainversion: '${releasetrain}' store: Jpa From e4fe9e22e70fe37efdc6a032061c18c6a6385214 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Aug 2025 16:03:54 +0200 Subject: [PATCH 177/224] Polishing. Update documentation for AOT support. See #3977 --- src/main/antora/modules/ROOT/pages/jpa/aot.adoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc index 5d71d15e31..ec06946abd 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/aot.adoc @@ -72,18 +72,18 @@ These are typically all query methods that are not backed by an xref:repositorie **Supported Features** * Derived query methods, `@Query`/`@NativeQuery` and named query methods +* Stored procedure query methods annotated with `@Procedure` * `@Modifying` methods returning `void` or `int` * `@QueryHints` support * Pagination, `Slice`, `Stream`, and `Optional` return types * Sort query rewriting -* DTO Projections +* Interface and 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 @@ -92,7 +92,6 @@ Mind that using Value Expressions requires expression parsing and contextual inf * `CrudRepository`, Querydsl, Query by Example, and other base interface methods as their implementation is provided by the base class respective fragments * Methods whose implementation would be overly complex ** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) -** Stored procedure query methods annotated with `@Procedure` ** Dynamic projections [[aot.repositories.json]] @@ -113,7 +112,7 @@ interface UserRepository extends CrudRepository { Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); <2> @Query("select u from User u where u.emailAddress = ?1") - User findAnnotatedQueryByEmailAddress(String username); <3> + User findAnnotatedQueryByEmailAddress(String emailAddress); <3> User findByEmailAddress(String emailAddress); <4> @@ -134,7 +133,7 @@ While stored procedure methods are included in JSON metadata, their method code ---- { "name": "com.acme.UserRepository", - "module": "", + "module": "JPA", "type": "IMPERATIVE", "methods": [ { From 64c5743c95a141a1c886b373991f573d6044fde8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Aug 2025 09:25:37 +0200 Subject: [PATCH 178/224] Add tests to verify entityName resolution in templated native query methods. Closes #3979 --- .../jpa/repository/query/TemplatedQueryUnitTests.java | 8 ++++++++ .../data/jpa/repository/sample/UserRepository.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java index 6581b628f7..b31be6de60 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TemplatedQueryUnitTests.java @@ -73,6 +73,14 @@ void renderAliasInExpressionQueryCorrectly() { assertThat(query.getQueryString()).isEqualTo("select u from User u"); } + @Test // GH-3979 + void renderAliasInNativeExpressionQueryCorrectly() { + + DefaultEntityQuery query = nativeEntityQuery("select u.* from #{#entityName} u"); + assertThat(query.getAlias()).isEqualTo("u"); + assertThat(query.getQueryString()).isEqualTo("select u.* from User u"); + } + @Test // DATAJPA-1695 void shouldDetectBindParameterCountCorrectly() { 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 161bb33a28..600bf3f0d1 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 @@ -609,7 +609,7 @@ Page findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, Map findMapWithNullValues(); // DATAJPA-1307 - @Query(value = "select * from SD_User u where u.emailAddress = ?", nativeQuery = true) + @Query(value = "select * from SD_#{#entityName} u where u.emailAddress = ?", nativeQuery = true) User findByEmailNativeAddressJdbcStyleParameter(String emailAddress); // DATAJPA-1334 From 71f15d8ec9c89c718a93cd501fa5eff96501b459 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Aug 2025 10:21:24 +0200 Subject: [PATCH 179/224] Reinstate parameter per entity for batch deletes using EclipseLink. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EclipseLink doesn't support WHERE e IN (:entities) and requires e = ?1 OR e = ?2 OR … style. Closes #3983 --- .../data/jpa/repository/query/QueryUtils.java | 57 ++++++++++++++++++- .../jpa/repository/UserRepositoryTests.java | 2 +- ...EclipseLinkQueryUtilsIntegrationTests.java | 30 ++++++++-- .../query/QueryUtilsIntegrationTests.java | 25 ++++++++ 4 files changed, 107 insertions(+), 7 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 0ef35d2b9d..0e45864378 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 @@ -52,6 +52,7 @@ 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.jpa.provider.PersistenceProvider; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -516,6 +517,21 @@ private static Integer findClose(final Integer open, final List closes, * @return Guaranteed to be not {@literal null}. */ public static Query applyAndBind(String queryString, Iterable entities, EntityManager entityManager) { + return applyAndBind(queryString, entities, entityManager, PersistenceProvider.fromEntityManager(entityManager)); + } + + /** + * Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given + * entities to the query. + * + * @param type of the entities. + * @param queryString must not be {@literal null}. + * @param entities must not be {@literal null}. + * @param entityManager must not be {@literal null}. + * @return Guaranteed to be not {@literal null}. + */ + static Query applyAndBind(String queryString, Iterable entities, EntityManager entityManager, + PersistenceProvider persistenceProvider) { Assert.notNull(queryString, "Querystring must not be null"); Assert.notNull(entities, "Iterable of entities must not be null"); @@ -527,9 +543,46 @@ public static Query applyAndBind(String queryString, Iterable entities, E return entityManager.createQuery(queryString); } + if (persistenceProvider == PersistenceProvider.HIBERNATE) { + + String alias = detectAlias(queryString); + Query query = entityManager.createQuery("%s where %s IN (?1)".formatted(queryString, alias)); + query.setParameter(1, entities instanceof Collection ? entities : Streamable.of(entities).toList()); + + return query; + } + + return applyWhereEqualsAndBind(queryString, entities, entityManager, iterator); + } + + private static Query applyWhereEqualsAndBind(String queryString, Iterable entities, EntityManager entityManager, + Iterator iterator) { + String alias = detectAlias(queryString); - Query query = entityManager.createQuery("%s where %s IN (?1)".formatted(queryString, alias)); - query.setParameter(1, entities instanceof Collection ? entities : Streamable.of(entities).toList()); + StringBuilder builder = new StringBuilder(queryString); + builder.append(" where"); + + int i = 0; + + while (iterator.hasNext()) { + + iterator.next(); + + builder.append(String.format(" %s = ?%d", alias, ++i)); + + if (iterator.hasNext()) { + builder.append(" or"); + } + } + + Query query = entityManager.createQuery(builder.toString()); + + iterator = entities.iterator(); + i = 0; + + while (iterator.hasNext()) { + query.setParameter(++i, iterator.next()); + } return query; } 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 8980836d8d..126c5ce6be 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 @@ -3656,7 +3656,7 @@ private interface UserProjectionInterfaceBased { String getLastname(); } - record UserDto(Integer id, String firstname, String lastname, String emailAddress) { + public record UserDto(Integer id, String firstname, String lastname, String emailAddress) { } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java index 1840c07c99..0b18feba5c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java @@ -22,12 +22,16 @@ import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; +import java.util.List; + +import org.eclipse.persistence.internal.jpa.EJBQueryImpl; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.mapping.PropertyPath; import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; /** * EclipseLink variant of {@link QueryUtilsIntegrationTests}. @@ -64,25 +68,43 @@ void prefersFetchOverJoin() { assertThat(from.getJoins()).hasSize(1); } - @Test // GH-3349 @Disabled @Override void doesNotCreateJoinForRelationshipSimpleId() { - //eclipse link produces join for path.get(relationship) + // eclipse link produces join for path.get(relationship) } @Test // GH-3349 @Disabled @Override void doesNotCreateJoinForRelationshipEmbeddedId() { - //eclipse link produces join for path.get(relationship) + // eclipse link produces join for path.get(relationship) } @Test // GH-3349 @Disabled @Override void doesNotCreateJoinForRelationshipIdClass() { - //eclipse link produces join for path.get(relationship) + // eclipse link produces join for path.get(relationship) + } + + @Test // GH-3983, GH-2870 + @Disabled("Not supported by EclipseLink") + @Transactional + @Override + void applyAndBindOptimizesIn() {} + + @Test // GH-3983, GH-2870 + @Transactional + @Override + void applyAndBindExpandsToPositionalPlaceholders() { + + em.getCriteriaBuilder(); + EJBQueryImpl query = (EJBQueryImpl) QueryUtils.applyAndBind("DELETE FROM User u", + List.of(new User(), new User()), em.unwrap(null)); + + assertThat(query.getDatabaseQuery().getJPQLString()).isEqualTo("DELETE FROM User u where u = ?1 or u = ?2"); } + } 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 f85973efbf..6797362679 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 @@ -45,6 +45,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import org.hibernate.query.sqm.internal.SqmQueryImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; @@ -62,6 +63,7 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; /** * Integration tests for {@link QueryUtils}. @@ -428,6 +430,29 @@ void doesNotCreateJoinForRelationshipIdClass() { assertThat(from.getJoins()).isEmpty(); } + @Test // GH-3983, GH-2870 + @Transactional + void applyAndBindOptimizesIn() { + + em.getCriteriaBuilder(); + SqmQueryImpl query = (SqmQueryImpl) QueryUtils + .applyAndBind("DELETE FROM User u", List.of(new User(), new User()), em.unwrap(null)); + + assertThat(query.getQueryString()).isEqualTo("DELETE FROM User u where u IN (?1)"); + } + + @Test // GH-3983, GH-2870 + @Transactional + void applyAndBindExpandsToPositionalPlaceholders() { + + em.getCriteriaBuilder(); + SqmQueryImpl query = (SqmQueryImpl) QueryUtils + .applyAndBind("DELETE FROM User u", List.of(new User(), new User()), em.unwrap(null), + org.springframework.data.jpa.provider.PersistenceProvider.ECLIPSELINK); + + assertThat(query.getQueryString()).isEqualTo("DELETE FROM User u where u = ?1 or u = ?2"); + } + int getNumberOfJoinsAfterCreatingAPath() { return 0; } From f13eaf5ec457d9791fdb01a99debbecfbb4c4320 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Aug 2025 10:51:02 +0200 Subject: [PATCH 180/224] Add missing nullability type constraints to `findBy` methods. Closes #3986 --- .../jpa/repository/support/QuerydslJpaPredicateExecutor.java | 3 ++- .../data/jpa/repository/support/SimpleJpaRepository.java | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 d04ca6d8b5..3d95f6280e 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 @@ -181,7 +181,8 @@ public Page findAll(Predicate predicate, Pageable pageable) { @SuppressWarnings("unchecked") @Override - public R findBy(Predicate predicate, Function, R> queryFunction) { + public R findBy(Predicate predicate, + Function, R> queryFunction) { Assert.notNull(predicate, PREDICATE_MUST_NOT_BE_NULL); Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_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 2cbd99c51c..619ccf14f8 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 @@ -518,7 +518,7 @@ public long delete(DeleteSpecification spec) { } @Override - public R findBy(Specification spec, + public R findBy(Specification spec, Function, R> queryFunction) { Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL); @@ -626,7 +626,8 @@ public Page findAll(Example example, Pageable pageable) { @Override @SuppressWarnings("unchecked") - public R findBy(Example example, Function, R> queryFunction) { + public R findBy(Example example, + Function, R> queryFunction) { Assert.notNull(example, EXAMPLE_MUST_NOT_BE_NULL); Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL); From 6bf361063367a2bb8119ed6ae3056a1f758c561b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Aug 2025 11:15:51 +0200 Subject: [PATCH 181/224] Refine AOT fragment constructor parameter lookup. Closes #3991 --- .../aot/JpaRepositoryContributor.java | 17 ++++++++++++++++- .../data/jpa/repository/aot/QueriesFactory.java | 2 +- 2 files changed, 17 insertions(+), 2 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 5712b38893..91f9575c53 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 @@ -27,6 +27,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -120,7 +121,16 @@ protected void customizeClass(AotRepositoryClassBuilder classBuilder) { @Override protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { - constructorBuilder.addParameter("entityManager", EntityManager.class); + String entityManagerFactoryRef = getEntityManagerFactoryRef(); + + constructorBuilder.addParameter("entityManager", EntityManager.class, customizer -> { + + customizer.bindToField().origin( + StringUtils.hasText(entityManagerFactoryRef) + ? new RuntimeBeanReference(entityManagerFactoryRef, EntityManager.class) + : new RuntimeBeanReference(EntityManager.class)); + }); + constructorBuilder.addParameter("context", RepositoryFactoryBeanSupport.FragmentCreationContext.class); Optional> queryEnhancerSelector = getQueryEnhancerSelectorClass(); @@ -135,6 +145,11 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB }); } + private String getEntityManagerFactoryRef() { + return context.getConfigurationSource().getAttribute("entityManagerFactoryRef") + .filter(it -> !"entityManagerFactory".equals(it)).orElse(null); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) private Optional> getQueryEnhancerSelectorClass() { return (Optional) context.getConfigurationSource().getAttribute("queryEnhancerSelector", Class.class) 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 df59aa8b46..3d5bee6bf9 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 @@ -141,7 +141,7 @@ private boolean hasNamedQuery(ReturnedType returnedType, String queryName) { private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, MergedAnnotation query, JpaQueryMethod queryMethod) { - UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName()); + UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getSimpleName()); boolean isNative = query.getBoolean("nativeQuery"); Function queryFunction = isNative ? DeclaredQuery::nativeQuery : DeclaredQuery::jpqlQuery; queryFunction = operator.andThen(queryFunction); From d73a925651678ad3a1822ef089865e4e3acad7de Mon Sep 17 00:00:00 2001 From: Minho Park Date: Tue, 26 Aug 2025 19:30:07 +0900 Subject: [PATCH 182/224] =?UTF-8?q?Qualify=20identifier=20used=20in=20`Sim?= =?UTF-8?q?pleJpaRepository.deleteAllByIdInBatch(=E2=80=A6)`=20JPQL.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Minho Park Closes #3990 Original pull request: #3993 --- .../springframework/data/jpa/repository/query/QueryUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0e45864378..40c61a765f 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 @@ -94,7 +94,7 @@ public abstract class QueryUtils { public static final String COUNT_QUERY_STRING = "select count(%s) from %s x"; public static final String DELETE_ALL_QUERY_STRING = "delete from %s x"; - public static final String DELETE_ALL_QUERY_BY_ID_STRING = "delete from %s x where %s in :ids"; + public static final String DELETE_ALL_QUERY_BY_ID_STRING = "delete from %s x where x.%s in :ids"; // Used Regex/Unicode categories (see https://www.unicode.org/reports/tr18/#General_Category_Property): // Z Separator From 48bc6aa692729cae6e8f02d4f2371e413ca43350 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Aug 2025 13:48:05 +0200 Subject: [PATCH 183/224] Polishing. Add integration tests. See #3990 Original pull request: #3993 --- .../EclipseLinkJpaRepositoryTests.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java index 4892d568c1..95a9308e02 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java @@ -15,7 +15,18 @@ */ package org.springframework.data.jpa.repository.support; +import static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.ContextConfiguration; /** @@ -23,10 +34,49 @@ * * @author Oliver Gierke * @author Greg Turnquist + * @author Mark Paluch */ @ContextConfiguration("classpath:eclipselink.xml") class EclipseLinkJpaRepositoryTests extends JpaRepositoryTests { + @PersistenceContext EntityManager em; + + SimpleJpaRepository repository; + User firstUser, secondUser; + + @BeforeEach + @Override + void setUp() { + + super.setUp(); + + repository = new SimpleJpaRepository<>(User.class, em); + + firstUser = new User("Oliver", "Gierke", "gierke@synyx.de"); + firstUser.setAge(28); + secondUser = new User("Joachim", "Arrasz", "arrasz@synyx.de"); + secondUser.setAge(35); + + repository.deleteAll(); + repository.saveAllAndFlush(List.of(firstUser, secondUser)); + } + + @Test // GH-3990 + void deleteAllBySimpleIdInBatch() { + + repository.deleteAllByIdInBatch(List.of(firstUser.getId(), secondUser.getId())); + + assertThat(repository.count()).isZero(); + } + + @Test // GH-3990 + void deleteAllInBatch() { + + repository.deleteAllInBatch(List.of(firstUser, secondUser)); + + assertThat(repository.count()).isZero(); + } + @Override @Disabled("https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477") void deleteAllByIdInBatch() { @@ -38,4 +88,5 @@ void deleteAllByIdInBatch() { void deleteAllByIdInBatchShouldConvertAnIterableToACollection() { // disabled } + } From 8ffc585d1fd7040b7e5b4a667407219b0d98a97e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 29 Aug 2025 16:27:47 +0200 Subject: [PATCH 184/224] Polishing. Use refined convenience methods from AotQueryMethodGenerationContext. See #3991 --- .../jpa/repository/aot/JpaCodeBlocks.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 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 aae52c1329..45eae1fa92 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 @@ -20,7 +20,6 @@ import jakarta.persistence.QueryHint; import jakarta.persistence.Tuple; -import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -160,8 +159,6 @@ 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(); String dynamicReturnType = null; if (queryMethod.getParameters().hasDynamicProjection()) { @@ -212,7 +209,7 @@ public CodeBlock build() { if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType)) && queries != null && queries.result() instanceof StringAotQuery && StringUtils.hasText(queryStringVariableName)) { - builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringVariableName, actualReturnType)); + builder.add(applyRewrite(sortParameterName, dynamicReturnType, isProjecting, queryStringVariableName)); } builder.add(createQuery(false, queryVariableName, queryStringVariableName, queryRewriterName, queries.result(), @@ -246,8 +243,8 @@ private CodeBlock buildQueryString(StringAotQuery sq, String queryStringVariable return builder.build(); } - private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, String queryString, - Class actualReturnType) { + private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, boolean isProjecting, + String queryString) { Builder builder = CodeBlock.builder(); @@ -266,6 +263,9 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe builder.addStatement("$L = rewriteQuery($L, $L, $L)", queryString, context.localVariable("declaredQuery"), sort, dynamicReturnType); } else if (hasSort) { + + Object actualReturnType = isProjecting ? context.getActualReturnTypeName() : context.getDomainType(); + builder.addStatement("$L = rewriteQuery($L, $L, $T.class)", queryString, context.localVariable("declaredQuery"), sort, actualReturnType); } else if (hasDynamicReturnType) { @@ -657,11 +657,9 @@ public CodeBlock build() { Builder builder = CodeBlock.builder(); - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); - Type actualReturnType = isProjecting ? context.getActualReturnType().getType() - : context.getRepositoryInformation().getDomainType(); + boolean isProjecting = !ObjectUtils.nullSafeEquals(context.getDomainType(), context.getActualReturnTypeName()); + TypeName actualReturnType = isProjecting ? context.getActualReturnTypeName() + : TypeName.get(context.getDomainType()); builder.add("\n"); if (modifying.isPresent()) { @@ -775,7 +773,7 @@ public CodeBlock build() { 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(), - context.getActualReturnType().toClass()); + queryResultType); } else { builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)", context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), From a1fbb5a4d271cc77aa8d81511898e53491d671ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=9D=AC=EC=9D=80?= Date: Fri, 29 Aug 2025 12:11:04 +0900 Subject: [PATCH 185/224] Reintroduce `Specification.where(Specification)` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reintroduce the overload to improve the migration path for users upgrading to Spring Data JPA 4.0 and to restore the intuitive fluent API but this time, the method does not accept null values. Closes #3992 Original pull request: #3998 Signed-off-by: 희은 --- .../data/jpa/domain/Specification.java | 16 ++++++++++ .../jpa/domain/SpecificationUnitTests.java | 31 +++++++++++++++++++ 2 files changed, 47 insertions(+) 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 b32dc1fa67..3e5fb2874b 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 @@ -48,6 +48,7 @@ * @author Jens Schauder * @author Daniel Shuy * @author Sergey Rukin + * @author Heeeun Cho */ @FunctionalInterface public interface Specification extends Serializable { @@ -78,6 +79,21 @@ static Specification where(PredicateSpecification spec) { return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder); } + /** + * Creates a {@link Specification} from the given {@link Specification}. This is a factory method for fluent composition. + * + * @param the type of the {@link Root} the resulting {@literal Specification} operates on. + * @param spec must not be {@literal null}. + * @return the given specification. + * @since 4.1 + */ + static Specification where(Specification spec) { + + Assert.notNull(spec, "Specification must not be null"); + + return spec; + } + /** * ANDs the given {@link Specification} to the current one. * 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 f819ed9a56..a6219a7a0b 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 @@ -42,6 +42,7 @@ * @author Jens Schauder * @author Mark Paluch * @author Daniel Shuy + * @author Heeeun Cho */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -137,6 +138,36 @@ void notWithNullPredicate() { verify(builder).disjunction(); } + @Test // GH-3992 + void whereWithSpecificationReturnsSameSpecification() { + + Specification originalSpec = (r, q, cb) -> predicate; + Specification wrappedSpec = Specification.where(originalSpec); + + assertThat(wrappedSpec).isSameAs(originalSpec); + } + + @Test // GH-3992 + void whereWithSpecificationSupportsFluentComposition() { + + Specification firstSpec = (r, q, cb) -> predicate; + Specification secondSpec = (r, q, cb) -> predicate; + + Specification composedSpec = Specification.where(firstSpec).and(secondSpec); + + assertThat(composedSpec).isNotNull(); + composedSpec.toPredicate(root, query, builder); + verify(builder).and(predicate, predicate); + } + + @Test // GH-3992 + void whereWithNullSpecificationThrowsException() { + + assertThatThrownBy(() -> Specification.where((Specification) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Specification must not be null"); + } + static class SerializableSpecification implements Serializable, Specification { @Override From f50d3567fe48139282a3eb2b27f0b2b1a1683f55 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 1 Sep 2025 08:44:19 +0200 Subject: [PATCH 186/224] Polishing. Reinstate original Javadoc, add note about nullability. Fix contract annotation, refine tests. See #3992 Original pull request: #3998 --- .../data/jpa/domain/Specification.java | 28 ++++++++++--------- .../repository/query/ParameterBinding.java | 2 +- .../jpa/domain/SpecificationUnitTests.java | 5 +--- 3 files changed, 17 insertions(+), 18 deletions(-) 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 3e5fb2874b..8427c5e8aa 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 @@ -65,33 +65,35 @@ static Specification unrestricted() { } /** - * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to - * {@link Specification}. + * Simple static factory method to add some syntactic sugar around a {@link Specification}. * + * @implNote does not accept {@literal null} values since 4.0, use {@link #unrestricted()} instead of passing + * {@literal null} values. * @param the type of the {@link Root} the resulting {@literal Specification} operates on. - * @param spec the {@link PredicateSpecification} to wrap. + * @param spec can be {@literal null}. * @return guaranteed to be not {@literal null}. + * @since 2.0 */ - static Specification where(PredicateSpecification spec) { + static Specification where(Specification spec) { - Assert.notNull(spec, "PredicateSpecification must not be null"); + Assert.notNull(spec, "Specification must not be null"); - return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder); + return spec; } /** - * Creates a {@link Specification} from the given {@link Specification}. This is a factory method for fluent composition. + * 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 must not be {@literal null}. - * @return the given specification. - * @since 4.1 + * @param spec the {@link PredicateSpecification} to wrap. + * @return guaranteed to be not {@literal null}. */ - static Specification where(Specification spec) { + static Specification where(PredicateSpecification spec) { - Assert.notNull(spec, "Specification must not be null"); + Assert.notNull(spec, "PredicateSpecification must not be null"); - return spec; + return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder); } /** 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 0a9a03b6b4..2122e250e5 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 @@ -291,7 +291,7 @@ public JpqlQueryTemplates getTemplates() { @SuppressWarnings("unchecked") - @Contract("false, _ -> param2; _, null -> null; true, !null -> new)") + @Contract("false, _ -> param2; _, null -> null; true, !null -> new") private @Nullable Collection potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection collection) { if (!ignoreCase || CollectionUtils.isEmpty(collection)) { 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 a6219a7a0b..cee8386513 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 @@ -162,10 +162,7 @@ void whereWithSpecificationSupportsFluentComposition() { @Test // GH-3992 void whereWithNullSpecificationThrowsException() { - - assertThatThrownBy(() -> Specification.where((Specification) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Specification must not be null"); + assertThatIllegalArgumentException().isThrownBy(() -> Specification.where((Specification) null)); } static class SerializableSpecification implements Serializable, Specification { From 9025ca3882690899c25b82f35f7f1060d25c28e7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 Sep 2025 11:35:19 +0200 Subject: [PATCH 187/224] Skip fenced comments in HQL, EQL and JPQL parsers. Align with Hibernate and allow comments also in EQL and JPQL. Closes #3997 --- .../data/jpa/repository/query/Eql.g4 | 1 + .../data/jpa/repository/query/Hql.g4 | 1 + .../data/jpa/repository/query/Jpql.g4 | 1 + .../repository/query/JpaQueryEnhancer.java | 4 +- .../query/JpaQueryEnhancerUnitTests.java | 57 +++++++++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancerUnitTests.java 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 73b118ebed..9b9f634190 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 @@ -883,6 +883,7 @@ reserved_word WS : [ \t\r\n] -> channel(HIDDEN) ; +COMMENT : '/*' (~'*' | '*' ~'/' )* '*/' -> skip; // Build up case-insentive tokens 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 c0fbf35ee0..1e09319885 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 @@ -1779,6 +1779,7 @@ identifier WS : [ \t\r\n] -> channel(HIDDEN); +COMMENT : '/*' (~'*' | '*' ~'/' )* '*/' -> skip; // Build up case-insentive tokens 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 6653f02bcd..6e8f374777 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 @@ -885,6 +885,7 @@ reserved_word WS : [ \t\r\n] -> channel(HIDDEN) ; +COMMENT : '/*' (~'*' | '*' ~'/' )* '*/' -> skip; // Build up case-insentive tokens 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 04d134c0ad..9c6e3e30bb 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 @@ -46,7 +46,6 @@ * @see HqlQueryParser * @see EqlQueryParser */ -@SuppressWarnings("removal") class JpaQueryEnhancer implements QueryEnhancer { private final ParserRuleContext context; @@ -215,7 +214,8 @@ public String getProjection() { */ @Override public DeclaredQuery getQuery() { - throw new UnsupportedOperationException(); + QueryTokenStream tokens = sortFunction.apply(Sort.unsorted(), this.queryInformation, null).visit(context); + return DeclaredQuery.jpqlQuery(QueryRenderer.TokenRenderer.render(tokens)); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancerUnitTests.java new file mode 100644 index 0000000000..43452f3977 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancerUnitTests.java @@ -0,0 +1,57 @@ +/* + * 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 java.util.function.Function; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Unit tests for {@link JpaQueryEnhancer}. + * + * @author Mark Paluch + */ +class JpaQueryEnhancerUnitTests { + + @ParameterizedTest // GH-3997 + @MethodSource("queryEnhancers") + void shouldRemoveCommentsFromJpql(Function> enhancerFunction) { + + QueryEnhancer enhancer = enhancerFunction + .apply("SELECT /* foo */ some_alias FROM /* some other */ table_name some_alias"); + + assertThat(enhancer.getQuery().getQueryString()) + .isEqualToIgnoringCase("SELECT some_alias FROM table_name some_alias"); + + enhancer = enhancerFunction.apply(""" + SELECT /* multi + line + comment + */ some_alias FROM /* some other */ table_name some_alias + """); + + assertThat(enhancer.getQuery().getQueryString()) + .isEqualToIgnoringCase("SELECT some_alias FROM table_name some_alias"); + } + + static Stream>> queryEnhancers() { + return Stream.of(JpaQueryEnhancer::forHql, JpaQueryEnhancer::forEql, JpaQueryEnhancer::forJpql); + } +} From 72dd53ac8834b263f112093cd5e6f677af67f8c4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 4 Sep 2025 12:01:13 +0200 Subject: [PATCH 188/224] Return deleted entity from derived deleteBy method. We now return the deleted entity and check, guard the delete query against batch deletes if the delete yields more than done result. Closes #3995 --- .../jpa/repository/aot/JpaCodeBlocks.java | 44 +++++++++++++++---- .../repository/query/JpaQueryExecution.java | 23 +++++++++- .../jpa/repository/UserRepositoryTests.java | 32 ++++++++++++++ .../jpa/repository/sample/UserRepository.java | 4 ++ 4 files changed, 92 insertions(+), 11 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 45eae1fa92..d14637addb 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 @@ -30,6 +30,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Score; import org.springframework.data.domain.SliceImpl; @@ -46,6 +47,7 @@ import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.data.util.ReflectionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -662,13 +664,14 @@ public CodeBlock build() { : TypeName.get(context.getDomainType()); builder.add("\n"); + Class methodReturnType = context.getMethod().getReturnType(); if (modifying.isPresent()) { if (modifying.getBoolean("flushAutomatically")) { builder.addStatement("this.$L.flush()", context.fieldNameOf(EntityManager.class)); } - Class returnType = context.getMethod().getReturnType(); + Class returnType = methodReturnType; if (returnsModifying(returnType)) { builder.addStatement("int $L = $L.executeUpdate()", context.localVariable("result"), queryVariableName); @@ -697,19 +700,42 @@ public CodeBlock build() { builder.addStatement("$T $L = $L.getResultList()", List.class, context.localVariable("resultList"), queryVariableName); + + boolean returnCount = ClassUtils.isAssignable(Number.class, methodReturnType); + boolean simpleBatch = returnCount || ReflectionUtils.isVoid(methodReturnType); + boolean collectionQuery = queryMethod.isCollectionQuery(); + + if (!simpleBatch && !collectionQuery) { + + builder.beginControlFlow("if ($L.size() > 1)", context.localVariable("resultList")); + builder.addStatement("throw new $1T($2S + $3L.size(), 1, $3L.size())", + IncorrectResultSizeDataAccessException.class, + "Delete query returned more than one element: expected 1, actual ", context.localVariable("resultList")); + builder.endControlFlow(); + + builder.addStatement("$L.forEach($L::remove)", context.localVariable("resultList"), + context.fieldNameOf(EntityManager.class)); + } + builder.addStatement("$L.forEach($L::remove)", context.localVariable("resultList"), context.fieldNameOf(EntityManager.class)); - if (!Collection.class.isAssignableFrom(context.getReturnType().toClass())) { - if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { - builder.addStatement("return $T.valueOf($L.size())", context.getMethod().getReturnType(), + + if (collectionQuery) { + builder.addStatement("return ($T) $L", List.class, context.localVariable("resultList")); + + } else if (returnCount) { + builder.addStatement("return $T.valueOf($L.size())", methodReturnType, context.localVariable("resultList")); } else { - builder.addStatement("return ($T) ($L.isEmpty() ? null : $L.iterator().next())", actualReturnType, - context.localVariable("resultList"), context.localVariable("resultList")); + + if (Optional.class.isAssignableFrom(methodReturnType)) { + builder.addStatement("return ($1T) $1T.ofNullable($2L.isEmpty() ? null : $2L.iterator().next())", + Optional.class, context.localVariable("resultList")); + } else { + builder.addStatement("return ($1T) ($2L.isEmpty() ? null : $2L.iterator().next())", actualReturnType, + context.localVariable("resultList")); + } } - } else { - builder.addStatement("return ($T) $L", List.class, context.localVariable("resultList")); - } } else if (aotQuery != null && aotQuery.isExists()) { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); } else if (aotQuery != null) { 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 b0e7c49a4f..1f3ca294b7 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 @@ -32,6 +32,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Score; @@ -51,6 +52,7 @@ import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -399,16 +401,33 @@ public DeleteExecution(EntityManager em) { } @Override - protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) { + protected @Nullable Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) { Query query = jpaQuery.createQuery(accessor); List resultList = query.getResultList(); + boolean simpleBatch = Number.class.isAssignableFrom(jpaQuery.getQueryMethod().getReturnType()) + || org.springframework.data.util.ReflectionUtils.isVoid(jpaQuery.getQueryMethod().getReturnType()); + boolean collectionQuery = jpaQuery.getQueryMethod().isCollectionQuery(); + + if (!simpleBatch && !collectionQuery) { + + if (resultList.size() > 1) { + throw new IncorrectResultSizeDataAccessException( + "Delete query returned more than one element: expected 1, actual " + resultList.size(), 1, + resultList.size()); + } + } + for (Object o : resultList) { em.remove(o); } - return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size(); + if (simpleBatch) { + return resultList.size(); + } + + return collectionQuery ? resultList : CollectionUtils.firstElement(resultList); } } 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 126c5ce6be..65415e8166 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 @@ -1582,6 +1582,38 @@ void deleteByShouldReturnListOfDeletedElementsWhenRetunTypeIsCollectionLike() { assertThat(result).containsOnly(firstUser); } + @Test // GH-3995 + void deleteOneByShouldReturnDeletedElement() { + + assertThat(repository.deleteOneByLastname(firstUser.getLastname())).isNull(); + + flushTestUsers(); + + User result = repository.deleteOneByLastname(firstUser.getLastname()); + assertThat(result).isEqualTo(firstUser); + } + + @Test // GH-3995 + void deleteOneOptionalByShouldReturnDeletedElement() { + + flushTestUsers(); + + Optional result = repository.deleteOneOptionalByLastname(firstUser.getLastname()); + assertThat(result).contains(firstUser); + } + + @Test // GH-3995 + void deleteOneShouldFailWhenMatchingMultipleResults() { + + firstUser.setLastname("foo"); + secondUser.setLastname("foo"); + firstUser = repository.save(firstUser); + secondUser = repository.save(secondUser); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> repository.deleteOneByLastname(firstUser.getLastname())); + } + @Test // DATAJPA-460 void deleteByShouldRemoveElementsMatchingDerivedQuery() { 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 600bf3f0d1..3e9f4701e0 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 @@ -302,6 +302,10 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 List deleteByLastname(String lastname); + User deleteOneByLastname(String lastname); + + Optional deleteOneOptionalByLastname(String lastname); + /** * Explicitly mapped to a procedure with name "plus1inout" in database. */ From d881e87f708d256882d302064afc234879932d3d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 5 Sep 2025 15:06:31 +0200 Subject: [PATCH 189/224] Upgrade to Eclipselink 5.0.0-B10. Closes #4006 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 16eaf699f7..2606ddd9a4 100755 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ 4.13.2 - 5.0.0-B09 + 5.0.0-B10 5.0.0-SNAPSHOT 7.1.0.Final 7.1.1-SNAPSHOT From de7c5684558a4723ab4ed0caa242aa5c01f1129a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 9 Sep 2025 13:18:44 +0200 Subject: [PATCH 190/224] Follow changes in data-commons AOT support. Closes: #4007 --- .../aot/TestJpaAotRepositoryContext.java | 19 +++++++++++++++++++ ...toryRegistrationAotProcessorUnitTests.java | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) 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 f2bda1d158..ced6e6c389 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 @@ -19,7 +19,10 @@ import jakarta.persistence.MappedSuperclass; import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; import java.util.Set; +import java.util.function.Consumer; import org.jspecify.annotations.Nullable; @@ -27,6 +30,7 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.SpecialUser; import org.springframework.data.jpa.domain.sample.User; @@ -89,6 +93,16 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return null; } + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } + @Override public String getBeanName() { return "dummyRepository"; @@ -129,6 +143,11 @@ public Set> getResolvedTypes() { return Set.of(User.class, SpecialUser.class, Role.class); } + @Override + public Set> getUserDomainTypes() { + return Set.of(); + } + public void setBeanFactory(ConfigurableListableBeanFactory beanFactory) { this.beanFactory = beanFactory; } 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 5f442fcf7b..0989e674a1 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 @@ -23,9 +23,11 @@ import java.lang.annotation.Annotation; import java.net.URL; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -45,6 +47,7 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.data.aot.AotContext; +import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.Repository; @@ -258,6 +261,11 @@ public Set> getResolvedTypes() { return Set.of(); } + @Override + public Set> getUserDomainTypes() { + return Set.of(); + } + @Override public ConfigurableListableBeanFactory getBeanFactory() { return applicationContext != null ? applicationContext.getBeanFactory() : null; @@ -278,6 +286,15 @@ public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { return null; } + @Override + public void typeConfiguration(Class type, Consumer configurationConsumer) { + + } + + @Override + public Collection typeConfigurations() { + return List.of(); + } } } From 6a26a73cdd8915408dbd11e260471ce9e4c9f62b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 11 Sep 2025 18:12:17 +0200 Subject: [PATCH 191/224] Refine JavaPoet usage. See #4007 --- .../jpa/repository/aot/JpaCodeBlocks.java | 148 ++++++++---------- .../aot/JpaRepositoryContributor.java | 2 +- 2 files changed, 69 insertions(+), 81 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 d14637addb..7a2afce85e 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 @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.function.LongSupplier; import org.jspecify.annotations.Nullable; @@ -36,6 +35,8 @@ import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Vector; +import org.springframework.data.javapoet.LordOfTheStrings; +import org.springframework.data.javapoet.TypeNames; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.QueryHints; @@ -45,9 +46,8 @@ import org.springframework.data.jpa.repository.query.ParameterBinding; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; -import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.aot.generate.MethodReturn; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.data.util.ReflectionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -88,7 +88,7 @@ static class QueryBlockBuilder { private final AotQueryMethodGenerationContext context; private final JpaQueryMethod queryMethod; private final String parameterNames; - private String queryVariableName; + private final String queryVariableName; private @Nullable AotQueries queries; private MergedAnnotation queryHints = MergedAnnotation.missing(); private @Nullable AotEntityGraph entityGraph; @@ -111,12 +111,6 @@ private QueryBlockBuilder(AotQueryMethodGenerationContext context, JpaQueryMetho } } - public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { - - this.queryVariableName = context.localVariable(queryVariableName); - return this; - } - public QueryBlockBuilder filter(AotQueries query) { this.queries = query; return this; @@ -160,7 +154,8 @@ public CodeBlock build() { Assert.notNull(queries, "Queries must not be null"); - boolean isProjecting = context.getReturnedType().isProjecting(); + MethodReturn methodReturn = context.getMethodReturn(); + boolean isProjecting = methodReturn.isProjecting(); String dynamicReturnType = null; if (queryMethod.getParameters().hasDynamicProjection()) { @@ -266,7 +261,8 @@ private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicRe dynamicReturnType); } else if (hasSort) { - Object actualReturnType = isProjecting ? context.getActualReturnTypeName() : context.getDomainType(); + Object actualReturnType = isProjecting ? context.getMethodReturn().getActualClassName() + : context.getDomainType(); builder.addStatement("$L = rewriteQuery($L, $L, $T.class)", queryString, context.localVariable("declaredQuery"), sort, actualReturnType); @@ -291,7 +287,6 @@ private CodeBlock applyLimits(boolean exists, @Nullable String pageable) { if (exists) { builder.addStatement("$L.setMaxResults(1)", queryVariableName); - return builder.build(); } @@ -434,7 +429,7 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, @Nullable String pageable, @Nullable Class queryReturnType) { - ReturnedType returnedType = context.getReturnedType(); + MethodReturn methodReturn = context.getMethodReturn(); Builder builder = CodeBlock.builder(); String queryStringNameToUse = queryStringName; @@ -478,16 +473,14 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, return builder.build(); } - if (sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting() - && returnedType.getReturnedType().isInterface()) { + if (sq.hasConstructorExpressionOrDefaultProjection() && !count && methodReturn.isInterfaceProjection()) { builder.addStatement("$T $L = this.$L.createQuery($L)", Query.class, queryVariableName, context.fieldNameOf(EntityManager.class), queryStringNameToUse); } else { String createQueryMethod = query.isNative() ? "createNativeQuery" : "createQuery"; - if (!sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting() - && returnedType.getReturnedType().isInterface()) { + if (!sq.hasConstructorExpressionOrDefaultProjection() && !count && methodReturn.isInterfaceProjection()) { builder.addStatement("$T $L = this.$L.$L($L, $T.class)", Query.class, queryVariableName, context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameToUse, Tuple.class); } else { @@ -501,8 +494,7 @@ private CodeBlock doCreateQuery(boolean count, String queryVariableName, if (query instanceof NamedAotQuery nq) { - if (!count && !nq.hasConstructorExpressionOrDefaultProjection() && returnedType.isProjecting() - && returnedType.getReturnedType().isInterface()) { + if (!count && !nq.hasConstructorExpressionOrDefaultProjection() && methodReturn.isInterfaceProjection()) { queryReturnType = Tuple.class; } @@ -571,9 +563,9 @@ private CodeBlock applyEntityGraph(AotEntityGraph entityGraph, String queryVaria } else { builder.addStatement("$T<$T> $L = $L.createEntityGraph($T.class)", - jakarta.persistence.EntityGraph.class, context.getActualReturnType().getType(), + jakarta.persistence.EntityGraph.class, context.getDomainType(), context.localVariable("entityGraph"), - context.fieldNameOf(EntityManager.class), context.getActualReturnType().getType()); + context.fieldNameOf(EntityManager.class), context.getDomainType()); for (String attributePath : entityGraph.attributePaths()) { @@ -618,8 +610,8 @@ static class QueryExecutionBlockBuilder { private final AotQueryMethodGenerationContext context; private final JpaQueryMethod queryMethod; + private final String queryVariableName; private @Nullable AotQuery aotQuery; - private String queryVariableName; private @Nullable String pageable; private MergedAnnotation modifying = MergedAnnotation.missing(); @@ -631,12 +623,6 @@ private QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, JpaQ this.pageable = context.getPageableParameterName() != null ? context.localVariable("pageable") : null; } - public QueryExecutionBlockBuilder referencing(String queryVariableName) { - - this.queryVariableName = context.localVariable(queryVariableName); - return this; - } - public QueryExecutionBlockBuilder query(AotQuery aotQuery) { this.aotQuery = aotQuery; @@ -658,20 +644,21 @@ public QueryExecutionBlockBuilder modifying(MergedAnnotation modifyin public CodeBlock build() { Builder builder = CodeBlock.builder(); - - boolean isProjecting = !ObjectUtils.nullSafeEquals(context.getDomainType(), context.getActualReturnTypeName()); - TypeName actualReturnType = isProjecting ? context.getActualReturnTypeName() + MethodReturn methodReturn = context.getMethodReturn(); + boolean isProjecting = methodReturn.isProjecting() + || !ObjectUtils.nullSafeEquals(context.getDomainType(), methodReturn.getActualReturnClass()) + || StringUtils.hasText(context.getDynamicProjectionParameterName()); + TypeName typeToRead = isProjecting ? methodReturn.getActualTypeName() : TypeName.get(context.getDomainType()); builder.add("\n"); - Class methodReturnType = context.getMethod().getReturnType(); if (modifying.isPresent()) { if (modifying.getBoolean("flushAutomatically")) { builder.addStatement("this.$L.flush()", context.fieldNameOf(EntityManager.class)); } - Class returnType = methodReturnType; + Class returnType = methodReturn.toClass(); if (returnsModifying(returnType)) { builder.addStatement("int $L = $L.executeUpdate()", context.localVariable("result"), queryVariableName); @@ -694,15 +681,13 @@ public CodeBlock build() { return builder.build(); } - TypeName queryResultType = TypeName.get(context.getActualReturnType().toClass()); - if (aotQuery != null && aotQuery.isDelete()) { builder.addStatement("$T $L = $L.getResultList()", List.class, context.localVariable("resultList"), queryVariableName); - boolean returnCount = ClassUtils.isAssignable(Number.class, methodReturnType); - boolean simpleBatch = returnCount || ReflectionUtils.isVoid(methodReturnType); + boolean returnCount = ClassUtils.isAssignable(Number.class, methodReturn.toClass()); + boolean simpleBatch = returnCount || methodReturn.isVoid(); boolean collectionQuery = queryMethod.isCollectionQuery(); if (!simpleBatch && !collectionQuery) { @@ -712,9 +697,6 @@ public CodeBlock build() { IncorrectResultSizeDataAccessException.class, "Delete query returned more than one element: expected 1, actual ", context.localVariable("resultList")); builder.endControlFlow(); - - builder.addStatement("$L.forEach($L::remove)", context.localVariable("resultList"), - context.fieldNameOf(EntityManager.class)); } builder.addStatement("$L.forEach($L::remove)", context.localVariable("resultList"), @@ -724,40 +706,52 @@ public CodeBlock build() { builder.addStatement("return ($T) $L", List.class, context.localVariable("resultList")); } else if (returnCount) { - builder.addStatement("return $T.valueOf($L.size())", methodReturnType, + builder.addStatement("return $T.valueOf($L.size())", methodReturn.getActualClassName(), context.localVariable("resultList")); } else { - if (Optional.class.isAssignableFrom(methodReturnType)) { - builder.addStatement("return ($1T) $1T.ofNullable($2L.isEmpty() ? null : $2L.iterator().next())", - Optional.class, context.localVariable("resultList")); - } else { - builder.addStatement("return ($1T) ($2L.isEmpty() ? null : $2L.iterator().next())", actualReturnType, - context.localVariable("resultList")); - } + builder.addStatement(LordOfTheStrings.returning(methodReturn.toClass()) + .optional("($1T) ($2L.isEmpty() ? null : $2L.iterator().next())", typeToRead, + context.localVariable("resultList")) // + .build()); } } else if (aotQuery != null && aotQuery.isExists()) { builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); } else if (aotQuery != null) { - if (context.getReturnedType().isProjecting()) { + if (isProjecting) { + + TypeName returnType = TypeNames.typeNameOrWrapper(methodReturn.getActualType()); + CodeBlock convertTo; + if (StringUtils.hasText(context.getDynamicProjectionParameterName())) { + convertTo = CodeBlock.of("$L", context.getDynamicProjectionParameterName()); + } else { + + if (methodReturn.isArray() && methodReturn.getActualType().toClass().equals(byte.class)) { + returnType = TypeName.get(byte[].class); + convertTo = CodeBlock.of("$T.class", returnType); + } else { + convertTo = CodeBlock.of("$T.class", TypeNames.classNameOrWrapper(methodReturn.getActualType())); + } + } if (queryMethod.isCollectionQuery()) { - builder.addStatement("return ($T) convertMany($L.getResultList(), $L, $T.class)", - context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType); + builder.addStatement("return ($T) convertMany($L.getResultList(), $L, $L)", methodReturn.getTypeName(), + queryVariableName, aotQuery.isNative(), convertTo); } else if (queryMethod.isStreamQuery()) { - builder.addStatement("return ($T) convertMany($L.getResultStream(), $L, $T.class)", - context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType); + builder.addStatement("return ($T) convertMany($L.getResultStream(), $L, $L)", methodReturn.getTypeName(), + queryVariableName, aotQuery.isNative(), convertTo); } else if (queryMethod.isPageQuery()) { builder.addStatement( - "return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, $L)", - PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, aotQuery.isNative(), - queryResultType, pageable, context.localVariable("countAll")); + "return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $L), $L, $L)", + PageableExecutionUtils.class, List.class, TypeNames.typeNameOrWrapper(methodReturn.getActualType()), + queryVariableName, aotQuery.isNative(), convertTo, pageable, context.localVariable("countAll")); } else if (queryMethod.isSliceQuery()) { - builder.addStatement("$T<$T> $L = ($T<$T>) convertMany($L.getResultList(), $L, $T.class)", List.class, - actualReturnType, context.localVariable("resultList"), List.class, actualReturnType, queryVariableName, + builder.addStatement("$T<$T> $L = ($T<$T>) convertMany($L.getResultList(), $L, $L)", List.class, + TypeNames.typeNameOrWrapper(methodReturn.getActualType()), context.localVariable("resultList"), + List.class, typeToRead, queryVariableName, aotQuery.isNative(), - queryResultType); + convertTo); builder.addStatement("boolean $L = $L.isPaged() && $L.size() > $L.getPageSize()", context.localVariable("hasNext"), pageable, context.localVariable("resultList"), pageable); builder.addStatement( @@ -766,27 +760,24 @@ public CodeBlock build() { pageable, context.localVariable("resultList"), pageable, context.localVariable("hasNext")); } 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); - } + builder.addStatement(LordOfTheStrings.returning(methodReturn.toClass()) + .optional("($T) convertOne($L.getSingleResultOrNull(), $L, $L)", returnType, queryVariableName, + aotQuery.isNative(), convertTo) // + .build()); } } else { if (queryMethod.isCollectionQuery()) { - builder.addStatement("return ($T) $L.getResultList()", context.getReturnTypeName(), queryVariableName); + builder.addStatement("return ($T) $L.getResultList()", methodReturn.getTypeName(), queryVariableName); } else if (queryMethod.isStreamQuery()) { - builder.addStatement("return ($T) $L.getResultStream()", context.getReturnTypeName(), queryVariableName); + builder.addStatement("return ($T) $L.getResultStream()", methodReturn.getTypeName(), queryVariableName); } else if (queryMethod.isPageQuery()) { builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, $L)", - PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, + PageableExecutionUtils.class, List.class, typeToRead, queryVariableName, pageable, context.localVariable("countAll")); } else if (queryMethod.isSliceQuery()) { - builder.addStatement("$T<$T> $L = $L.getResultList()", List.class, actualReturnType, + builder.addStatement("$T<$T> $L = $L.getResultList()", List.class, typeToRead, context.localVariable("resultList"), queryVariableName); builder.addStatement("boolean $L = $L.isPaged() && $L.size() > $L.getPageSize()", context.localVariable("hasNext"), pageable, context.localVariable("resultList"), pageable); @@ -796,15 +787,11 @@ public CodeBlock build() { pageable, context.localVariable("resultList"), pageable, context.localVariable("hasNext")); } 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(), - context.getReturnType().toClass()); - } + builder.addStatement(LordOfTheStrings.returning(methodReturn.toClass()) + .optional("($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)", + TypeNames.typeNameOrWrapper(methodReturn.getActualType()), queryVariableName, aotQuery.isNative(), + TypeNames.classNameOrWrapper(methodReturn.getActualType())) // + .build()); } } } @@ -820,4 +807,5 @@ 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 91f9575c53..0bd50476f9 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 @@ -202,7 +202,7 @@ private Optional> getQueryEnhancerSelectorClass() { queryMethod); // no KeysetScrolling for now. - if (parameters.hasScrollPositionParameter()) { + if (parameters.hasScrollPositionParameter() || queryMethod.isScrollQuery()) { return MethodContributor.forQueryMethod(queryMethod) .metadataOnly(aotQueries.toMetadata(queryMethod.isPageQuery())); } From b379b832077551a4a45cb5dbf943e1693820b1ad Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 12:42:57 +0200 Subject: [PATCH 192/224] Prepare 4.0 M6 (2025.1.0). See #3977 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 2606ddd9a4..fc6b072cda 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-M6 @@ -39,7 +39,7 @@ 9.2.0 42.7.7 23.8.0.25.04 - 4.0.0-SNAPSHOT + 4.0.0-M6 0.10.3 org.hibernate @@ -193,20 +193,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From a38893577b76f4c82091b467fe8637e131d5d4f5 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 12:43:44 +0200 Subject: [PATCH 193/224] Release version 4.0 M6 (2025.1.0). See #3977 --- 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 fc6b072cda..7bc87c65b8 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-M6 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 0bdf2c8e7e..9973e465f5 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-M6 org.springframework.data spring-data-jpa-parent - 4.0.0-SNAPSHOT + 4.0.0-M6 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index af5244a230..0e0e21b5ad 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-M6 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index cbec8a2645..98e8139762 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-M6 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-M6 ../pom.xml From 243ffd70e0fad3158c257b6be5aeab42f6830dfb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 12:47:14 +0200 Subject: [PATCH 194/224] Prepare next development iteration. See #3977 --- 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 7bc87c65b8..fc6b072cda 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 4.0.0-M6 + 4.0.0-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 9973e465f5..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-M6 + 4.0.0-SNAPSHOT org.springframework.data spring-data-jpa-parent - 4.0.0-M6 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 0e0e21b5ad..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-M6 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 98e8139762..cbec8a2645 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-M6 + 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-M6 + 4.0.0-SNAPSHOT ../pom.xml From c3187fb57f9acbcbac3b136ac29e3b2dad366ce3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 12:47:16 +0200 Subject: [PATCH 195/224] After release cleanups. See #3977 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index fc6b072cda..2606ddd9a4 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M6 + 4.0.0-SNAPSHOT @@ -39,7 +39,7 @@ 9.2.0 42.7.7 23.8.0.25.04 - 4.0.0-M6 + 4.0.0-SNAPSHOT 0.10.3 org.hibernate @@ -193,8 +193,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From bc234f0406f29b96d4fb748c53e6c613671816d4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Sep 2025 15:26:18 +0200 Subject: [PATCH 196/224] Fix nested EQL and JPQL aggregation function argument grammar. We now accept a wider range of function arguments instead of limiting to property paths. Closes #4013 --- .../springframework/data/jpa/repository/query/Eql.g4 | 9 +++------ .../springframework/data/jpa/repository/query/Jpql.g4 | 9 +++------ .../data/jpa/repository/query/EqlQueryRenderer.java | 10 ++-------- .../data/jpa/repository/query/JpqlQueryRenderer.java | 11 +++-------- .../jpa/repository/query/EqlQueryRendererTests.java | 8 ++++++++ .../jpa/repository/query/HqlQueryRendererTests.java | 6 ++++++ .../jpa/repository/query/JpqlQueryRendererTests.java | 8 ++++++++ 7 files changed, 33 insertions(+), 28 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 9b9f634190..18f7c6fb8c 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 @@ -219,8 +219,8 @@ constructor_item ; aggregate_expression - : (AVG | MAX | MIN | SUM) '(' (DISTINCT)? state_valued_path_expression ')' - | COUNT '(' (DISTINCT)? (identification_variable | state_valued_path_expression | single_valued_object_path_expression) ')' + : (AVG | MAX | MIN | SUM) '(' (DISTINCT)? simple_select_expression ')' + | COUNT '(' (DISTINCT)? simple_select_expression ')' | function_invocation ; @@ -593,10 +593,7 @@ datetime_part ; function_arg - : literal - | state_valued_path_expression - | input_parameter - | scalar_expression + : simple_select_expression ; case_expression 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 6e8f374777..5c8730f523 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 @@ -220,8 +220,8 @@ constructor_item ; aggregate_expression - : (AVG | MAX | MIN | SUM) '(' (DISTINCT)? state_valued_path_expression ')' - | COUNT '(' (DISTINCT)? (identification_variable | state_valued_path_expression | single_valued_object_path_expression) ')' + : (AVG | MAX | MIN | SUM) '(' (DISTINCT)? simple_select_expression ')' + | COUNT '(' (DISTINCT)? simple_select_expression ')' | function_invocation ; @@ -593,10 +593,7 @@ datetime_part ; function_arg - : literal - | state_valued_path_expression - | input_parameter - | scalar_expression + : simple_select_expression ; case_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 707ccd6995..230da53fed 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 @@ -494,7 +494,7 @@ public QueryTokenStream visitAggregate_expression(EqlParser.Aggregate_expression builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.appendInline(visit(ctx.state_valued_path_expression())); + builder.appendInline(visit(ctx.simple_select_expression())); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.COUNT() != null) { @@ -503,13 +503,7 @@ public QueryTokenStream visitAggregate_expression(EqlParser.Aggregate_expression if (ctx.DISTINCT() != null) { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - if (ctx.identification_variable() != null) { - builder.appendInline(visit(ctx.identification_variable())); - } else if (ctx.state_valued_path_expression() != null) { - builder.appendInline(visit(ctx.state_valued_path_expression())); - } else if (ctx.single_valued_object_path_expression() != null) { - builder.appendInline(visit(ctx.single_valued_object_path_expression())); - } + builder.appendInline(visit(ctx.simple_select_expression())); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); 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 3fe487516c..3e3c39fa19 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 @@ -495,7 +495,7 @@ public QueryTokenStream visitAggregate_expression(JpqlParser.Aggregate_expressio builder.append(QueryTokens.expression(ctx.DISTINCT())); } - builder.appendInline(visit(ctx.state_valued_path_expression())); + builder.appendInline(visit(ctx.simple_select_expression())); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.COUNT() != null) { @@ -504,13 +504,8 @@ public QueryTokenStream visitAggregate_expression(JpqlParser.Aggregate_expressio if (ctx.DISTINCT() != null) { builder.append(QueryTokens.expression(ctx.DISTINCT())); } - if (ctx.identification_variable() != null) { - builder.appendInline(visit(ctx.identification_variable())); - } else if (ctx.state_valued_path_expression() != null) { - builder.appendInline(visit(ctx.state_valued_path_expression())); - } else if (ctx.single_valued_object_path_expression() != null) { - builder.appendInline(visit(ctx.single_valued_object_path_expression())); - } + + builder.appendInline(visit(ctx.simple_select_expression())); builder.append(TOKEN_CLOSE_PAREN); } else if (ctx.function_invocation() != null) { builder.append(visit(ctx.function_invocation())); 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 a98a01adc7..7366ff8d90 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 @@ -1254,6 +1254,14 @@ void findOrdersThatHaveProductNamedByAParameter() { """); } + @Test // GH-4013 + void minMaxFunctionsShouldWork() { + assertQuery("SELECT MAX(e.age), e.address.city FROM Employee e"); + assertQuery("SELECT MAX(1), e.address.city FROM Employee e"); + assertQuery("SELECT MAX(MIN(MOD(e.salary, 10))), e.address.city FROM Employee e"); + assertQuery("SELECT MIN(MOD(e.salary, 10)), e.address.city FROM Employee e"); + } + @Test // GH-2982 void floorShouldBeValidEntityName() { 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 1ecbfb1e56..f130b6cea0 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 @@ -2080,6 +2080,12 @@ void lnFunctionShouldWork() { assertQuery("select ln(7.5) from Element a"); } + @Test // GH-4013 + void minMaxFunctionsShouldWork() { + assertQuery("SELECT MAX(MIN(MOD(e.salary, 10))), e.address.city FROM Employee e"); + assertQuery("SELECT MIN(MOD(e.salary, 10)), e.address.city FROM Employee e"); + } + @Test // GH-2981 void cteWithClauseShouldWork() { 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 0c35ed80bf..707cbaf536 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 @@ -1260,6 +1260,14 @@ void findOrdersThatHaveProductNamedByAParameter() { """); } + @Test // GH-4013 + void minMaxFunctionsShouldWork() { + assertQuery("SELECT MAX(e.age), e.address.city FROM Employee e"); + assertQuery("SELECT MAX(1), e.address.city FROM Employee e"); + assertQuery("SELECT MAX(MIN(MOD(e.salary, 10))), e.address.city FROM Employee e"); + assertQuery("SELECT MIN(MOD(e.salary, 10)), e.address.city FROM Employee e"); + } + @Test // GH-2982 void floorShouldBeValidEntityName() { From 18db5faf26cd5ea273fcbe33c8b4fd0742b7eb73 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Sep 2025 15:50:33 +0200 Subject: [PATCH 197/224] Refine HQL rendering of CTE with SEARCH clause. Add tests for CYCLE, ensure no space between search order items. Closes #4012 --- .../repository/query/HqlQueryRenderer.java | 5 ++++ .../query/HqlQueryRendererTests.java | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) 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 58b5a3cb3a..15522f0263 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 @@ -131,6 +131,11 @@ public QueryTokenStream visitCteAttributes(HqlParser.CteAttributesContext ctx) { return QueryTokenStream.concat(ctx.identifier(), this::visit, TOKEN_COMMA); } + @Override + public QueryTokenStream visitSearchSpecifications(HqlParser.SearchSpecificationsContext ctx) { + return QueryTokenStream.concat(ctx.searchSpecification(), this::visit, TOKEN_COMMA); + } + @Override public QueryTokenStream visitOrderedQuery(HqlParser.OrderedQueryContext ctx) { 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 f130b6cea0..022983f796 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 @@ -2097,6 +2097,31 @@ WITH maxId AS (select max(sr.snapshot.id) snapshotId from SnapshotReference sr """); } + @Test // GH-4012 + void cteWithSearch() { + + assertQuery(""" + WITH Tree AS (SELECT o.uuid AS test_uuid FROM DemoEntity o) + SEARCH BREADTH FIRST BY foo ASC NULLS FIRST, bar DESC NULLS LAST SET baz + SELECT test_uuid FROM Tree + """); + } + + @Test // GH-4012 + void cteWithCycle() { + + assertQuery(""" + WITH Tree AS (SELECT o.uuid AS test_uuid FROM DemoEntity o) CYCLE test_uuid SET circular TO true DEFAULT false + SELECT test_uuid FROM Tree + """); + + assertQuery( + """ + WITH Tree AS (SELECT o.uuid AS test_uuid FROM DemoEntity o) CYCLE test_uuid SET circular TO true DEFAULT false USING bar + SELECT test_uuid FROM Tree + """); + } + @Test // GH-2982 void floorShouldBeValidEntityName() { From 8c3a0679bd340ad316de94dddd1671f24592345f Mon Sep 17 00:00:00 2001 From: ChaedongIm Date: Thu, 4 Sep 2025 04:31:02 +0900 Subject: [PATCH 198/224] Allow customization of `@RevisionTimestamp` property name. We now detect the property name annotated with RevisionTimestamp to determine the property name from the model instead of assuming a hard-coded timestamp property. Signed-off-by: ChaedongIm See #2850 Original pull request: #4003 --- .../DefaultRevisionEntityInformation.java | 5 ++ .../support/EnversRevisionRepositoryImpl.java | 16 +++++- .../ReflectionRevisionEntityInformation.java | 17 +++++- ...EnversRevisionRepositoryImplUnitTests.java | 1 + ...mRevisionEntityWithDifferentTimestamp.java | 56 +++++++++++++++++++ 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java index d8b96b28d9..493aab3e8c 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java @@ -22,6 +22,7 @@ * {@link RevisionEntityInformation} for {@link DefaultRevisionEntity}. * * @author Oliver Gierke + * @author Chaedong Im */ class DefaultRevisionEntityInformation implements RevisionEntityInformation { @@ -36,4 +37,8 @@ public boolean isDefaultRevisionEntity() { public Class getRevisionEntityClass() { return DefaultRevisionEntity.class; } + + public String getRevisionTimestampFieldName() { + return "timestamp"; + } } diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index 4515a74ed9..2d7bd98344 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java @@ -66,12 +66,14 @@ * @author Greg Turnquist * @author Aref Behboodi * @author Ngoc Nhan + * @author Chaedong Im */ @Transactional(readOnly = true) public class EnversRevisionRepositoryImpl> implements RevisionRepository { private final EntityInformation entityInformation; + private final RevisionEntityInformation revisionEntityInformation; private final EntityManager entityManager; /** @@ -90,14 +92,16 @@ public EnversRevisionRepositoryImpl(JpaEntityInformation entityInformation Assert.notNull(revisionEntityInformation, "RevisionEntityInformation must not be null!"); this.entityInformation = entityInformation; + this.revisionEntityInformation = revisionEntityInformation; this.entityManager = entityManager; } @SuppressWarnings("unchecked") public Optional> findLastChangeRevision(ID id) { + String timestampFieldName = getRevisionTimestampFieldName(); List singleResult = createBaseQuery(id) // - .addOrder(AuditEntity.revisionProperty("timestamp").desc()) // + .addOrder(AuditEntity.revisionProperty(timestampFieldName).desc()) // .addOrder(AuditEntity.revisionNumber().desc()) // .setMaxResults(1) // .getResultList(); @@ -213,6 +217,16 @@ private Revision createRevision(QueryResult queryResult) { return Revision.of((RevisionMetadata) queryResult.createRevisionMetadata(), queryResult.entity); } + private String getRevisionTimestampFieldName() { + if (revisionEntityInformation instanceof ReflectionRevisionEntityInformation reflection) { + return reflection.getRevisionTimestampFieldName(); + } else if (revisionEntityInformation instanceof DefaultRevisionEntityInformation defaultInfo) { + return defaultInfo.getRevisionTimestampFieldName(); + } else { + return "timestamp"; + } + } + @SuppressWarnings("unchecked") static class QueryResult { diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java index 631dbca9f8..193cf5d513 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java @@ -16,6 +16,7 @@ package org.springframework.data.envers.repository.support; import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; import org.springframework.data.repository.history.support.RevisionEntityInformation; import org.springframework.data.util.AnnotationDetectionFieldCallback; @@ -27,11 +28,13 @@ * find out about the revision number type. * * @author Oliver Gierke + * @author Chaedong Im */ public class ReflectionRevisionEntityInformation implements RevisionEntityInformation { private final Class revisionEntityClass; private final Class revisionNumberType; + private final String revisionTimestampFieldName; /** * Creates a new {@link ReflectionRevisionEntityInformation} inspecting the given revision entity class. @@ -42,10 +45,14 @@ public ReflectionRevisionEntityInformation(Class revisionEntityClass) { Assert.notNull(revisionEntityClass, "Revision entity type must not be null"); - AnnotationDetectionFieldCallback fieldCallback = new AnnotationDetectionFieldCallback(RevisionNumber.class); - ReflectionUtils.doWithFields(revisionEntityClass, fieldCallback); + AnnotationDetectionFieldCallback revisionNumberFieldCallback = new AnnotationDetectionFieldCallback(RevisionNumber.class); + ReflectionUtils.doWithFields(revisionEntityClass, revisionNumberFieldCallback); - this.revisionNumberType = fieldCallback.getRequiredType(); + AnnotationDetectionFieldCallback revisionTimestampFieldCallback = new AnnotationDetectionFieldCallback(RevisionTimestamp.class); + ReflectionUtils.doWithFields(revisionEntityClass, revisionTimestampFieldCallback); + + this.revisionNumberType = revisionNumberFieldCallback.getRequiredType(); + this.revisionTimestampFieldName = revisionTimestampFieldCallback.getRequiredField().getName(); this.revisionEntityClass = revisionEntityClass; } @@ -61,4 +68,8 @@ public Class getRevisionEntityClass() { public Class getRevisionNumberType() { return this.revisionNumberType; } + + public String getRevisionTimestampFieldName() { + return this.revisionTimestampFieldName; + } } diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImplUnitTests.java b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImplUnitTests.java index 625e099d5a..78c229910b 100644 --- a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImplUnitTests.java +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImplUnitTests.java @@ -21,6 +21,7 @@ import org.hibernate.envers.DefaultRevisionEntity; import org.hibernate.envers.RevisionType; import org.junit.jupiter.api.Test; + import org.springframework.data.history.AnnotationRevisionMetadata; import org.springframework.data.history.RevisionMetadata; diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java new file mode 100644 index 0000000000..c6825707a9 --- /dev/null +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java @@ -0,0 +1,56 @@ +/* + * 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.envers.sample; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; + +/** + * Custom revision entity with a non-standard timestamp field name to test dynamic timestamp property detection. + * + * @author Chaedong Im + */ +@Entity +@RevisionEntity +public class CustomRevisionEntityWithDifferentTimestamp { + + @Id @GeneratedValue @RevisionNumber + private int revisionId; + + @RevisionTimestamp + private long myCustomTimestamp; // Non-standard field name + + public int getRevisionId() { + return revisionId; + } + + public void setRevisionId(int revisionId) { + this.revisionId = revisionId; + } + + public long getMyCustomTimestamp() { + return myCustomTimestamp; + } + + public void setMyCustomTimestamp(long myCustomTimestamp) { + this.myCustomTimestamp = myCustomTimestamp; + } +} From aa900658b33879ba03a6a1b7d676f01a1e8a52ee Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Sep 2025 16:37:39 +0200 Subject: [PATCH 199/224] Polishing. Introduce EnversRevisionEntityInformation to reflect envers-specific revision information. Refactor DefaultRevisionEntityInformation to enum to keep a singleton around. Refine tests. See #2850 Original pull request: #4003 --- .../DefaultRevisionEntityInformation.java | 10 ++- .../EnversRevisionEntityInformation.java | 35 ++++++++++ .../EnversRevisionRepositoryFactoryBean.java | 4 +- .../support/EnversRevisionRepositoryImpl.java | 8 +-- .../ReflectionRevisionEntityInformation.java | 9 ++- ...ultRevisionEntityInformationUnitTests.java | 38 +++++++++++ ...ionRevisionEntityInformationUnitTests.java | 64 +++++++++++++++++++ ...mRevisionEntityWithDifferentTimestamp.java | 56 ---------------- 8 files changed, 156 insertions(+), 68 deletions(-) create mode 100644 spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionEntityInformation.java create mode 100644 spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformationUnitTests.java create mode 100644 spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformationUnitTests.java delete mode 100644 spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java index 493aab3e8c..565b84dc45 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformation.java @@ -24,21 +24,27 @@ * @author Oliver Gierke * @author Chaedong Im */ -class DefaultRevisionEntityInformation implements RevisionEntityInformation { +enum DefaultRevisionEntityInformation implements EnversRevisionEntityInformation { + INSTANCE; + + @Override public Class getRevisionNumberType() { return Integer.class; } + @Override public boolean isDefaultRevisionEntity() { return true; } + @Override public Class getRevisionEntityClass() { return DefaultRevisionEntity.class; } - public String getRevisionTimestampFieldName() { + @Override + public String getRevisionTimestampPropertyName() { return "timestamp"; } } diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionEntityInformation.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionEntityInformation.java new file mode 100644 index 0000000000..a7149f3205 --- /dev/null +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionEntityInformation.java @@ -0,0 +1,35 @@ +/* + * 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.envers.repository.support; + +import org.springframework.data.repository.history.support.RevisionEntityInformation; + +/** + * Envers-specific extension to {@link RevisionEntityInformation}. + * + * @author Mark Paluch + * @since 4.0 + */ +public interface EnversRevisionEntityInformation extends RevisionEntityInformation { + + /** + * Return the name of the timestamp property (annotated with {@link org.hibernate.envers.RevisionTimestamp}). + * + * @return the name of the timestamp property, + */ + String getRevisionTimestampPropertyName(); + +} 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 b152bef044..decbcf3f3a 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 @@ -20,8 +20,8 @@ import java.util.Optional; 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; @@ -90,7 +90,7 @@ public RevisionRepositoryFactory(EntityManager entityManager, @Nullable Class this.revisionEntityInformation = Optional.ofNullable(revisionEntityClass) // .filter(it -> !it.equals(DefaultRevisionEntity.class))// . map(ReflectionRevisionEntityInformation::new) // - .orElseGet(DefaultRevisionEntityInformation::new); + .orElse(DefaultRevisionEntityInformation.INSTANCE); } @Override diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index 2d7bd98344..7ead1e1be4 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java @@ -218,12 +218,10 @@ private Revision createRevision(QueryResult queryResult) { } private String getRevisionTimestampFieldName() { - if (revisionEntityInformation instanceof ReflectionRevisionEntityInformation reflection) { - return reflection.getRevisionTimestampFieldName(); - } else if (revisionEntityInformation instanceof DefaultRevisionEntityInformation defaultInfo) { - return defaultInfo.getRevisionTimestampFieldName(); + if (revisionEntityInformation instanceof EnversRevisionEntityInformation reflection) { + return reflection.getRevisionTimestampPropertyName(); } else { - return "timestamp"; + return DefaultRevisionEntityInformation.INSTANCE.getRevisionTimestampPropertyName(); } } diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java index 193cf5d513..7022e33b28 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformation.java @@ -30,7 +30,7 @@ * @author Oliver Gierke * @author Chaedong Im */ -public class ReflectionRevisionEntityInformation implements RevisionEntityInformation { +public class ReflectionRevisionEntityInformation implements EnversRevisionEntityInformation { private final Class revisionEntityClass; private final Class revisionNumberType; @@ -54,22 +54,25 @@ public ReflectionRevisionEntityInformation(Class revisionEntityClass) { this.revisionNumberType = revisionNumberFieldCallback.getRequiredType(); this.revisionTimestampFieldName = revisionTimestampFieldCallback.getRequiredField().getName(); this.revisionEntityClass = revisionEntityClass; - } + @Override public boolean isDefaultRevisionEntity() { return false; } + @Override public Class getRevisionEntityClass() { return this.revisionEntityClass; } + @Override public Class getRevisionNumberType() { return this.revisionNumberType; } - public String getRevisionTimestampFieldName() { + @Override + public String getRevisionTimestampPropertyName() { return this.revisionTimestampFieldName; } } diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformationUnitTests.java b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformationUnitTests.java new file mode 100644 index 0000000000..5d6d49e075 --- /dev/null +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/DefaultRevisionEntityInformationUnitTests.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.envers.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link DefaultRevisionEntityInformation}. + * + * @author Mark Paluch + * @author Chaedong Im + */ +class DefaultRevisionEntityInformationUnitTests { + + @Test // GH-2850 + void defaultRevisionEntityInformationReturnsStandardTimestampFieldName() { + + DefaultRevisionEntityInformation revisionInfo = DefaultRevisionEntityInformation.INSTANCE; + + assertThat(revisionInfo.getRevisionTimestampPropertyName()).isEqualTo("timestamp"); + } + +} diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformationUnitTests.java b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformationUnitTests.java new file mode 100644 index 0000000000..94d173742a --- /dev/null +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/ReflectionRevisionEntityInformationUnitTests.java @@ -0,0 +1,64 @@ +/* + * 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.envers.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; +import org.junit.jupiter.api.Test; + +import org.springframework.data.envers.sample.CustomRevisionEntity; + +/** + * Unit tests for {@link ReflectionRevisionEntityInformation}. + * + * @author Mark Paluch + * @author Chaedong Im + */ +class ReflectionRevisionEntityInformationUnitTests { + + @Test // GH-2850 + void reflectionRevisionEntityInformationDetectsStandardTimestampField() { + + ReflectionRevisionEntityInformation revisionInfo = new ReflectionRevisionEntityInformation( + CustomRevisionEntity.class); + + assertThat(revisionInfo.getRevisionTimestampPropertyName()).isEqualTo("timestamp"); + } + + @Test // GH-2850 + void reflectionRevisionEntityInformationDetectsCustomTimestampField() { + + ReflectionRevisionEntityInformation revisionInfo = new ReflectionRevisionEntityInformation( + WithCustomTimestampPropertyName.class); + + assertThat(revisionInfo.getRevisionTimestampPropertyName()).isEqualTo("myCustomTimestamp"); + } + + /** + * Custom revision entity with a non-standard timestamp field name to test dynamic timestamp property detection. + * + * @author Chaedong Im + */ + private static class WithCustomTimestampPropertyName { + + @RevisionNumber private int revisionId; + + @RevisionTimestamp private long myCustomTimestamp; // Non-standard field name + } + +} diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java deleted file mode 100644 index c6825707a9..0000000000 --- a/spring-data-envers/src/test/java/org/springframework/data/envers/sample/CustomRevisionEntityWithDifferentTimestamp.java +++ /dev/null @@ -1,56 +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.envers.sample; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; - -import org.hibernate.envers.RevisionEntity; -import org.hibernate.envers.RevisionNumber; -import org.hibernate.envers.RevisionTimestamp; - -/** - * Custom revision entity with a non-standard timestamp field name to test dynamic timestamp property detection. - * - * @author Chaedong Im - */ -@Entity -@RevisionEntity -public class CustomRevisionEntityWithDifferentTimestamp { - - @Id @GeneratedValue @RevisionNumber - private int revisionId; - - @RevisionTimestamp - private long myCustomTimestamp; // Non-standard field name - - public int getRevisionId() { - return revisionId; - } - - public void setRevisionId(int revisionId) { - this.revisionId = revisionId; - } - - public long getMyCustomTimestamp() { - return myCustomTimestamp; - } - - public void setMyCustomTimestamp(long myCustomTimestamp) { - this.myCustomTimestamp = myCustomTimestamp; - } -} From 9f403908a46cb6167032f7ff8e4982d6f687b833 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 18 Sep 2025 08:22:53 +0200 Subject: [PATCH 200/224] Fix method return for delete execution returning primitive numbers. We now properly check for assignability of numeric values considering primitive types. Closes #4015 --- .../data/jpa/repository/aot/JpaCodeBlocks.java | 3 ++- .../data/jpa/repository/query/JpaQueryExecution.java | 5 +++-- .../data/jpa/repository/UserRepositoryTests.java | 11 ++++++++++- .../data/jpa/repository/sample/UserRepository.java | 4 ++++ 4 files changed, 19 insertions(+), 4 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 7a2afce85e..08cca9685c 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 @@ -706,7 +706,8 @@ public CodeBlock build() { builder.addStatement("return ($T) $L", List.class, context.localVariable("resultList")); } else if (returnCount) { - builder.addStatement("return $T.valueOf($L.size())", methodReturn.getActualClassName(), + builder.addStatement("return $T.valueOf($L.size())", + ClassUtils.resolvePrimitiveIfNecessary(methodReturn.getActualReturnClass()), context.localVariable("resultList")); } else { 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 1f3ca294b7..04f6186056 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 @@ -405,9 +405,10 @@ public DeleteExecution(EntityManager em) { Query query = jpaQuery.createQuery(accessor); List resultList = query.getResultList(); + Class returnType = jpaQuery.getQueryMethod().getReturnType(); - boolean simpleBatch = Number.class.isAssignableFrom(jpaQuery.getQueryMethod().getReturnType()) - || org.springframework.data.util.ReflectionUtils.isVoid(jpaQuery.getQueryMethod().getReturnType()); + boolean simpleBatch = ClassUtils.isAssignable(Number.class, returnType) + || org.springframework.data.util.ReflectionUtils.isVoid(returnType); boolean collectionQuery = jpaQuery.getQueryMethod().isCollectionQuery(); if (!simpleBatch && !collectionQuery) { 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 65415e8166..239ece0c1e 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 @@ -1623,12 +1623,21 @@ void deleteByShouldRemoveElementsMatchingDerivedQuery() { assertThat(repository.countByLastname(firstUser.getLastname())).isZero(); } - @Test // DATAJPA-460 + @Test // DATAJPA-460, GH-4015 void deleteByShouldReturnNumberOfEntitiesRemovedIfReturnTypeIsLong() { flushTestUsers(); assertThat(repository.removeByLastname(firstUser.getLastname())).isOne(); + assertThat(repository.removeOneByLastname(secondUser.getLastname())).isOne(); + } + + @Test // GH-4015 + void deleteByShouldReturnNumberOfEntitiesRemovedIfReturnTypeIsInt() { + + flushTestUsers(); + + assertThat(repository.removeOneMoreByLastname(secondUser.getLastname())).isOne(); } @Test // DATAJPA-460 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 3e9f4701e0..d51b89202c 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,6 +299,10 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 Long removeByLastname(String lastname); + long removeOneByLastname(String lastname); + + int removeOneMoreByLastname(String lastname); + // DATAJPA-460 List deleteByLastname(String lastname); From 811266f71c1f560830f4dd4f05139ef4677573e0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 18 Sep 2025 08:55:14 +0200 Subject: [PATCH 201/224] Polishing. Align assignability check for modifying execution. See #4015 --- .../jpa/repository/query/JpaQueryExecution.java | 13 ++++--------- .../query/JpaQueryExecutionUnitTests.java | 3 +-- 2 files changed, 5 insertions(+), 11 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 04f6186056..78c220f54f 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 @@ -352,12 +352,11 @@ public ModifyingExecution(JpaQueryMethod method, EntityManager em) { Class returnType = method.getReturnType(); - boolean isVoid = ClassUtils.isAssignable(returnType, Void.class); - boolean isInt = ClassUtils.isAssignable(returnType, Integer.class); - boolean isLong = ClassUtils.isAssignable(returnType, Long.class); + boolean isVoid = org.springframework.data.util.ReflectionUtils.isVoid(returnType); + boolean isNumber = ClassUtils.isAssignable(Number.class, returnType); - Assert.isTrue(isInt || isLong || isVoid, - "Modifying queries can only use void or int/Integer as return type; Offending method: " + method); + Assert.isTrue(isNumber || isVoid, + "Modifying queries can only use void, int/Integer, or long/Long as return type; Offending method: " + method); this.em = em; this.flush = method.getFlushAutomatically(); @@ -377,10 +376,6 @@ 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/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 c7d160d6d1..6dfa8c2be3 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,7 +24,6 @@ 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; @@ -171,7 +170,7 @@ void allowsMethodReturnTypesForModifyingQuery() { @Test void modifyingExecutionRejectsNonIntegerOrVoidReturnType() { - when(method.getReturnType()).thenReturn((Class) BigDecimal.class); + when(method.getReturnType()).thenReturn((Class) String.class); assertThatIllegalArgumentException().isThrownBy(() -> new ModifyingExecution(method, em)); } From 8b4d6018869c2d70fc7bed76beb529b03a89515e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 18 Sep 2025 10:02:34 +0200 Subject: [PATCH 202/224] Add `TypeCollectorFilters` to filter `$$_hibernate` fields and methods. Closes #4014 --- .../jpa/repository/aot/JpaTypeFilters.java | 58 ++++++++++++++++ .../resources/META-INF/spring/aot.factories | 3 + .../aot/JpaTypeFiltersUnitTests.java | 66 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaTypeFilters.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaTypeFiltersUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaTypeFilters.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaTypeFilters.java new file mode 100644 index 0000000000..91b8b84a25 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/JpaTypeFilters.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; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.function.Predicate; + +import org.springframework.data.util.Predicates; +import org.springframework.data.util.TypeCollector; +import org.springframework.data.util.TypeUtils; + +/** + * {@link TypeCollector} predicates to exclude JPA provider types. + * + * @author Mark Paluch + * @since 4.0 + */ +class JpaTypeFilters implements TypeCollector.TypeCollectorFilters { + + /** + * Match for bytecode-enhanced members. + */ + private static final Predicate IS_HIBERNATE_MEMBER = member -> member.getName().startsWith("$$_hibernate"); + + private static final Predicate> CLASS_FILTER = it -> TypeUtils.type(it).isPartOf("org.hibernate", + "org.eclipse.persistence", "jakarta.persistence"); + + @Override + public Predicate> classPredicate() { + return CLASS_FILTER.negate(); + } + + @Override + public Predicate fieldPredicate() { + return Predicates. declaringClass(CLASS_FILTER).or(IS_HIBERNATE_MEMBER).negate(); + } + + @Override + public Predicate methodPredicate() { + return Predicates. declaringClass(CLASS_FILTER).or(IS_HIBERNATE_MEMBER).negate(); + } + +} diff --git a/spring-data-jpa/src/main/resources/META-INF/spring/aot.factories b/spring-data-jpa/src/main/resources/META-INF/spring/aot.factories index 50d5fc795e..f4ea3321d5 100644 --- a/spring-data-jpa/src/main/resources/META-INF/spring/aot.factories +++ b/spring-data-jpa/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,5 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ org.springframework.data.jpa.repository.aot.JpaRuntimeHints + +org.springframework.data.util.TypeCollector$TypeCollectorFilters=\ + org.springframework.data.jpa.repository.aot.JpaTypeFilters diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaTypeFiltersUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaTypeFiltersUnitTests.java new file mode 100644 index 0000000000..0d919cc307 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaTypeFiltersUnitTests.java @@ -0,0 +1,66 @@ +/* + * 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 org.assertj.core.api.Assertions.*; + +import jakarta.persistence.LockOption; + +import org.eclipse.persistence.sessions.DatabaseSession; +import org.hibernate.Session; +import org.junit.jupiter.api.Test; + +import org.springframework.data.util.TypeCollector; + +/** + * Unit tests for {@link JpaTypeFilters}. + * + * @author Mark Paluch + */ +class JpaTypeFiltersUnitTests { + + @Test // GH-4014 + void shouldFilterUnreachableField() { + assertThat(TypeCollector.inspect(EnhancedEntity.class).list()).containsOnly(EnhancedEntity.class, Reachable.class); + } + + static class Unreachable { + + } + + static class Reachable { + + } + + static class EnhancedEntity { + + private Unreachable $$_hibernate_field; + private Reachable reachable; + private Session session; + private DatabaseSession databaseSession; + private LockOption lockOption; + + public EnhancedEntity(Session session, LockOption lockOption) { + this.session = session; + this.lockOption = lockOption; + } + + public void setSession(Session session) { + + } + } + +} From 2dacd711ab8ff3c180e70bb10c38c0f7a28a7bd5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 18 Sep 2025 10:23:03 +0200 Subject: [PATCH 203/224] Upgrade to Hibernate 7.1.1.Final. Closes #4016 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2606ddd9a4..6fd19eccb9 100755 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 4.13.2 5.0.0-B10 5.0.0-SNAPSHOT - 7.1.0.Final - 7.1.1-SNAPSHOT + 7.1.1.Final + 7.1.2-SNAPSHOT 2.7.4

        2.3.232

        3.2.0 From 10c80247f87818133f2b84c607a45060a93d6429 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 1 Sep 2025 13:16:04 +0200 Subject: [PATCH 204/224] Fix unpaged revision query. We now return all results for an unpaged query. Closes #3999 Original pull request #4000 --- .../support/EnversRevisionRepositoryImpl.java | 7 ++- .../support/RepositoryIntegrationTests.java | 55 ++++++++++++++++--- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index 7ead1e1be4..7d2cda9d15 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java @@ -186,9 +186,12 @@ public Page> findRevisions(ID id, Pageable pageable) { orderMapped.forEach(baseQuery::addOrder); + if (pageable.isPaged()) { + baseQuery.setFirstResult((int) pageable.getOffset()) // + .setMaxResults(pageable.getPageSize()); + } + List resultList = baseQuery // - .setFirstResult((int) pageable.getOffset()) // - .setMaxResults(pageable.getPageSize()) // .getResultList(); Long count = (Long) createBaseQuery(id) // diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java index 43f159b633..bf04d06e28 100755 --- a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java @@ -15,12 +15,23 @@ */ package org.springframework.data.envers.repository.support; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.history.RevisionMetadata.RevisionType.*; + +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Optional; + 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.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.envers.Config; import org.springframework.data.envers.sample.Country; @@ -33,21 +44,13 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.time.Instant; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.history.RevisionMetadata.RevisionType.*; - /** * Integration tests for repositories. * * @author Oliver Gierke * @author Jens Schauder * @author Niklas Loechte + * @author Mark Paluch */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = Config.class) @@ -107,6 +110,40 @@ void testLifeCycle() { }); } + @Test // GH-3999 + void shouldReturnUnpagedResults() { + + License license = new License(); + license.name = "Schnitzel"; + + licenseRepository.save(license); + + Country de = new Country(); + de.code = "de"; + de.name = "Deutschland"; + + countryRepository.save(de); + + Country se = new Country(); + se.code = "se"; + se.name = "Schweden"; + + countryRepository.save(se); + + license.laender = new HashSet<>(); + license.laender.addAll(Arrays.asList(de, se)); + + licenseRepository.save(license); + + de.name = "Daenemark"; + + countryRepository.save(de); + + Page> revisions = licenseRepository.findRevisions(license.id, Pageable.unpaged()); + + assertThat(revisions).hasSize(2); + } + @Test // #1 void returnsEmptyLastRevisionForUnrevisionedEntity() { assertThat(countryRepository.findLastChangeRevision(100L)).isEmpty(); From f0345f5ce6ea75edb353232838402cf1164ebb97 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 1 Sep 2025 13:16:52 +0200 Subject: [PATCH 205/224] Polishing. Add missing Override annotations. See #3999 Original pull request #4000 --- .../repository/support/EnversRevisionRepositoryImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index 7d2cda9d15..a724d86f9f 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java @@ -96,6 +96,7 @@ public EnversRevisionRepositoryImpl(JpaEntityInformation entityInformation this.entityManager = entityManager; } + @Override @SuppressWarnings("unchecked") public Optional> findLastChangeRevision(ID id) { @@ -135,6 +136,7 @@ public Optional> findRevision(ID id, N revisionNumber) { return Optional.of(createRevision(new QueryResult<>(singleResult.get(0)))); } + @Override @SuppressWarnings("unchecked") public Revisions findRevisions(ID id) { @@ -175,6 +177,7 @@ private List mapPropertySort(Sort sort) { return result; } + @Override @SuppressWarnings("unchecked") public Page> findRevisions(ID id, Pageable pageable) { From b8ebcf9ded607e6deffca022847ecd21d9c0a2e7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 19 Sep 2025 09:23:25 +0200 Subject: [PATCH 206/224] =?UTF-8?q?Document=20placeholder=20and=20Ant-styl?= =?UTF-8?q?e=20pattern=20support=20for=20`@Enable=E2=80=A6Repositories`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes spring-projects/spring-data-commons#3366 --- .../repository/config/EnableJpaRepositories.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 22f32ed2de..ce89b4ace6 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 @@ -57,8 +57,20 @@ String[] value() default {}; /** - * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this - * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * Base packages to scan for annotated components. + *

        + * {@link #value} is an alias for (and mutually exclusive with) this attribute. + *

        + * Supports {@code ${…}} placeholders which are resolved against the {@link org.springframework.core.env.Environment + * Environment} as well as Ant-style package patterns — for example, {@code "org.example.**"}. + *

        + * Multiple packages or patterns may be specified, either separately or within a single {@code String} — for + * example, {@code {"org.example.config", "org.example.service.**"}} or + * {@code "org.example.config, org.example.service.**"}. + *

        + * Use {@link #basePackageClasses} for a type-safe alternative to String-based package names. + * + * @see org.springframework.context.ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS */ String[] basePackages() default {}; From 03e986243cf25a214eb60fa8bac4826239f03681 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Sep 2025 10:52:06 +0200 Subject: [PATCH 207/224] Update GitHub Actions. See #4011 --- .github/workflows/codeql.yml | 21 +++++++++++++++++++++ .github/workflows/project.yml | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..411d4a9338 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,21 @@ +# GitHub Actions for CodeQL Scanning + +name: "CodeQL Advanced" + +on: + push: + pull_request: + workflow_dispatch: + schedule: + # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule + - cron: '0 5 * * *' + +permissions: read-all + +jobs: + codeql-analysis-call: + permissions: + actions: read + contents: read + security-events: write + uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1 diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index a5f764579a..4c8108d353 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -10,6 +10,11 @@ on: pull_request_target: types: [opened, edited, reopened] +permissions: + contents: read + issues: write + pull-requests: write + jobs: Inbox: runs-on: ubuntu-latest From a5b5c2e610a3fffa3c736ff878c21747bc63c150 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Sep 2025 10:07:35 +0200 Subject: [PATCH 208/224] Adapt to AOT Infrastructure changes in Commons. See spring-projects/spring-data-commons#3267 --- .../AotRepositoryQueryMethodBenchmarks.java | 17 ++-- .../config/JpaRepositoryConfigExtension.java | 15 +++- .../aot/AotContributionIntegrationTests.java | 4 - .../AotFragmentTestConfigurationSupport.java | 20 ++--- .../aot/TestJpaAotRepositoryContext.java | 69 ++--------------- ...toryRegistrationAotProcessorUnitTests.java | 77 ++++--------------- 6 files changed, 61 insertions(+), 141 deletions(-) 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 index 1ec94e1602..e2dd2d3107 100644 --- 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 @@ -40,6 +40,7 @@ import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.test.tools.TestCompiler; @@ -56,6 +57,7 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -82,11 +84,16 @@ public class AotRepositoryQueryMethodBenchmarks { public static class BenchmarkParameters { public static Class aot; - public static TestJpaAotRepositoryContext repositoryContext = new TestJpaAotRepositoryContext<>( - PersonRepository.class, null, - new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(SampleConfig.class), - EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), - Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); + public static TestJpaAotRepositoryContext repositoryContext; + + static { + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource( + AnnotationMetadata.introspect(SampleConfig.class), EnableJpaRepositories.class, new DefaultResourceLoader(), + new StandardEnvironment(), Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE); + + repositoryContext = new TestJpaAotRepositoryContext<>(new DefaultListableBeanFactory(), PersonRepository.class, + null, configurationSource); + } EntityManager entityManager; RepositoryComposition.RepositoryFragments fragments; 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 28b75ff3f9..e61f0e13ab 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 @@ -58,6 +58,7 @@ import org.springframework.core.io.ResourceLoader; 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.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.DefaultJpaContext; @@ -374,10 +375,22 @@ static boolean isActive(@Nullable ClassLoader classLoader) { public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor { public static final String USE_ENTITY_MANAGER = "spring.aot.jpa.repositories.use-entitymanager"; + private static final String MODULE_NAME = "jpa"; - protected @Nullable JpaRepositoryContributor contribute(AotRepositoryContext repositoryContext, + @Override + protected void configureTypeContributions(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + super.configureTypeContributions(repositoryContext, generationContext); + } + + @Override + protected void configureTypeContribution(Class type, AotContext aotContext) { + aotContext.typeConfiguration(type, config -> config.contributeAccessors().forQuerydsl()); + } + + @Override + protected @Nullable JpaRepositoryContributor contributeAotRepository(AotRepositoryContext repositoryContext) { if (!repositoryContext.isGeneratedRepositoriesEnabled(MODULE_NAME)) { return null; 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 60ba9c23df..69fed19329 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 @@ -30,10 +30,8 @@ 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. @@ -70,8 +68,6 @@ void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOExcep 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(); 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 317cdcd9c6..960b1a4410 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 @@ -47,6 +47,7 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; @@ -69,7 +70,8 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce private final Class repositoryInterface; private final boolean registerFragmentFacade; - private final TestJpaAotRepositoryContext repositoryContext; + private final Class[] additionalFragments; + private final RepositoryConfigurationSource configSource; public AotFragmentTestConfigurationSupport(Class repositoryInterface) { this(repositoryInterface, SampleConfig.class, true); @@ -82,22 +84,22 @@ public AotFragmentTestConfigurationSupport(Class repositoryInterface, Class repositoryInterface, Class configClass, boolean registerFragmentFacade, Class... additionalFragments) { this.repositoryInterface = repositoryInterface; - - RepositoryComposition composition = RepositoryComposition - .of((List) Arrays.stream(additionalFragments).map(RepositoryFragment::structural).toList()); - this.repositoryContext = new TestJpaAotRepositoryContext<>(repositoryInterface, composition, - new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(configClass), - EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), - Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE)); this.registerFragmentFacade = registerFragmentFacade; + this.additionalFragments = additionalFragments; + this.configSource = new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(configClass), + EnableJpaRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(), + Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface); + RepositoryComposition composition = RepositoryComposition + .of((List) Arrays.stream(additionalFragments).map(RepositoryFragment::structural).toList()); - repositoryContext.setBeanFactory(beanFactory); + TestJpaAotRepositoryContext repositoryContext = new TestJpaAotRepositoryContext<>(beanFactory, + repositoryInterface, composition, configSource); new JpaRepositoryContributor(repositoryContext).contribute(generationContext); 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 ced6e6c389..bb107c5e2c 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 @@ -19,24 +19,20 @@ import jakarta.persistence.MappedSuperclass; import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.List; import java.util.Set; -import java.util.function.Consumer; import org.jspecify.annotations.Nullable; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.BeanFactory; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.data.aot.AotTypeConfiguration; +import org.springframework.data.aot.AotContext; 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.data.jpa.repository.support.JpaRepositoryFragmentsContributor; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.AotRepositoryContextSupport; import org.springframework.data.repository.config.AotRepositoryInformation; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; @@ -49,15 +45,16 @@ * * @author Christoph Strobl */ -public class TestJpaAotRepositoryContext implements AotRepositoryContext { +public class TestJpaAotRepositoryContext extends AotRepositoryContextSupport { private final AotRepositoryInformation repositoryInformation; private final Class repositoryInterface; private final RepositoryConfigurationSource configurationSource; - private @Nullable ConfigurableListableBeanFactory beanFactory; - public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition, + public TestJpaAotRepositoryContext(BeanFactory beanFactory, Class repositoryInterface, + @Nullable RepositoryComposition composition, RepositoryConfigurationSource configurationSource) { + super(AotContext.from(beanFactory)); this.repositoryInterface = repositoryInterface; this.configurationSource = configurationSource; @@ -69,45 +66,6 @@ public TestJpaAotRepositoryContext(Class repositoryInterface, @Nullable Repos composition.append(fragments).getFragments().stream().toList()); } - public Class getRepositoryInterface() { - return repositoryInterface; - } - - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return beanFactory; - } - - @Override - public Environment getEnvironment() { - return new StandardEnvironment(); - } - - @Override - public TypeIntrospector introspectType(String typeName) { - return null; - } - - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return null; - } - - @Override - public void typeConfiguration(Class type, Consumer configurationConsumer) { - - } - - @Override - public Collection typeConfigurations() { - return List.of(); - } - - @Override - public String getBeanName() { - return "dummyRepository"; - } - @Override public String getModuleName() { return "JPA"; @@ -118,11 +76,6 @@ public RepositoryConfigurationSource getConfigurationSource() { return configurationSource; } - @Override - public Set getBasePackages() { - return Set.of("org.springframework.data.dummy.repository.aot"); - } - @Override public Set> getIdentifyingAnnotations() { return Set.of(Entity.class, MappedSuperclass.class); @@ -143,12 +96,4 @@ public Set> getResolvedTypes() { return Set.of(User.class, SpecialUser.class, Role.class); } - @Override - public Set> getUserDomainTypes() { - return Set.of(); - } - - public void setBeanFactory(ConfigurableListableBeanFactory beanFactory) { - this.beanFactory = beanFactory; - } } 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 0989e674a1..25277bad6c 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 @@ -23,13 +23,12 @@ import java.lang.annotation.Annotation; import java.net.URL; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.function.Consumer; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearSystemProperty; import org.junitpioneer.jupiter.SetSystemProperty; @@ -47,17 +46,15 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.data.aot.AotContext; -import org.springframework.data.aot.AotTypeConfiguration; import org.springframework.data.jpa.repository.aot.JpaRepositoryContributor; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.AotRepositoryContextSupport; import org.springframework.data.repository.config.AotRepositoryInformation; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.javapoet.ClassName; -import org.springframework.mock.env.MockPropertySource; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; /** @@ -70,13 +67,14 @@ class JpaRepositoryRegistrationAotProcessorUnitTests { @Test // GH-2628 + @Disabled("TODO: Superfluous contributeType in Commons") void aotProcessorMustNotRegisterDomainTypes() { GenerationContext ctx = createGenerationContext(); GenericApplicationContext context = new GenericApplicationContext(); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(context) { + .configureTypeContributions(new DummyAotRepositoryContext(context) { @Override public Set> getResolvedTypes() { return Collections.singleton(Person.class); @@ -93,7 +91,7 @@ void aotProcessorMustNotRegisterAnnotations() { GenericApplicationContext context = new GenericApplicationContext(); new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(context) { + .configureTypeContributions(new DummyAotRepositoryContext(context) { @Override public Set> getResolvedAnnotations() { @@ -109,8 +107,6 @@ public Set> getResolvedAnnotations() { @Test // GH-3838 void repositoryProcessorShouldConsiderPersistenceManagedTypes() { - GenerationContext ctx = createGenerationContext(); - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(PersistenceManagedTypes.class, () -> { @@ -132,11 +128,8 @@ public List getManagedPackages() { }; }); - context.getEnvironment().getPropertySources() - .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); - JpaRepositoryContributor contributor = new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(context), ctx); + .contributeAotRepository(new DummyAotRepositoryContext(context)); assertThat(contributor.getMetamodel().managedType(Person.class)).isNotNull(); } @@ -145,10 +138,9 @@ public List getManagedPackages() { @SetSystemProperty(key = AotDetector.AOT_ENABLED, value = "true") void repositoryProcessorShouldEnableAotRepositoriesByDefaultWhenAotIsEnabled() { - GenerationContext ctx = createGenerationContext(); GenericApplicationContext context = new GenericApplicationContext(); - JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context); assertThat(contributor).isNotNull(); } @@ -157,10 +149,9 @@ void repositoryProcessorShouldEnableAotRepositoriesByDefaultWhenAotIsEnabled() { @ClearSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED) void shouldEnableAotRepositoriesByDefault() { - GenerationContext ctx = createGenerationContext(); GenericApplicationContext context = new GenericApplicationContext(); - JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context); assertThat(contributor).isNotNull(); } @@ -169,10 +160,9 @@ void shouldEnableAotRepositoriesByDefault() { @SetSystemProperty(key = AotContext.GENERATED_REPOSITORIES_ENABLED, value = "false") void shouldDisableAotRepositoriesWhenGeneratedRepositoriesIsFalse() { - GenerationContext ctx = createGenerationContext(); GenericApplicationContext context = new GenericApplicationContext(); - JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context); assertThat(contributor).isNull(); } @@ -181,10 +171,9 @@ void shouldDisableAotRepositoriesWhenGeneratedRepositoriesIsFalse() { @SetSystemProperty(key = "spring.aot.jpa.repositories.enabled", value = "false") void shouldDisableAotRepositoriesWhenJpaGeneratedRepositoriesIsFalse() { - GenerationContext ctx = createGenerationContext(); GenericApplicationContext context = new GenericApplicationContext(); - JpaRepositoryContributor contributor = createContributorWithPersonTypes(context, ctx); + JpaRepositoryContributor contributor = createContributorWithPersonTypes(context); assertThat(contributor).isNull(); } @@ -194,15 +183,15 @@ private GenerationContext createGenerationContext() { new InMemoryGeneratedFiles()); } - private JpaRepositoryContributor createContributorWithPersonTypes(GenericApplicationContext context, GenerationContext ctx) { + private JpaRepositoryContributor createContributorWithPersonTypes(GenericApplicationContext context) { return new JpaRepositoryConfigExtension.JpaRepositoryRegistrationAotProcessor() - .contribute(new DummyAotRepositoryContext(context) { + .contributeAotRepository(new DummyAotRepositoryContext(context) { @Override public Set> getResolvedTypes() { return Collections.singleton(Person.class); } - }, ctx); + }); } @Entity @@ -212,19 +201,15 @@ static class Person { interface PersonRepository extends Repository {} - static class DummyAotRepositoryContext implements AotRepositoryContext { + static class DummyAotRepositoryContext extends AotRepositoryContextSupport { - private final @Nullable AbstractApplicationContext applicationContext; + private final AbstractApplicationContext applicationContext; - DummyAotRepositoryContext(@Nullable AbstractApplicationContext applicationContext) { + DummyAotRepositoryContext(AbstractApplicationContext applicationContext) { + super(AotContext.from(applicationContext, applicationContext.getEnvironment())); this.applicationContext = applicationContext; } - @Override - public String getBeanName() { - return "jpaRepository"; - } - @Override public String getModuleName() { return "JPA"; @@ -235,11 +220,6 @@ public RepositoryConfigurationSource getConfigurationSource() { return mock(RepositoryConfigurationSource.class); } - @Override - public Set getBasePackages() { - return Collections.singleton(this.getClass().getPackageName()); - } - @Override public Set> getIdentifyingAnnotations() { return Collections.singleton(Entity.class); @@ -261,11 +241,6 @@ public Set> getResolvedTypes() { return Set.of(); } - @Override - public Set> getUserDomainTypes() { - return Set.of(); - } - @Override public ConfigurableListableBeanFactory getBeanFactory() { return applicationContext != null ? applicationContext.getBeanFactory() : null; @@ -276,25 +251,7 @@ public Environment getEnvironment() { return applicationContext == null ? new StandardEnvironment() : applicationContext.getEnvironment(); } - @Override - public TypeIntrospector introspectType(String typeName) { - return null; - } - - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return null; - } - - @Override - public void typeConfiguration(Class type, Consumer configurationConsumer) { - } - - @Override - public Collection typeConfigurations() { - return List.of(); - } } } From ee039fe094bdabec3a6c9e02422a9fd4194373f2 Mon Sep 17 00:00:00 2001 From: Peter Aisher Date: Thu, 25 Sep 2025 17:21:16 +0200 Subject: [PATCH 209/224] Constistent `unrestricted()` behaviour for all `*Specification` types. Closes #4023 Original pull request: #4024 Signed-off-by: Peter Aisher --- .../data/jpa/domain/DeleteSpecification.java | 24 ++++++++++++++----- .../jpa/domain/PredicateSpecification.java | 24 ++++++++++++++----- .../data/jpa/domain/Specification.java | 19 +++++++++++---- .../data/jpa/domain/UpdateSpecification.java | 24 ++++++++++++++----- .../domain/DeleteSpecificationUnitTests.java | 11 ++++----- .../PredicateSpecificationUnitTests.java | 11 ++++----- .../jpa/domain/SpecificationUnitTests.java | 11 ++++----- .../domain/UpdateSpecificationUnitTests.java | 11 ++++----- 8 files changed, 89 insertions(+), 46 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 4c7deb638d..79a3865afe 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 @@ -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. @@ -35,18 +35,30 @@ *

        * 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. + * {@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}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Mark Paluch + * @author Peter Aisher * @since 4.0 */ @FunctionalInterface public interface DeleteSpecification extends Serializable { /** - * Simple static factory method to create a specification deleting all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

        +	 * {@code
        +	 * unrestricted().and(other) // consider only `other`
        +	 * unrestricted().or(other) // consider only `other`
        +	 * not(unrestricted()) // equivalent to `unrestricted()`
        +	 * }
        +	 * 
        * * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. * @return guaranteed to be not {@literal null}. @@ -159,7 +171,7 @@ static DeleteSpecification not(DeleteSpecification spec) { return (root, delete, builder) -> { Predicate predicate = spec.toPredicate(root, delete, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : 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 daa39b9ba7..3c7e048b81 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 @@ -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. @@ -34,18 +34,30 @@ *

        * 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. + * {@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}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Mark Paluch + * @author Peter Aisher * @since 4.0 */ @FunctionalInterface public interface PredicateSpecification extends Serializable { /** - * Simple static factory method to create a specification matching all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

        +	 * {@code
        +	 * unrestricted().and(other) // consider only `other`
        +	 * unrestricted().or(other) // consider only `other`
        +	 * not(unrestricted()) // equivalent to `unrestricted()`
        +	 * }
        +	 * 
        * * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. * @return guaranteed to be not {@literal null}. @@ -113,7 +125,7 @@ static PredicateSpecification not(PredicateSpecification spec) { return (root, builder) -> { Predicate predicate = spec.toPredicate(root, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : 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 8427c5e8aa..b0b2c943b2 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 @@ -36,9 +36,10 @@ *

        * 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. + * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, 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 @@ -49,12 +50,22 @@ * @author Daniel Shuy * @author Sergey Rukin * @author Heeeun Cho + * @author Peter Aisher */ @FunctionalInterface public interface Specification extends Serializable { /** - * Simple static factory method to create a specification matching all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

        +	 * {@code
        +	 * unrestricted().and(other) // consider only `other`
        +	 * unrestricted().or(other) // consider only `other`
        +	 * not(unrestricted()) // equivalent to `unrestricted()`
        +	 * }
        +	 * 
        * * @param the type of the {@link Root} the resulting {@literal Specification} operates on. * @return guaranteed to be not {@literal null}. @@ -175,7 +186,7 @@ static Specification not(Specification spec) { return (root, query, builder) -> { Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : 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 1a27d428a4..02bff0c3d6 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 @@ -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. @@ -35,18 +35,30 @@ *

        * 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. + * {@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}, such as {@link #unrestricted()}, are + * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * * @author Mark Paluch + * @author Peter Aisher * @since 4.0 */ @FunctionalInterface public interface UpdateSpecification extends Serializable { /** - * Simple static factory method to create a specification updating all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

        +	 * {@code
        +	 * unrestricted().and(other) // consider only `other`
        +	 * unrestricted().or(other) // consider only `other`
        +	 * not(unrestricted()) // equivalent to `unrestricted()`
        +	 * }
        +	 * 
        * * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. * @return guaranteed to be not {@literal null}. @@ -180,7 +192,7 @@ static UpdateSpecification not(UpdateSpecification spec) { return (root, update, builder) -> { Predicate predicate = spec.toPredicate(root, update, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : 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 index 99e6bb80ac..13e051c46f 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 @@ -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. @@ -38,6 +38,7 @@ * Unit tests for {@link DeleteSpecification}. * * @author Mark Paluch + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -158,15 +159,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 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(); + assertThat(notSpec.toPredicate(root, delete, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, DeleteSpecification { 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 0bcefc79ae..0bce2b5a2c 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 @@ -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. @@ -37,6 +37,7 @@ * Unit tests for {@link PredicateSpecification}. * * @author Mark Paluch + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -156,15 +157,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 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(); + assertThat(notSpec.toPredicate(root, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, PredicateSpecification { 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 cee8386513..4082596884 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 @@ -43,6 +43,7 @@ * @author Mark Paluch * @author Daniel Shuy * @author Heeeun Cho + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -127,15 +128,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 void notWithNullPredicate() { - when(builder.disjunction()).thenReturn(mock(Predicate.class)); + Specification notSpec = Specification.not(Specification.unrestricted()); - Specification notSpec = Specification.not((r, q, cb) -> null); - - assertThat(notSpec.toPredicate(root, query, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, query, builder)).isNull(); + verifyNoInteractions(builder); } @Test // GH-3992 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 f65ab6ecaa..a5415a3bd1 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 @@ -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. @@ -38,6 +38,7 @@ * Unit tests for {@link UpdateSpecification}. * * @author Mark Paluch + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation" }) @ExtendWith(MockitoExtension.class) @@ -158,15 +159,13 @@ void orCombinesSpecificationsInOrder() { verify(builder).or(firstPredicate, secondPredicate); } - @Test // GH-3849 + @Test // GH-3849, GH-4023 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(); + assertThat(notSpec.toPredicate(root, update, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, UpdateSpecification { From 4010a93a0bd92e69af0ec752912824dfb493e1f2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Sep 2025 10:12:43 +0200 Subject: [PATCH 210/224] Polishing. Refine Javadoc. See #4023 Original pull request: #4024 --- .../data/jpa/domain/DeleteSpecification.java | 14 ++++++-------- .../data/jpa/domain/PredicateSpecification.java | 14 ++++++-------- .../data/jpa/domain/Specification.java | 14 ++++++-------- .../data/jpa/domain/SpecificationComposition.java | 2 +- .../data/jpa/domain/UpdateSpecification.java | 14 ++++++-------- 5 files changed, 25 insertions(+), 33 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 79a3865afe..ea66edd6a7 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 @@ -51,13 +51,11 @@ public interface DeleteSpecification extends Serializable { /** * Simple static factory method to create a specification which does not participate in matching. The specification * returned is {@code null}-like, and is elided in all operations. - * - *
        -	 * {@code
        +	 *
        +	 * 
         	 * unrestricted().and(other) // consider only `other`
         	 * unrestricted().or(other) // consider only `other`
         	 * not(unrestricted()) // equivalent to `unrestricted()`
        -	 * }
         	 * 
        * * @param the type of the {@link Root} the resulting {@literal DeleteSpecification} operates on. @@ -177,7 +175,7 @@ static DeleteSpecification not(DeleteSpecification spec) { /** * 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. + * resulting {@link DeleteSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the conjunction of the specifications. @@ -191,7 +189,7 @@ static DeleteSpecification allOf(DeleteSpecification... specifications /** * 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. + * resulting {@link DeleteSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the conjunction of the specifications. @@ -206,7 +204,7 @@ static DeleteSpecification allOf(Iterable> specifi /** * 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. + * resulting {@link DeleteSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the disjunction of the specifications. @@ -220,7 +218,7 @@ static DeleteSpecification anyOf(DeleteSpecification... specifications /** * 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. + * resulting {@link DeleteSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link DeleteSpecification}s to compose. * @return the disjunction of the specifications. 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 3c7e048b81..de5a0ecdf0 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 @@ -50,13 +50,11 @@ public interface PredicateSpecification extends Serializable { /** * Simple static factory method to create a specification which does not participate in matching. The specification * returned is {@code null}-like, and is elided in all operations. - * - *
        -	 * {@code
        +	 *
        +	 * 
         	 * unrestricted().and(other) // consider only `other`
         	 * unrestricted().or(other) // consider only `other`
         	 * not(unrestricted()) // equivalent to `unrestricted()`
        -	 * }
         	 * 
        * * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. @@ -131,7 +129,7 @@ static PredicateSpecification not(PredicateSpecification spec) { /** * 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. + * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the conjunction of the specifications. @@ -145,7 +143,7 @@ static PredicateSpecification allOf(PredicateSpecification... specific /** * 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. + * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the conjunction of the specifications. @@ -160,7 +158,7 @@ static PredicateSpecification allOf(Iterable> 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. + * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the disjunction of the specifications. @@ -174,7 +172,7 @@ static PredicateSpecification anyOf(PredicateSpecification... specific /** * 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. + * resulting {@link PredicateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link PredicateSpecification}s to compose. * @return the disjunction of the specifications. 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 b0b2c943b2..551ffb5367 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 @@ -58,13 +58,11 @@ public interface Specification extends Serializable { /** * Simple static factory method to create a specification which does not participate in matching. The specification * returned is {@code null}-like, and is elided in all operations. - * - *
        -	 * {@code
        +	 *
        +	 * 
         	 * unrestricted().and(other) // consider only `other`
         	 * unrestricted().or(other) // consider only `other`
         	 * not(unrestricted()) // equivalent to `unrestricted()`
        -	 * }
         	 * 
        * * @param the type of the {@link Root} the resulting {@literal Specification} operates on. @@ -192,7 +190,7 @@ static Specification not(Specification spec) { /** * 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. + * {@link Specification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the conjunction of the specifications. @@ -207,7 +205,7 @@ static Specification allOf(Specification... specifications) { /** * 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. + * {@link Specification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the conjunction of the specifications. @@ -223,7 +221,7 @@ static Specification allOf(Iterable> specifications) { /** * 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. + * {@link Specification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the disjunction of the specifications @@ -238,7 +236,7 @@ static Specification anyOf(Specification... specifications) { /** * 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. + * {@link Specification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link Specification}s to compose. * @return the disjunction of the specifications 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 0c73627bae..48dfd1a141 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 @@ -34,8 +34,8 @@ * @author Oliver Gierke * @author Jens Schauder * @author Mark Paluch - * @see Specification * @since 2.2 + * @see Specification */ class SpecificationComposition { 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 02bff0c3d6..eab5275ba1 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 @@ -51,13 +51,11 @@ public interface UpdateSpecification extends Serializable { /** * Simple static factory method to create a specification which does not participate in matching. The specification * returned is {@code null}-like, and is elided in all operations. - * - *
        -	 * {@code
        +	 *
        +	 * 
         	 * unrestricted().and(other) // consider only `other`
         	 * unrestricted().or(other) // consider only `other`
         	 * not(unrestricted()) // equivalent to `unrestricted()`
        -	 * }
         	 * 
        * * @param the type of the {@link Root} the resulting {@literal UpdateSpecification} operates on. @@ -198,7 +196,7 @@ static UpdateSpecification not(UpdateSpecification spec) { /** * 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. + * resulting {@link UpdateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the conjunction of the specifications. @@ -212,7 +210,7 @@ static UpdateSpecification allOf(UpdateSpecification... specifications /** * 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. + * resulting {@link UpdateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the conjunction of the specifications. @@ -227,7 +225,7 @@ static UpdateSpecification allOf(Iterable> specifi /** * 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. + * resulting {@link UpdateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the disjunction of the specifications. @@ -241,7 +239,7 @@ static UpdateSpecification anyOf(UpdateSpecification... specifications /** * 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. + * resulting {@link UpdateSpecification} will be {@link #unrestricted()} applying to all objects. * * @param specifications the {@link UpdateSpecification}s to compose. * @return the disjunction of the specifications. From 73a16326b6f4327161d52e44639546cc6e570572 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Sep 2025 12:01:24 +0200 Subject: [PATCH 211/224] Update CI Properties. See #4011 --- ci/pipeline.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/pipeline.properties b/ci/pipeline.properties index cde4a8e881..ed898052b1 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,6 +1,6 @@ # Java versions -java.main.tag=24.0.1_9-jdk-noble -java.next.tag=24.0.1_9-jdk-noble +java.main.tag=25-jdk-noble +java.next.tag=25-jdk-noble # Docker container images - standard docker.java.main.image=library/eclipse-temurin:${java.main.tag} @@ -14,6 +14,7 @@ docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis docker.redis.6.version=6.2.13 docker.redis.7.version=7.2.4 +docker.valkey.8.version=8.1.1 # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home From 13340080183238c430ea05242a0589d1794c8bef Mon Sep 17 00:00:00 2001 From: KNU-K Date: Fri, 26 Sep 2025 07:11:35 +0900 Subject: [PATCH 212/224] =?UTF-8?q?Replace=20recursion=20in=20`QueryRender?= =?UTF-8?q?er.isSubquery(=E2=80=A6)`=20with=20loop.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: KNU-K Closes #4025 --- .../repository/query/EqlQueryRenderer.java | 28 +++++++------ .../repository/query/HqlQueryRenderer.java | 40 ++++++++++--------- .../repository/query/JpqlQueryRenderer.java | 28 +++++++------ 3 files changed, 54 insertions(+), 42 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 230da53fed..a1f2bcde65 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 @@ -32,6 +32,7 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. * + * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch @@ -47,15 +48,16 @@ class EqlQueryRenderer extends EqlBaseVisitor { */ static boolean isSubquery(ParserRuleContext ctx) { - if (ctx instanceof EqlParser.SubqueryContext) { - return true; - } else if (ctx instanceof EqlParser.Update_statementContext) { - return false; - } else if (ctx instanceof EqlParser.Delete_statementContext) { - return false; - } else { - return ctx.getParent() != null && isSubquery(ctx.getParent()); + while (ctx != null) { + if (ctx instanceof EqlParser.SubqueryContext) { + return true; + } + if (ctx instanceof EqlParser.Update_statementContext || ctx instanceof EqlParser.Delete_statementContext) { + return false; + } + ctx = ctx.getParent(); } + return false; } /** @@ -65,11 +67,13 @@ static boolean isSubquery(ParserRuleContext ctx) { */ static boolean isSetQuery(ParserRuleContext ctx) { - if (ctx instanceof EqlParser.Set_fuctionContext) { - return true; + while (ctx != null) { + if (ctx instanceof EqlParser.Set_fuctionContext) { + return true; + } + ctx = ctx.getParent(); } - - return ctx.getParent() != null && isSetQuery(ctx.getParent()); + return false; } @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 15522f0263..45300c2183 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 @@ -32,6 +32,7 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. * + * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @author Oscar Fanchin @@ -48,19 +49,20 @@ class HqlQueryRenderer extends HqlBaseVisitor { */ static boolean isSubquery(ParserRuleContext ctx) { - if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) { - return true; - } else if (ctx instanceof HqlParser.SelectStatementContext) { - return false; - } else if (ctx instanceof HqlParser.InsertStatementContext) { - return false; - } else if (ctx instanceof HqlParser.DeleteStatementContext) { - return false; - } else if (ctx instanceof HqlParser.UpdateStatementContext) { - return false; - } else { - return ctx.getParent() != null && isSubquery(ctx.getParent()); + while (ctx != null) { + if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) { + return true; + } + if (ctx instanceof HqlParser.SelectStatementContext || + ctx instanceof HqlParser.InsertStatementContext || + ctx instanceof HqlParser.DeleteStatementContext || + ctx instanceof HqlParser.UpdateStatementContext + ) { + return false; + } + ctx = ctx.getParent(); } + return false; } /** @@ -70,14 +72,16 @@ static boolean isSubquery(ParserRuleContext ctx) { */ static boolean isSetQuery(ParserRuleContext ctx) { - if (ctx instanceof HqlParser.OrderedQueryContext - && ctx.getParent() instanceof HqlParser.QueryExpressionContext qec) { - if (qec.orderedQuery().indexOf(ctx) != 0) { - return true; + while (ctx != null) { + ParserRuleContext parent = ctx.getParent(); + if (ctx instanceof HqlParser.OrderedQueryContext && parent instanceof HqlParser.QueryExpressionContext qec) { + if (qec.orderedQuery().indexOf(ctx) != 0) { + return true; + } } + ctx = parent; } - - return ctx.getParent() != null && isSetQuery(ctx.getParent()); + return false; } @Override 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 3e3c39fa19..f9da5749ce 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 @@ -32,6 +32,7 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. * + * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch @@ -47,15 +48,16 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { */ static boolean isSubquery(ParserRuleContext ctx) { - if (ctx instanceof JpqlParser.SubqueryContext) { - return true; - } else if (ctx instanceof JpqlParser.Update_statementContext) { - return false; - } else if (ctx instanceof JpqlParser.Delete_statementContext) { - return false; - } else { - return ctx.getParent() != null && isSubquery(ctx.getParent()); + while (ctx != null) { + if (ctx instanceof JpqlParser.SubqueryContext) { + return true; + } + if (ctx instanceof JpqlParser.Update_statementContext || ctx instanceof JpqlParser.Delete_statementContext) { + return false; + } + ctx = ctx.getParent(); } + return false; } /** @@ -65,11 +67,13 @@ static boolean isSubquery(ParserRuleContext ctx) { */ static boolean isSetQuery(ParserRuleContext ctx) { - if (ctx instanceof JpqlParser.Set_fuctionContext) { - return true; + while (ctx != null) { + if (ctx instanceof JpqlParser.Set_fuctionContext) { + return true; + } + ctx = ctx.getParent(); } - - return ctx.getParent() != null && isSetQuery(ctx.getParent()); + return false; } @Override From 9e135f0aaf6b24caefb802d58c2f5bbdc8875ee5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Sep 2025 08:42:39 +0200 Subject: [PATCH 213/224] Polishing. Reformat code and reorder author tags. See #4025 --- .../repository/query/EqlQueryRenderer.java | 11 ++++++++-- .../repository/query/HqlQueryRenderer.java | 22 ++++++++++++------- .../repository/query/JpqlQueryRenderer.java | 11 ++++++++-- 3 files changed, 32 insertions(+), 12 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 a1f2bcde65..f88cfa3e23 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 @@ -32,10 +32,10 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. * - * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch + * @author TaeHyun Kang * @since 3.2 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode" }) @@ -44,19 +44,23 @@ class EqlQueryRenderer extends EqlBaseVisitor { /** * Is this AST tree a {@literal subquery}? * - * @return boolean + * @return {@literal true} is the query is a subquery; {@literal false} otherwise. */ static boolean isSubquery(ParserRuleContext ctx) { while (ctx != null) { + if (ctx instanceof EqlParser.SubqueryContext) { return true; } + if (ctx instanceof EqlParser.Update_statementContext || ctx instanceof EqlParser.Delete_statementContext) { return false; } + ctx = ctx.getParent(); } + return false; } @@ -68,11 +72,14 @@ static boolean isSubquery(ParserRuleContext ctx) { static boolean isSetQuery(ParserRuleContext ctx) { while (ctx != null) { + if (ctx instanceof EqlParser.Set_fuctionContext) { return true; } + ctx = ctx.getParent(); } + return false; } 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 45300c2183..72b3296778 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 @@ -32,11 +32,11 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. * - * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @author Oscar Fanchin * @author Mark Paluch + * @author TaeHyun Kang * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode", "UnreachableCode" }) @@ -45,42 +45,48 @@ class HqlQueryRenderer extends HqlBaseVisitor { /** * Is this AST tree a {@literal subquery}? * - * @return boolean + * @return {@literal true} is the query is a subquery; {@literal false} otherwise. */ static boolean isSubquery(ParserRuleContext ctx) { while (ctx != null) { + if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) { return true; } + if (ctx instanceof HqlParser.SelectStatementContext || ctx instanceof HqlParser.InsertStatementContext || ctx instanceof HqlParser.DeleteStatementContext || - ctx instanceof HqlParser.UpdateStatementContext - ) { + ctx instanceof HqlParser.UpdateStatementContext) { return false; } + ctx = ctx.getParent(); } + return false; } /** * Is this AST tree a {@literal set} query that has been added through {@literal UNION|INTERSECT|EXCEPT}? * - * @return boolean + * @return {@literal true} is the query is a set query; {@literal false} otherwise. */ static boolean isSetQuery(ParserRuleContext ctx) { while (ctx != null) { - ParserRuleContext parent = ctx.getParent(); - if (ctx instanceof HqlParser.OrderedQueryContext && parent instanceof HqlParser.QueryExpressionContext qec) { + + if (ctx instanceof HqlParser.OrderedQueryContext + && ctx.getParent() instanceof HqlParser.QueryExpressionContext qec) { if (qec.orderedQuery().indexOf(ctx) != 0) { return true; } } - ctx = parent; + + ctx = ctx.getParent(); } + return false; } 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 f9da5749ce..4dd74b1c2f 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 @@ -32,10 +32,10 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. * - * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch + * @author TaeHyun Kang * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode" }) @@ -44,19 +44,23 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { /** * Is this AST tree a {@literal subquery}? * - * @return boolean + * @return {@literal true} is the query is a subquery; {@literal false} otherwise. */ static boolean isSubquery(ParserRuleContext ctx) { while (ctx != null) { + if (ctx instanceof JpqlParser.SubqueryContext) { return true; } + if (ctx instanceof JpqlParser.Update_statementContext || ctx instanceof JpqlParser.Delete_statementContext) { return false; } + ctx = ctx.getParent(); } + return false; } @@ -68,11 +72,14 @@ static boolean isSubquery(ParserRuleContext ctx) { static boolean isSetQuery(ParserRuleContext ctx) { while (ctx != null) { + if (ctx instanceof JpqlParser.Set_fuctionContext) { return true; } + ctx = ctx.getParent(); } + return false; } From fc6f18a9018b0e29a248bef720e9af2bdfbe2c4f Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Fri, 8 Aug 2025 19:17:33 +0700 Subject: [PATCH 214/224] Remove unused imports. Signed-off-by: Tran Ngoc Nhan Closes #3968 --- .../data/jpa/convert/QueryByExamplePredicateBuilder.java | 1 - .../org/springframework/data/jpa/domain/AbstractAuditable.java | 1 - .../data/jpa/repository/query/EqlQueryIntrospector.java | 2 -- .../data/jpa/repository/query/JpaKeysetScrollQueryCreator.java | 2 -- .../data/jpa/repository/support/CrudMethodMetadata.java | 1 - .../jpa/repository/support/CrudMethodMetadataPostProcessor.java | 1 - 6 files changed, 8 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 69baedf0dd..10001ec8c8 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,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.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; 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 0b394d0472..1764eb86b3 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,7 +24,6 @@ import java.time.ZoneId; import java.util.Optional; -import org.jspecify.annotations.NullUnmarked; import org.springframework.data.domain.Auditable; import org.jspecify.annotations.Nullable; 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 8be9930b9f..71f65523b8 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 @@ -23,8 +23,6 @@ import org.jspecify.annotations.Nullable; -import org.springframework.data.jpa.repository.query.EqlParser.Range_variable_declarationContext; - /** * {@link ParsedQueryIntrospector} for EQL queries. * 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 e7252b510a..56ba95fa7e 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 @@ -23,8 +23,6 @@ import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; 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 6ac031cc56..34d05a6043 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 @@ -18,7 +18,6 @@ import jakarta.persistence.LockModeType; import java.lang.reflect.Method; -import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; 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 0a9a902e00..9f22d22546 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 @@ -20,7 +20,6 @@ import java.lang.reflect.Method; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; From cff0724afee4ba0f3fba0df32518c52d42262a92 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 21 Dec 2022 22:21:14 +0100 Subject: [PATCH 215/224] Improve query method validation exceptions for declared queries. When validating manually declared queries on repositories, the exception that captures the query to validate now actually also reports it in the exception message. Closes: #2736. Original pull request: #2738 --- .../jpa/repository/query/SimpleJpaQuery.java | 22 +++++++++++-------- .../query/SimpleJpaQueryUnitTests.java | 6 ++--- 2 files changed, 16 insertions(+), 12 deletions(-) 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 b042318b13..3c0410fbfb 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 @@ -48,11 +48,10 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery que super(method, em, query, countQuery, queryConfiguration); - validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", method); + validateQuery(getQuery(), "Validation failed for query %s for method %s", method); if (method.isPageQuery()) { - validateQuery(getCountQuery().getQueryString(), - String.format("Count query validation failed for method %s", method)); + validateQuery(getCountQuery(), "Count query %s validation failed for method %s", method); } } @@ -62,19 +61,24 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery que * @param query * @param errorMessage */ - private void validateQuery(String query, String errorMessage, Object... arguments) { + private void validateQuery(QueryProvider query, String errorMessage, JpaQueryMethod method) { if (getQueryMethod().isProcedureQuery()) { return; } - try (EntityManager validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager()) { - validatingEm.createQuery(query); - } catch (RuntimeException e) { + EntityManager validatingEm = null; + var queryString = query.getQueryString(); + + try { + validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager(); + validatingEm.createQuery(queryString); + + } 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); + // https://download.oracle.com/javaee-archive/jpa-spec.java.net/users/2012/07/0404.html + throw new IllegalArgumentException(errorMessage.formatted(query, method), e); } } } 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 a87cb3d4e3..064189a55e 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 @@ -41,7 +41,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -194,14 +193,15 @@ void doesNotValidateCountQueryIfNotPagingMethod() throws Exception { } @Test // DATAJPA-352 - @SuppressWarnings("unchecked") void validatesAndRejectsCountQueryIfPagingMethod() throws Exception { Method method = SampleRepository.class.getMethod("pageByAnnotatedQuery", Pageable.class); when(em.createQuery(Mockito.contains("count"))).thenThrow(IllegalArgumentException.class); - assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(method)).withMessageContaining("Count") + assertThatIllegalArgumentException() // + .isThrownBy(() -> createJpaQuery(method)) // + .withMessageContaining("Count") // .withMessageContaining(method.getName()); } From a15ebdbcbafde3f45efdc1e05a583d1876df2bed Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Sep 2025 09:07:54 +0200 Subject: [PATCH 216/224] Polishing. Refine error message format. Consistently use QueryCreationException. See #2736 Original pull request: #2738 --- .../query/AbstractStringBasedJpaQuery.java | 6 +++-- .../jpa/repository/query/JpaQueryMethod.java | 7 +++-- .../data/jpa/repository/query/NamedQuery.java | 4 +-- .../repository/query/PartTreeJpaQuery.java | 26 +++++++++---------- .../jpa/repository/query/SimpleJpaQuery.java | 21 +++++++-------- .../JpaQueryLookupStrategyUnitTests.java | 4 ++- .../PartTreeJpaQueryIntegrationTests.java | 23 +++++++--------- .../query/SimpleJpaQueryUnitTests.java | 10 +++---- 8 files changed, 50 insertions(+), 51 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 841eaffffd..09f54bbc7b 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 @@ -29,6 +29,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.jpa.repository.QueryRewriter; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -124,8 +125,9 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Decl } } - Assert.isTrue(method.isNativeQuery() || !this.query.usesJdbcStyleParameters(), - "JDBC style parameters (?) are not supported for JPA queries"); + if (!method.isNativeQuery() && this.query.usesJdbcStyleParameters()) { + throw QueryCreationException.create(method, "JDBC-style parameters (?) are not supported for JPA queries"); + } } @Override 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 10b985449d..219ce95791 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 @@ -43,6 +43,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.util.Lazy; @@ -148,8 +149,10 @@ public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFact this.metaAnnotation = Lazy .of(() -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Meta.class))); - Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()), - () -> String.format("Modifying method must not contain %s", Parameters.TYPES)); + if (isModifyingQuery() && getParameters().hasSpecialParameter()) { + throw QueryCreationException.create(this, + String.format("Modifying method must not contain %s", Parameters.TYPES)); + } } private static Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) { 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 9bfbb750ee..2eaaa0ef87 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 @@ -75,9 +75,9 @@ private NamedQuery(JpaQueryMethod method, EntityManager em, JpaQueryConfiguratio Parameters parameters = method.getParameters(); if (parameters.hasSortParameter()) { - throw new IllegalStateException(String.format("Query method %s is backed by a NamedQuery and must " + throw QueryCreationException.create(method, String.format("Query method is backed by a NamedQuery and must " + "not contain a sort parameter as we cannot modify the query; Use @%s(value=…) instead to apply sorting or remove the 'Sort' parameter.", - method, method.isNativeQuery() ? "NativeQuery" : "Query")); + method.isNativeQuery() ? "NativeQuery" : "Query")); } this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName); 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 06ee74cf02..d6d9cb19d8 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 @@ -38,6 +38,7 @@ import org.springframework.data.jpa.repository.query.JpaQueryExecution.ScrollExecution; import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.Part; @@ -102,13 +103,12 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { try { this.tree = new PartTree(method.getName(), domainClass); - validate(tree, parameters, method.toString()); + validate(tree, parameters); this.countQuery = new CountQueryPreparer(); this.queryPreparer = tree.isCountProjection() ? countQuery : new QueryPreparer(); } catch (Exception o_O) { - throw new IllegalArgumentException( - String.format("Failed to create query for method %s; %s", method, o_O.getMessage()), o_O); + throw QueryCreationException.create(getQueryMethod(), o_O); } } @@ -142,7 +142,7 @@ protected JpaQueryExecution getExecution(JpaParametersParameterAccessor accessor return super.getExecution(accessor); } - private static void validate(PartTree tree, JpaParameters parameters, String methodName) { + private static void validate(PartTree tree, JpaParameters parameters) { int argCount = 0; @@ -154,14 +154,14 @@ private static void validate(PartTree tree, JpaParameters parameters, String met for (int i = 0; i < numberOfArguments; i++) { - throwExceptionOnArgumentMismatch(methodName, part, parameters, argCount); + throwExceptionOnArgumentMismatch(part, parameters, argCount); argCount++; } } } - private static void throwExceptionOnArgumentMismatch(String methodName, Part part, JpaParameters parameters, + private static void throwExceptionOnArgumentMismatch(Part part, JpaParameters parameters, int index) { Type type = part.getType(); @@ -169,28 +169,28 @@ private static void throwExceptionOnArgumentMismatch(String methodName, Part par if (!parameters.getBindableParameters().hasParameterAt(index)) { throw new IllegalStateException(String.format( - "Method %s expects at least %d arguments but only found %d; This leaves an operator of type %s for property %s unbound", - methodName, index + 1, index, type.name(), property)); + "Method expects at least %d arguments but only found %d; This leaves an operator of type '%s' for property '%s' unbound", + index + 1, index, type.name(), property)); } JpaParameter parameter = parameters.getBindableParameter(index); if (expectsCollection(type)) { if (!parameterIsCollectionLike(parameter)) { - throw new IllegalStateException(wrongParameterTypeMessage(methodName, property, type, "Collection", parameter)); + throw new IllegalStateException(wrongParameterTypeMessage(property, type, "Collection", parameter)); } } else { if (!part.getProperty().isCollection() && !parameterIsScalarLike(parameter)) { - throw new IllegalStateException(wrongParameterTypeMessage(methodName, property, type, "scalar", parameter)); + throw new IllegalStateException(wrongParameterTypeMessage(property, type, "scalar", parameter)); } } } - private static String wrongParameterTypeMessage(String methodName, String property, Type operatorType, + private static String wrongParameterTypeMessage(String property, Type operatorType, String expectedArgumentType, JpaParameter parameter) { - return String.format("Operator %s on %s requires a %s argument, found %s in method %s", operatorType.name(), - property, expectedArgumentType, parameter.getType(), methodName); + return String.format("Operator '%s' on '%s' requires a %s argument, found '%s'", operatorType.name(), property, + expectedArgumentType, parameter.getType()); } private static boolean parameterIsCollectionLike(JpaParameter parameter) { 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 3c0410fbfb..4d1f5e7f44 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 @@ -20,6 +20,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.RepositoryQuery; /** @@ -48,10 +49,10 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, DeclaredQuery que super(method, em, query, countQuery, queryConfiguration); - validateQuery(getQuery(), "Validation failed for query %s for method %s", method); + validateQuery(getQuery(), "Query validation failed for '%s'", method); if (method.isPageQuery()) { - validateQuery(getCountQuery(), "Count query %s validation failed for method %s", method); + validateQuery(getCountQuery(), "Count query validation failed for '%s'", method); } } @@ -67,18 +68,14 @@ private void validateQuery(QueryProvider query, String errorMessage, JpaQueryMet return; } - EntityManager validatingEm = null; - var queryString = query.getQueryString(); - - try { - validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager(); + String queryString = query.getQueryString(); + try (EntityManager validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager()) { validatingEm.createQuery(queryString); - } catch (RuntimeException e) { - // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider - // https://download.oracle.com/javaee-archive/jpa-spec.java.net/users/2012/07/0404.html - throw new IllegalArgumentException(errorMessage.formatted(query, method), e); - } + // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider + // https://download.oracle.com/javaee-archive/jpa-spec.java.net/users/2012/07/0404.html + throw QueryCreationException.create(method, errorMessage.formatted(queryString), e); + } } } 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 e68faf4092..0c76c343c1 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 @@ -45,6 +45,7 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +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.RepositoryQuery; @@ -58,6 +59,7 @@ * @author Jens Schauder * @author Réda Housni Alaoui * @author Greg Turnquist + * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -160,7 +162,7 @@ void namedQueryWithSortShouldThrowIllegalStateException() throws NoSuchMethodExc Method method = UserRepository.class.getMethod("customNamedQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); - assertThatIllegalStateException() + assertThatExceptionOfType(QueryCreationException.class) .isThrownBy(() -> strategy.resolveQuery(method, metadata, projectionFactory, namedQueries)) .withMessageContaining( "is backed by a NamedQuery and must not contain a sort parameter as we cannot modify the query; Use @Query(value=…) instead to apply sorting or remove the 'Sort' parameter."); 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 02d63e6770..3b1fe54569 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 @@ -50,6 +50,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -192,8 +193,7 @@ void rejectsInPredicateWithNonIterableParameter() throws Exception { assertThatExceptionOfType(RuntimeException.class) // .isThrownBy(() -> new PartTreeJpaQuery(method, entityManager)) // - .withMessageContaining("findByIdIn") // - .withMessageContaining(" IN ") // + .withMessageContaining("'IN'") // .withMessageContaining("Collection") // .withMessageContaining("Integer"); } @@ -203,11 +203,10 @@ void rejectsOtherThanInPredicateWithIterableParameter() throws Exception { JpaQueryMethod method = getQueryMethod("findById", Collection.class); - assertThatExceptionOfType(RuntimeException.class) // + assertThatExceptionOfType(QueryCreationException.class) // .isThrownBy(() -> new PartTreeJpaQuery(method, entityManager)) // - .withMessageContaining("findById") // - .withMessageContaining(" SIMPLE_PROPERTY ") // - .withMessageContaining(" scalar ") // + .withMessageContaining("'SIMPLE_PROPERTY'") // + .withMessageContaining("scalar ") // .withMessageContaining("Collection"); } @@ -226,11 +225,9 @@ void errorsDueToMismatchOfParametersContainNameOfMethodInterfaceAndPropertyPath( JpaQueryMethod method = getQueryMethod("findByFirstname"); - assertThatExceptionOfType(IllegalArgumentException.class) // + assertThatExceptionOfType(QueryCreationException.class) // .isThrownBy(() -> new PartTreeJpaQuery(method, entityManager)) // - .withMessageContaining("findByFirstname") // the method being analyzed - .withMessageContaining(" firstname ") // the property we are looking for - .withMessageContaining("UserRepository"); // the repository + .withMessageContaining("'firstname'"); // the property we are looking for } @Test // DATAJPA-863 @@ -238,11 +235,9 @@ void errorsDueToMissingPropertyContainNameOfMethodAndInterface() throws Exceptio JpaQueryMethod method = getQueryMethod("findByNoSuchProperty", String.class); - assertThatExceptionOfType(IllegalArgumentException.class) // + assertThatExceptionOfType(QueryCreationException.class) // .isThrownBy(() -> new PartTreeJpaQuery(method, entityManager)) // - .withMessageContaining("findByNoSuchProperty") // the method being analyzed - .withMessageContaining("'noSuchProperty'") // the property we are looking for - .withMessageContaining("UserRepository"); // the repository + .withMessageContaining("'noSuchProperty'"); // the property we are looking for } @Test // GH-3356 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 064189a55e..6866e7c2d3 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 @@ -57,6 +57,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -192,17 +193,16 @@ void doesNotValidateCountQueryIfNotPagingMethod() throws Exception { createJpaQuery(method); } - @Test // DATAJPA-352 + @Test // DATAJPA-352, GH-2736 void validatesAndRejectsCountQueryIfPagingMethod() throws Exception { Method method = SampleRepository.class.getMethod("pageByAnnotatedQuery", Pageable.class); when(em.createQuery(Mockito.contains("count"))).thenThrow(IllegalArgumentException.class); - assertThatIllegalArgumentException() // + assertThatExceptionOfType(QueryCreationException.class) // .isThrownBy(() -> createJpaQuery(method)) // - .withMessageContaining("Count") // - .withMessageContaining(method.getName()); + .withMessageContaining("User u"); } @Test @@ -323,7 +323,7 @@ void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception { Method illegalMethod = SampleRepository.class.getMethod("illegalUseOfJdbcStyleParameters", String.class); - assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(illegalMethod)); + assertThatExceptionOfType(QueryCreationException.class).isThrownBy(() -> createJpaQuery(illegalMethod)); } @Test // GH-3929 From c0d18705e58e41bb872b0c25a071cc217d255d07 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 1 Oct 2025 09:37:09 +0200 Subject: [PATCH 217/224] Evaluate entity name for `StringAotQuery` from `EntityMetadata`. Closes #4029 Original pull request: #4030 --- .../jpa/repository/aot/QueriesFactory.java | 7 +- .../aot/QueriesFactoryUnitTests.java | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java 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 3d5bee6bf9..bf43d72f68 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 @@ -57,6 +57,7 @@ * Factory for {@link AotQueries}. * * @author Mark Paluch + * @author Christoph Strobl * @since 4.0 */ class QueriesFactory { @@ -123,7 +124,7 @@ public AotQueries createQueries(RepositoryInformation repositoryInformation, Ret QueryEnhancerSelector selector, MergedAnnotation query, JpaQueryMethod queryMethod) { if (query.isPresent() && StringUtils.hasText(query.getString("value"))) { - return buildStringQuery(repositoryInformation.getDomainType(), returnedType, selector, query, queryMethod); + return buildStringQuery(returnedType, selector, query, queryMethod); } String queryName = queryMethod.getNamedQueryName(); @@ -138,10 +139,10 @@ private boolean hasNamedQuery(ReturnedType returnedType, String queryName) { return namedQueries.hasQuery(queryName) || getNamedQuery(returnedType, queryName) != null; } - private AotQueries buildStringQuery(Class domainType, ReturnedType returnedType, QueryEnhancerSelector selector, + private AotQueries buildStringQuery(ReturnedType returnedType, QueryEnhancerSelector selector, MergedAnnotation query, JpaQueryMethod queryMethod) { - UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", domainType.getSimpleName()); + UnaryOperator operator = s -> s.replaceAll("#\\{#entityName}", queryMethod.getEntityInformation().getEntityName()); boolean isNative = query.getBoolean("nativeQuery"); Function queryFunction = isNative ? DeclaredQuery::nativeQuery : DeclaredQuery::jpqlQuery; queryFunction = operator.andThen(queryFunction); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java new file mode 100644 index 0000000000..dbd4dc472f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025-present 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 org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import jakarta.persistence.EntityManagerFactory; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.JpaEntityMetadata; +import org.springframework.data.jpa.repository.query.JpaQueryMethod; +import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.ReturnedType; + +/** + * Unit tests for {@link QueriesFactory}. + * + * @author Christoph Strobl + */ +class QueriesFactoryUnitTests { + + QueriesFactory factory; + + @BeforeEach + void setUp() { + + RepositoryConfigurationSource configSource = Mockito.mock(RepositoryConfigurationSource.class); + EntityManagerFactory entityManagerFactory = Mockito.mock(EntityManagerFactory.class); + + factory = new QueriesFactory(configSource, entityManagerFactory, this.getClass().getClassLoader()); + } + + @Test // GH-4029 + @SuppressWarnings({ "rawtypes", "unchecked" }) + void stringQueryShouldResolveEntityNameFromJakartaAnnotationIfPresent() { + + RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); + JpaEntityMetadata entityMetadata = Mockito.mock(JpaEntityInformation.class); + when(entityMetadata.getEntityName()).thenReturn("CustomNamed"); + + MergedAnnotation queryAnnotation = Mockito.mock(MergedAnnotation.class); + when(queryAnnotation.isPresent()).thenReturn(true); + when(queryAnnotation.getString(eq("value"))).thenReturn("select t from #{#entityName} t"); + when(queryAnnotation.getBoolean(eq("nativeQuery"))).thenReturn(false); + when(queryAnnotation.getString("countQuery")).thenReturn("select count(t) from #{#entityName} t"); + + JpaQueryMethod queryMethod = Mockito.mock(JpaQueryMethod.class); + when(queryMethod.getEntityInformation()).thenReturn((JpaEntityMetadata) entityMetadata); + + AotQueries generatedQueries = factory.createQueries(repositoryInformation, + ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()), + QueryEnhancerSelector.DEFAULT_SELECTOR, queryAnnotation, queryMethod); + + assertThat(generatedQueries.result()).asInstanceOf(InstanceOfAssertFactories.type(StringAotQuery.class)) + .extracting(StringAotQuery::getQueryString).isEqualTo("select t from CustomNamed t"); + assertThat(generatedQueries.count()).asInstanceOf(InstanceOfAssertFactories.type(StringAotQuery.class)) + .extracting(StringAotQuery::getQueryString).isEqualTo("select count(t) from CustomNamed t"); + } +} From 7a66d6572768aef93dbfcf4f239fe3e9dcd1ad9f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 2 Oct 2025 10:21:43 +0200 Subject: [PATCH 218/224] Polishing. Simplify tests. See #4029 Original pull request: #4030 --- .../aot/QueriesFactoryUnitTests.java | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java index dbd4dc472f..0881714f23 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/QueriesFactoryUnitTests.java @@ -15,31 +15,38 @@ */ package org.springframework.data.jpa.repository.aot; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.InstanceOfAssertFactories.*; +import static org.mockito.Mockito.*; +import jakarta.persistence.Entity; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Id; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; -import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.core.annotation.MergedAnnotation; + +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.query.JpaEntityMetadata; import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.QueryEnhancerSelector; -import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.config.AotRepositoryInformation; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; /** * Unit tests for {@link QueriesFactory}. * * @author Christoph Strobl + * @author Mark Paluch */ class QueriesFactoryUnitTests { @@ -48,36 +55,42 @@ class QueriesFactoryUnitTests { @BeforeEach void setUp() { - RepositoryConfigurationSource configSource = Mockito.mock(RepositoryConfigurationSource.class); - EntityManagerFactory entityManagerFactory = Mockito.mock(EntityManagerFactory.class); + RepositoryConfigurationSource configSource = mock(RepositoryConfigurationSource.class); + EntityManagerFactory entityManagerFactory = mock(EntityManagerFactory.class); factory = new QueriesFactory(configSource, entityManagerFactory, this.getClass().getClassLoader()); } @Test // GH-4029 - @SuppressWarnings({ "rawtypes", "unchecked" }) - void stringQueryShouldResolveEntityNameFromJakartaAnnotationIfPresent() { - - RepositoryInformation repositoryInformation = Mockito.mock(RepositoryInformation.class); - JpaEntityMetadata entityMetadata = Mockito.mock(JpaEntityInformation.class); - when(entityMetadata.getEntityName()).thenReturn("CustomNamed"); + void stringQueryShouldResolveEntityNameFromJakartaAnnotationIfPresent() throws NoSuchMethodException { - MergedAnnotation queryAnnotation = Mockito.mock(MergedAnnotation.class); - when(queryAnnotation.isPresent()).thenReturn(true); - when(queryAnnotation.getString(eq("value"))).thenReturn("select t from #{#entityName} t"); - when(queryAnnotation.getBoolean(eq("nativeQuery"))).thenReturn(false); - when(queryAnnotation.getString("countQuery")).thenReturn("select count(t) from #{#entityName} t"); + RepositoryInformation repositoryInformation = new AotRepositoryInformation( + AbstractRepositoryMetadata.getMetadata(MyRepository.class), MyRepository.class, Collections.emptyList()); - JpaQueryMethod queryMethod = Mockito.mock(JpaQueryMethod.class); - when(queryMethod.getEntityInformation()).thenReturn((JpaEntityMetadata) entityMetadata); + Method method = MyRepository.class.getMethod("someFind"); + JpaQueryMethod queryMethod = new JpaQueryMethod(method, repositoryInformation, + new SpelAwareProxyProjectionFactory(), mock(QueryExtractor.class)); AotQueries generatedQueries = factory.createQueries(repositoryInformation, - ReturnedType.of(Object.class, Object.class, new SpelAwareProxyProjectionFactory()), - QueryEnhancerSelector.DEFAULT_SELECTOR, queryAnnotation, queryMethod); + queryMethod.getResultProcessor().getReturnedType(), QueryEnhancerSelector.DEFAULT_SELECTOR, + MergedAnnotations.from(method).get(Query.class), queryMethod); - assertThat(generatedQueries.result()).asInstanceOf(InstanceOfAssertFactories.type(StringAotQuery.class)) + assertThat(generatedQueries.result()).asInstanceOf(type(StringAotQuery.class)) .extracting(StringAotQuery::getQueryString).isEqualTo("select t from CustomNamed t"); - assertThat(generatedQueries.count()).asInstanceOf(InstanceOfAssertFactories.type(StringAotQuery.class)) + assertThat(generatedQueries.count()).asInstanceOf(type(StringAotQuery.class)) .extracting(StringAotQuery::getQueryString).isEqualTo("select count(t) from CustomNamed t"); } + + interface MyRepository extends Repository { + + @Query("select t from #{#entityName} t") + Collection someFind(); + } + + @Entity(name = "CustomNamed") + static class MyEntity { + + @Id Long id; + + } } From 472a359e221b8575811fbf5d53f0b0944524dc32 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 Oct 2025 11:36:22 +0200 Subject: [PATCH 219/224] Revise `PredicateSpecification` for improved reuse. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept From instead of Root and apply the PredicateSpecification type parameter to From's target type. Dropping the type reduces our assumptions, it allows also for improved PredicateSpecification reuse without requiring a specific design choice of whether the PredicateSpecification applies to Root directly or a different From (Join, Embeddable, …). Closes #4035 --- .../jpa/domain/PredicateSpecification.java | 26 +++++++++++-------- .../jpa/domain/SpecificationComposition.java | 5 ++-- .../PredicateSpecificationUnitTests.java | 3 ++- 3 files changed, 20 insertions(+), 14 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 de5a0ecdf0..318cf7c580 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 @@ -16,8 +16,8 @@ package org.springframework.data.jpa.domain; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; import java.io.Serializable; import java.util.Arrays; @@ -34,12 +34,16 @@ *

        * 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)}. + * {@link #allOf(Iterable)} with reduced type interference of the query source type. + *

        + * PredicateSpecifications are building blocks for composition and do not express their type opinion towards a specific + * entity source or join source type for improved reuse. *

        * Composition considers whether one or more specifications contribute to the overall predicate by returning a * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * + * @param the type of the {@link From From target} to which the specification is applied. * @author Mark Paluch * @author Peter Aisher * @since 4.0 @@ -57,17 +61,17 @@ public interface PredicateSpecification extends Serializable { * not(unrestricted()) // equivalent to `unrestricted()` *

        * - * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. + * @param the type of the {@link From} the resulting {@literal PredicateSpecification} operates on. * @return guaranteed to be not {@literal null}. */ static PredicateSpecification unrestricted() { - return (root, builder) -> null; + return (from, 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 the type of the {@link From} the resulting {@literal PredicateSpecification} operates on. * @param spec must not be {@literal null}. * @return guaranteed to be not {@literal null}. * @since 2.0 @@ -112,7 +116,7 @@ default PredicateSpecification or(PredicateSpecification other) { /** * Negates the given {@link PredicateSpecification}. * - * @param the type of the {@link Root} the resulting {@literal PredicateSpecification} operates on. + * @param the type of the {@link From} the resulting {@literal PredicateSpecification} operates on. * @param spec can be {@literal null}. * @return guaranteed to be not {@literal null}. */ @@ -120,9 +124,9 @@ static PredicateSpecification not(PredicateSpecification spec) { Assert.notNull(spec, "Specification must not be null"); - return (root, builder) -> { + return (from, builder) -> { - Predicate predicate = spec.toPredicate(root, builder); + Predicate predicate = spec.toPredicate(from, builder); return predicate != null ? builder.not(predicate) : null; }; } @@ -187,13 +191,13 @@ static PredicateSpecification anyOf(Iterable> s /** * 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}. + * {@link From} and {@link CriteriaBuilder}. * - * @param root must not be {@literal null}. + * @param from 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); + Predicate toPredicate(From from, 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 48dfd1a141..3b6c9e1273 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 @@ -19,6 +19,7 @@ import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -127,9 +128,9 @@ static PredicateSpecification composed(PredicateSpecification lhs, Pre }; } - private static @Nullable Predicate toPredicate(@Nullable PredicateSpecification specification, Root root, + private static @Nullable Predicate toPredicate(@Nullable PredicateSpecification specification, From from, CriteriaBuilder builder) { - return specification == null ? null : specification.toPredicate(root, builder); + return specification == null ? null : specification.toPredicate(from, builder); } } 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 0bce2b5a2c..797aaaea2f 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 @@ -20,6 +20,7 @@ import static org.springframework.util.SerializationUtils.*; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -169,7 +170,7 @@ void notWithNullPredicate() { static class SerializableSpecification implements Serializable, PredicateSpecification { @Override - public Predicate toPredicate(Root root, CriteriaBuilder cb) { + public Predicate toPredicate(From root, CriteriaBuilder cb) { return null; } } From 4adf2e33dccebcc03ce54033c21b5f780fb1f1c7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 Oct 2025 11:37:04 +0200 Subject: [PATCH 220/224] Polishing. Add type parameters to Javadoc, remove final keywords, rewording. See #4035 --- .../data/jpa/domain/DeleteSpecification.java | 1 + .../data/jpa/domain/Specification.java | 1 + .../data/jpa/domain/UpdateSpecification.java | 1 + .../data/jpa/domain/sample/UserSpecifications.java | 14 +++++++------- 4 files changed, 10 insertions(+), 7 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 ea66edd6a7..598797a984 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 @@ -41,6 +41,7 @@ * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * + * @param the type of the {@link Root entity} to which the specification is applied. * @author Mark Paluch * @author Peter Aisher * @since 4.0 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 551ffb5367..4e17dba2d5 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 @@ -41,6 +41,7 @@ * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * + * @param the type of the {@link Root entity} to which the specification is applied. * @author Oliver Gierke * @author Thomas Darimont * @author Krzysztof Rzymkowski 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 eab5275ba1..7299b5abc5 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 @@ -41,6 +41,7 @@ * {@link Predicate} or {@literal null}. Specifications returning {@literal null}, such as {@link #unrestricted()}, are * considered to not contribute to the overall predicate, and their result is not considered in the final predicate. * + * @param the type of the {@link Root entity} to which the specification is applied. * @author Mark Paluch * @author Peter Aisher * @since 4.0 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 cbd8ffd410..bd65c8403c 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 @@ -26,27 +26,27 @@ */ public class UserSpecifications { - public static PredicateSpecification userHasFirstname(final String firstname) { + public static PredicateSpecification userHasFirstname(String firstname) { return simplePropertySpec("firstname", firstname); } - public static PredicateSpecification userHasLastname(final String lastname) { + public static PredicateSpecification userHasLastname(String lastname) { return simplePropertySpec("lastname", lastname); } - public static PredicateSpecification userHasFirstnameLike(final String expression) { + public static PredicateSpecification userHasFirstnameLike(String expression) { return (root, cb) -> cb.like(root.get("firstname").as(String.class), String.format("%%%s%%", expression)); } - public static PredicateSpecification userHasAgeLess(final Integer age) { + public static PredicateSpecification userHasAgeLess(Integer age) { return (root, cb) -> cb.lessThan(root.get("age").as(Integer.class), age); } - public static Specification userHasLastnameLikeWithSort(final String expression) { + public static Specification userHasLastnameLikeWithSort(String expression) { return (root, query, cb) -> { @@ -56,8 +56,8 @@ public static Specification userHasLastnameLikeWithSort(final String expre }; } - private static PredicateSpecification simplePropertySpec(final String property, final Object value) { + private static PredicateSpecification simplePropertySpec(String property, Object value) { - return (root, builder) -> builder.equal(root.get(property), value); + return (from, builder) -> builder.equal(from.get(property), value); } } From 596e07ea59bf8e1b679998bac1105f8c70a0a04c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 Oct 2025 12:15:44 +0200 Subject: [PATCH 221/224] Consider EntityName in JpqlQueryBuilder. Closes #4036 --- .../query/DefaultJpaEntityMetadata.java | 9 +++++- .../repository/query/JpqlQueryBuilder.java | 30 +++++++++---------- .../query/JpqlQueryBuilderUnitTests.java | 14 +++++++-- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java index daa30cbb87..63d6c8ae01 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java @@ -17,6 +17,8 @@ import jakarta.persistence.Entity; +import java.util.function.Function; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -49,8 +51,13 @@ public Class getJavaType() { @Override public String getEntityName() { + return getEntityNameOr(Class::getSimpleName); + } + + String getEntityNameOr(Function, String> alternative) { Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class); - return null != entity && StringUtils.hasText(entity.name()) ? entity.name() : domainType.getSimpleName(); + return null != entity && StringUtils.hasText(entity.name()) ? entity.name() : alternative.apply(domainType); } + } 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 c41bb8d25b..907b3fc7ef 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 @@ -58,7 +58,8 @@ private JpqlQueryBuilder() {} * @return */ public static Entity entity(Class from) { - return new Entity(from.getName(), from.getSimpleName(), + DefaultJpaEntityMetadata entityMetadata = new DefaultJpaEntityMetadata<>(from); + return new Entity(from.getName(), entityMetadata.getEntityNameOr(Class::getName), getAlias(from.getSimpleName(), Predicates.isTrue(), () -> "r")); } @@ -538,7 +539,8 @@ 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())); + PropertyPath from = PropertyPath.from(path, + ClassUtils.forName(entity.className, Entity.class.getClassLoader())); return new PathAndOrigin(from, entity, false); } catch (ClassNotFoundException e) { throw new RuntimeException(e); @@ -847,7 +849,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.getEntity(), entity.getAlias())); + "SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.getName(), entity.getAlias())); if (getWhere() != null) { where.append(" WHERE ").append(getWhere().render(renderContext)); @@ -1024,28 +1026,24 @@ public interface Origin { */ public static final class Entity implements Origin { + private final String className; private final String entity; - private final String simpleName; private final String alias; /** - * @param entity fully-qualified entity name. - * @param simpleName simple class name. + * @param className fully-qualified entity name. + * @param entity entity name (as in {@code @Entity(…)}). * @param alias alias to use. */ - Entity(String entity, String simpleName, String alias) { + Entity(String className, String entity, String alias) { + this.className = className; this.entity = entity; - this.simpleName = simpleName; this.alias = alias; } - public String getEntity() { - return entity; - } - @Override public String getName() { - return simpleName; + return entity; } public String getAlias() { @@ -1061,18 +1059,18 @@ public boolean equals(Object obj) { return false; } var that = (Entity) obj; - return Objects.equals(this.entity, that.entity) && Objects.equals(this.simpleName, that.simpleName) + return Objects.equals(this.entity, that.entity) && Objects.equals(this.className, that.className) && Objects.equals(this.alias, that.alias); } @Override public int hashCode() { - return Objects.hash(entity, simpleName, alias); + return Objects.hash(entity, className, alias); } @Override public String toString() { - return "Entity[" + "entity=" + entity + ", " + "simpleName=" + simpleName + ", " + "alias=" + alias + ']'; + return "Entity[" + "entity=" + entity + ", " + "className=" + className + ", " + "alias=" + alias + ']'; } } 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 fde8dd493d..fa6cde1bc9 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 @@ -72,8 +72,16 @@ void entity() { Entity entity = JpqlQueryBuilder.entity(Order.class); assertThat(entity.getAlias()).isEqualTo("o"); - assertThat(entity.getEntity()).isEqualTo(Order.class.getName()); - assertThat(entity.getName()).isEqualTo(Order.class.getSimpleName()); + assertThat(entity.getName()).isEqualTo(Order.class.getName()); + } + + @Test // GH-4032 + void considersEntityName() { + + Entity entity = JpqlQueryBuilder.entity(Product.class); + + assertThat(entity.getAlias()).isEqualTo("p"); + assertThat(entity.getName()).isEqualTo("my_product"); } @Test // GH-3588 @@ -266,7 +274,7 @@ static class Person { String name; } - @jakarta.persistence.Entity + @jakarta.persistence.Entity(name = "my_product") static class Product { @Id Long id; From 5af0c40f538703ab94d07237ae9529f6b89ca18a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 Oct 2025 15:47:08 +0200 Subject: [PATCH 222/224] Consistently use JPA metamodel to determine entity name. We now use JpaMetamodelEntityMetadata where possible to determine the entity name. Previously, we defaulted in many places to DefaultJpaEntityMetadata not considering XML-based entity names and also using improper unqualifying of entity names that lead to usage of Class.getSimpleName() without considering inner class prefixes. While this commit introduces a consistent scheme, we still have to resolve some package tangles and improve our design as entity information duplicates parts of JpaPersistentEntity and causing duplicate introspections for JpaEntityInformationSupport. We will have to revisit the design with #4037. Closes #4032 --- .../aot/JpaRepositoryContributor.java | 64 ++++++--- .../jpa/repository/aot/QueriesFactory.java | 15 +- .../query/DefaultJpaEntityMetadata.java | 20 ++- .../query/JpaCountQueryCreator.java | 15 +- .../query/JpaKeysetScrollQueryCreator.java | 7 +- .../query/JpaMetamodelEntityMetadata.java | 53 +++++++ .../jpa/repository/query/JpaQueryCreator.java | 9 +- .../repository/query/JpqlQueryBuilder.java | 45 ++---- .../query/KeysetScrollSpecification.java | 9 +- .../repository/query/PartTreeJpaQuery.java | 22 +-- .../support/JpaEntityInformationSupport.java | 46 +++++- .../JpaMetamodelEntityInformation.java | 27 +++- .../JpaPersistableEntityInformation.java | 20 ++- ...positoryContributorConfigurationTests.java | 2 +- ...JpaRepositoryMetadataIntegrationTests.java | 2 +- .../JpaKeysetScrollQueryCreatorTests.java | 2 +- .../query/JpaQueryCreatorTests.java | 136 ++++++++++-------- .../query/JpqlQueryBuilderUnitTests.java | 33 +++-- .../JpaEntityInformationSupportUnitTests.java | 4 +- 19 files changed, 369 insertions(+), 162 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaMetamodelEntityMetadata.java 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 0bd50476f9..564fb50ef6 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 @@ -17,12 +17,14 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnitUtil; import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.spi.PersistenceUnitInfo; import java.lang.reflect.Method; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -32,22 +34,28 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; 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.JpaEntityMetadata; 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.jpa.repository.support.JpaEntityInformationSupport; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.aot.generate.AotRepositoryClassBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; 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.RepositoryMetadata; 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.util.TypeInformation; @@ -70,6 +78,7 @@ public class JpaRepositoryContributor extends RepositoryContributor { private final Metamodel metamodel; + private final PersistenceUnitUtil persistenceUnitUtil; private final PersistenceProvider persistenceProvider; private final QueriesFactory queriesFactory; private final EntityGraphLookup entityGraphLookup; @@ -88,28 +97,24 @@ public JpaRepositoryContributor(AotRepositoryContext repositoryContext, Persiste } public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) { - this(repositoryContext, entityManagerFactory.getMetamodel(), - PersistenceProvider.fromEntityManagerFactory(entityManagerFactory), - new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory, - repositoryContext.getRequiredClassLoader()), - new EntityGraphLookup(entityManagerFactory)); + this(repositoryContext, entityManagerFactory, entityManagerFactory.getMetamodel()); } private JpaRepositoryContributor(AotRepositoryContext repositoryContext, AotMetamodel metamodel) { - this(repositoryContext, metamodel, - PersistenceProvider.fromEntityManagerFactory(metamodel.getEntityManagerFactory()), - new QueriesFactory(repositoryContext.getConfigurationSource(), metamodel.getEntityManagerFactory(), - repositoryContext.getRequiredClassLoader()), - new EntityGraphLookup(metamodel.getEntityManagerFactory())); + this(repositoryContext, metamodel.getEntityManagerFactory(), metamodel); } - private JpaRepositoryContributor(AotRepositoryContext repositoryContext, Metamodel metamodel, - PersistenceProvider persistenceProvider, QueriesFactory queriesFactory, EntityGraphLookup entityGraphLookup) { + private JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory, + Metamodel metamodel) { + super(repositoryContext); + this.metamodel = metamodel; - this.persistenceProvider = persistenceProvider; - this.queriesFactory = queriesFactory; - this.entityGraphLookup = entityGraphLookup; + this.persistenceUnitUtil = entityManagerFactory.getPersistenceUnitUtil(); + this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory); + this.queriesFactory = new QueriesFactory(repositoryContext.getConfigurationSource(), entityManagerFactory, + repositoryContext.getRequiredClassLoader()); + this.entityGraphLookup = new EntityGraphLookup(entityManagerFactory); this.context = repositoryContext; } @@ -159,8 +164,10 @@ private Optional> getQueryEnhancerSelectorClass() { @Override protected @Nullable MethodContributor contributeQueryMethod(Method method) { - JpaQueryMethod queryMethod = new JpaQueryMethod(method, getRepositoryInformation(), getProjectionFactory(), - persistenceProvider); + JpaEntityMetadata entityInformation = JpaEntityInformationSupport + .getEntityInformation(getRepositoryInformation().getDomainType(), metamodel, persistenceUnitUtil); + AotJpaQueryMethod queryMethod = new AotJpaQueryMethod(method, getRepositoryInformation(), entityInformation, + getProjectionFactory(), persistenceProvider, JpaParameters::new); Optional> queryEnhancerSelectorClass = getQueryEnhancerSelectorClass(); QueryEnhancerSelector selector = queryEnhancerSelectorClass.map(BeanUtils::instantiateClass) @@ -271,4 +278,27 @@ public Map serialize() { } } + /** + * AOT extension to {@link JpaQueryMethod} providing a metamodel backed {@link JpaEntityMetadata} object. + */ + static class AotJpaQueryMethod extends JpaQueryMethod { + + private final JpaEntityMetadata entityMetadata; + + public AotJpaQueryMethod(Method method, RepositoryMetadata metadata, JpaEntityMetadata entityMetadata, + ProjectionFactory factory, QueryExtractor extractor, + Function parametersFunction) { + + super(method, metadata, factory, extractor, parametersFunction); + + this.entityMetadata = entityMetadata; + } + + @Override + public JpaEntityMetadata getEntityInformation() { + return this.entityMetadata; + } + + } + } 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 bf43d72f68..9a764c255c 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 @@ -271,7 +271,8 @@ private AotQueries buildPartTreeQuery(RepositoryInformation repositoryInformatio MergedAnnotation query, JpaQueryMethod queryMethod) { PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); - AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates); + AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates, + queryMethod.getEntityInformation()); if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) { return AotQueries.from(aotQuery, StringAotQuery.of(DeclaredQuery.jpqlQuery(query.getString("countQuery")))); @@ -282,27 +283,28 @@ private AotQueries buildPartTreeQuery(RepositoryInformation repositoryInformatio createNamedAotQuery(returnedType, selector, queryMethod.getNamedCountQueryName(), queryMethod, false)); } - AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates); + AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates, + queryMethod.getEntityInformation()); return AotQueries.from(aotQuery, partTreeCountQuery); } private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters, - JpqlQueryTemplates templates) { + JpqlQueryTemplates templates, JpaEntityMetadata entityMetadata) { ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, escapeCharacter, templates); JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, false, returnedType, metadataProvider, templates, - metamodel); + entityMetadata, 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) { + JpqlQueryTemplates templates, JpaEntityMetadata entityMetadata) { ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, escapeCharacter, templates); JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates, - metamodel); + entityMetadata, metamodel); return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), Limit.unlimited(), false, false); @@ -333,7 +335,6 @@ private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, return returnedType.getReturnedType(); } - return result; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java index 63d6c8ae01..429b499a43 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultJpaEntityMetadata.java @@ -19,6 +19,8 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -32,6 +34,7 @@ public class DefaultJpaEntityMetadata implements JpaEntityMetadata { private final Class domainType; + private final @Nullable Entity entity; /** * Creates a new {@link DefaultJpaEntityMetadata} for the given domain type. @@ -41,7 +44,9 @@ public class DefaultJpaEntityMetadata implements JpaEntityMetadata { public DefaultJpaEntityMetadata(Class domainType) { Assert.notNull(domainType, "Domain type must not be null"); + this.domainType = domainType; + this.entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class); } @Override @@ -51,13 +56,20 @@ public Class getJavaType() { @Override public String getEntityName() { - return getEntityNameOr(Class::getSimpleName); + return getEntityNameOr(DefaultJpaEntityMetadata::unqualify); } - String getEntityNameOr(Function, String> alternative) { + private String getEntityNameOr(Function, String> alternative) { + return (entity != null && StringUtils.hasText(entity.name())) ? entity.name() : alternative.apply(domainType); + } + + static String unqualify(Class clazz) { + return unqualify(clazz.getName()); + } - Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class); - return null != entity && StringUtils.hasText(entity.name()) ? entity.name() : alternative.apply(domainType); + static String unqualify(String qualifiedName) { + int loc = qualifiedName.lastIndexOf('.'); + return loc < 0 ? qualifiedName : qualifiedName.substring(loc + 1); } } 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 b95e272b1c..6293c14077 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 @@ -34,7 +34,6 @@ public class JpaCountQueryCreator extends JpaQueryCreator { private final boolean distinct; - private final ReturnedType returnedType; /** * Creates a new {@link JpaCountQueryCreator} @@ -51,7 +50,6 @@ public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterM super(tree, returnedType, provider, templates, em.getMetamodel()); this.distinct = tree.isDistinct(); - this.returnedType = returnedType; } /** @@ -69,12 +67,21 @@ public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterM super(tree, returnedType, provider, templates, metamodel); this.distinct = tree.isDistinct(); - this.returnedType = returnedType; + } + + public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider, + JpqlQueryTemplates templates, JpaEntityMetadata entityMetadata, Metamodel metamodel) { + + super(tree, false, returnedType, provider, templates, entityMetadata, metamodel); + + this.distinct = tree.isDistinct(); } @Override protected JpqlQueryBuilder.Select buildQuery(Sort sort) { - JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(returnedType.getDomainType()); + + JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(getEntity()); + if (this.distinct) { selectStep = selectStep.distinct(); } 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 56ba95fa7e..d8050137f0 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 @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.query; import jakarta.persistence.EntityManager; +import jakarta.persistence.metamodel.Metamodel; import java.util.ArrayList; import java.util.Collection; @@ -40,6 +41,7 @@ */ class JpaKeysetScrollQueryCreator extends JpaQueryCreator { + private final Metamodel metamodel; private final JpaEntityInformation entityInformation; private final KeysetScrollPosition scrollPosition; private final ParameterMetadataProvider provider; @@ -49,8 +51,9 @@ public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, ParameterMe JpqlQueryTemplates templates, JpaEntityInformation entityInformation, KeysetScrollPosition scrollPosition, EntityManager em) { - super(tree, type, provider, templates, em.getMetamodel()); + super(tree, false, type, provider, templates, entityInformation, em.getMetamodel()); + this.metamodel = em.getMetamodel(); this.entityInformation = entityInformation; this.scrollPosition = scrollPosition; this.provider = provider; @@ -76,7 +79,7 @@ protected JpqlQueryBuilder.AbstractJpqlQuery createQuery(JpqlQueryBuilder.@Nulla JpqlQueryBuilder.Select query = buildQuery(keysetSpec.sort()); Map> cachedBindings = new LinkedHashMap<>(); - JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(getFrom(), getEntity(), + JpqlQueryBuilder.Predicate keysetPredicate = keysetSpec.createJpqlPredicate(metamodel, getFrom(), getEntity(), (property, value) -> { Map bindings = cachedBindings.computeIfAbsent(property, k -> new LinkedHashMap<>()); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaMetamodelEntityMetadata.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaMetamodelEntityMetadata.java new file mode 100644 index 0000000000..ed80223d3a --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaMetamodelEntityMetadata.java @@ -0,0 +1,53 @@ +/* + * 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.metamodel.EntityType; + +import org.springframework.util.Assert; + +/** + * Metamodel-based implementation for {@link JpaEntityMetadata}. + * + * @author Mark Paluch + * @since 4.0 + */ +public class JpaMetamodelEntityMetadata implements JpaEntityMetadata { + + private final EntityType entityType; + + /** + * Creates a new {@link JpaMetamodelEntityMetadata} for the given domain type. + * + * @param entityType must not be {@literal null}. + */ + public JpaMetamodelEntityMetadata(EntityType entityType) { + + Assert.notNull(entityType, "Entity type must not be null"); + this.entityType = entityType; + } + + @Override + public Class getJavaType() { + return entityType.getJavaType(); + } + + @Override + public String getEntityName() { + return entityType.getName(); + } + +} 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 a154585d3c..0624bc2421 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 @@ -115,8 +115,15 @@ public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvid this(tree, false, type, provider, templates, metamodel); } + @SuppressWarnings({ "rawtypes", "unchecked" }) public JpaQueryCreator(PartTree tree, boolean searchQuery, ReturnedType type, ParameterMetadataProvider provider, JpqlQueryTemplates templates, Metamodel metamodel) { + this(tree, searchQuery, type, provider, templates, + new JpaMetamodelEntityMetadata(metamodel.entity(type.getDomainType())), metamodel); + } + + public JpaQueryCreator(PartTree tree, boolean searchQuery, ReturnedType type, ParameterMetadataProvider provider, + JpqlQueryTemplates templates, JpaEntityMetadata entityMetadata, Metamodel metamodel) { super(tree); @@ -144,7 +151,7 @@ public JpaQueryCreator(PartTree tree, boolean searchQuery, ReturnedType type, Pa this.templates = templates; this.escape = provider.getEscape(); this.entityType = metamodel.entity(type.getDomainType()); - this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType()); + this.entity = JpqlQueryBuilder.entity(entityMetadata); this.metamodel = metamodel; this.similarityNormalizer = provider.getSimilarityNormalizer(); } 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 907b3fc7ef..3cc0c764b6 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 @@ -36,7 +36,6 @@ 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; import org.springframework.util.StringUtils; @@ -52,15 +51,14 @@ public final class JpqlQueryBuilder { private JpqlQueryBuilder() {} /** - * Create an {@link Entity} from the given {@link Class entity class}. + * Create an {@link Entity} from the given {@link JpaEntityMetadata}. * * @param from the entity type to select from. * @return */ - public static Entity entity(Class from) { - DefaultJpaEntityMetadata entityMetadata = new DefaultJpaEntityMetadata<>(from); - return new Entity(from.getName(), entityMetadata.getEntityNameOr(Class::getName), - getAlias(from.getSimpleName(), Predicates.isTrue(), () -> "r")); + public static Entity entity(JpaEntityMetadata from) { + return new Entity(from.getJavaType(), from.getEntityName(), + getAlias(from.getJavaType().getSimpleName(), Predicates.isTrue(), () -> "r")); } /** @@ -85,17 +83,6 @@ 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}. * @@ -538,13 +525,8 @@ static PathAndOrigin path(Origin origin, String path) { if (origin instanceof Entity entity) { - try { - PropertyPath from = PropertyPath.from(path, - ClassUtils.forName(entity.className, Entity.class.getClassLoader())); - return new PathAndOrigin(from, entity, false); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } + PropertyPath from = PropertyPath.from(path, entity.entityClass); + return new PathAndOrigin(from, entity, false); } if (origin instanceof Join join) { @@ -1026,17 +1008,17 @@ public interface Origin { */ public static final class Entity implements Origin { - private final String className; + private final Class entityClass; private final String entity; private final String alias; /** - * @param className fully-qualified entity name. + * @param entityClass entity class. * @param entity entity name (as in {@code @Entity(…)}). * @param alias alias to use. */ - Entity(String className, String entity, String alias) { - this.className = className; + Entity(Class entityClass, String entity, String alias) { + this.entityClass = entityClass; this.entity = entity; this.alias = alias; } @@ -1059,18 +1041,19 @@ public boolean equals(Object obj) { return false; } var that = (Entity) obj; - return Objects.equals(this.entity, that.entity) && Objects.equals(this.className, that.className) + return Objects.equals(this.entity, that.entity) && Objects.equals(this.entityClass, that.entityClass) && Objects.equals(this.alias, that.alias); } @Override public int hashCode() { - return Objects.hash(entity, className, alias); + return Objects.hash(entity, entityClass, alias); } @Override public String toString() { - return "Entity[" + "entity=" + entity + ", " + "className=" + className + ", " + "alias=" + alias + ']'; + return "Entity[" + "entity=" + entity + ", " + "className=" + entityClass.getName() + ", " + "alias=" + alias + + ']'; } } 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 76b3ed0a29..80986a5e5e 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 @@ -79,11 +79,12 @@ 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, + public JpqlQueryBuilder.@Nullable Predicate createJpqlPredicate(Metamodel metamodel, Bindable from, + JpqlQueryBuilder.Entity entity, ParameterFactory factory) { KeysetScrollDelegate delegate = KeysetScrollDelegate.of(position.getDirection()); - return delegate.createPredicate(position, sort, new JpqlStrategy(null, from, entity, factory)); + return delegate.createPredicate(position, sort, new JpqlStrategy(metamodel, from, entity, factory)); } @SuppressWarnings("rawtypes") @@ -137,9 +138,9 @@ private static class JpqlStrategy implements QueryStrategy from; private final JpqlQueryBuilder.Entity entity; private final ParameterFactory factory; - private final @Nullable Metamodel metamodel; + private final Metamodel metamodel; - public JpqlStrategy(@Nullable Metamodel metamodel, Bindable from, JpqlQueryBuilder.Entity entity, + public JpqlStrategy(Metamodel metamodel, Bindable from, JpqlQueryBuilder.Entity entity, ParameterFactory factory) { this.from = from; 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 d6d9cb19d8..9d5c42c572 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 @@ -16,7 +16,6 @@ package org.springframework.data.jpa.repository.query; import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceUnitUtil; import jakarta.persistence.Query; import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; @@ -36,7 +35,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.support.JpaMetamodelEntityInformation; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.ResultProcessor; @@ -44,6 +44,7 @@ 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.data.util.Lazy; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -69,7 +70,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { private final QueryPreparer countQuery; private final EntityManager em; private final EscapeCharacter escape; - private final JpaMetamodelEntityInformation entityInformation; + private final Lazy> entityInformation; /** * Creates a new {@link PartTreeJpaQuery}. @@ -97,8 +98,7 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { this.parameters = method.getParameters(); Class domainClass = method.getEntityInformation().getJavaType(); - PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil(); - this.entityInformation = new JpaMetamodelEntityInformation<>(domainClass, em.getMetamodel(), persistenceUnitUtil); + this.entityInformation = Lazy.of(() -> JpaEntityInformationSupport.getEntityInformation(domainClass, em)); try { @@ -132,7 +132,7 @@ public TypedQuery doCreateCountQuery(JpaParametersParameterAccessor access protected JpaQueryExecution getExecution(JpaParametersParameterAccessor accessor) { if (this.getQueryMethod().isScrollQuery()) { - return new ScrollExecution(this.tree.getSort(), new ScrollDelegate<>(entityInformation)); + return new ScrollExecution(this.tree.getSort(), new ScrollDelegate<>(entityInformation.get())); } else if (this.tree.isDelete()) { return new DeleteExecution(em); } else if (this.tree.isExistsProjection()) { @@ -297,7 +297,7 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess ReturnedType returnedType = processor.withDynamicProjection(accessor).getReturnedType(); if (accessor.getScrollPosition() instanceof KeysetScrollPosition keyset) { - return new JpaKeysetScrollQueryCreator(tree, returnedType, provider, templates, entityInformation, keyset, + return new JpaKeysetScrollQueryCreator(tree, returnedType, provider, templates, entityInformation.get(), keyset, entityManager); } @@ -305,11 +305,12 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess if (accessor.getParameters().hasDynamicProjection() || getQueryMethod().isSearchQuery() || parameters.hasScoreRangeParameter() || parameters.hasScoreParameter()) { return new JpaQueryCreator(tree, getQueryMethod().isSearchQuery(), returnedType, provider, templates, - em.getMetamodel()); + entityInformation.get(), em.getMetamodel()); } JpqlQueryCreator creator = new CacheableJpqlQueryCreator(sort, new JpaQueryCreator(tree, - getQueryMethod().isSearchQuery(), returnedType, provider, templates, em.getMetamodel())); + getQueryMethod().isSearchQuery(), returnedType, provider, templates, entityInformation.get(), + em.getMetamodel())); cache.put(sort, accessor, creator); @@ -391,7 +392,8 @@ protected JpqlQueryCreator createCreator(Sort sort, JpaParametersParameterAccess ParameterMetadataProvider provider = new ParameterMetadataProvider(accessor, escape, templates); JpaCountQueryCreator creator = new JpaCountQueryCreator(tree, - getQueryMethod().getResultProcessor().getReturnedType(), provider, templates, em); + getQueryMethod().getResultProcessor().getReturnedType(), provider, templates, entityInformation.get(), + em.getMetamodel()); if (!accessor.getParameters().hasDynamicProjection()) { cached = new CacheableJpqlCountQueryCreator(creator); 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 62af516073..4572f8bf9c 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 @@ -17,6 +17,8 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceUnitUtil; +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.ManagedType; import jakarta.persistence.metamodel.Metamodel; import org.springframework.data.domain.Persistable; @@ -43,8 +45,17 @@ public abstract class JpaEntityInformationSupport extends AbstractEntityI * @param domainClass must not be {@literal null}. */ public JpaEntityInformationSupport(Class domainClass) { - super(domainClass); - this.metadata = new DefaultJpaEntityMetadata<>(domainClass); + this(new DefaultJpaEntityMetadata<>(domainClass)); + } + + /** + * Creates a new {@link JpaEntityInformationSupport} with the given {@link JpaEntityMetadata}. + * + * @param metadata must not be {@literal null}. + */ + public JpaEntityInformationSupport(JpaEntityMetadata metadata) { + super(metadata.getJavaType()); + this.metadata = metadata; } /** @@ -54,14 +65,39 @@ public JpaEntityInformationSupport(Class domainClass) { * @param em must not be {@literal null}. * @return */ - @SuppressWarnings({ "rawtypes", "unchecked" }) public static JpaEntityInformation getEntityInformation(Class domainClass, EntityManager em) { Assert.notNull(domainClass, "Domain class must not be null"); Assert.notNull(em, "EntityManager must not be null"); - Metamodel metamodel = em.getMetamodel(); - PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil(); + return getEntityInformation(domainClass, em.getMetamodel(), em.getEntityManagerFactory().getPersistenceUnitUtil()); + } + + /** + * Creates a {@link JpaEntityInformation} for the given domain class and {@link Metamodel}. + * + * @param domainClass must not be {@literal null}. + * @param metamodel must not be {@literal null}. + * @param persistenceUnitUtil must not be {@literal null}. + * @return + * @since 4.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static JpaEntityInformation getEntityInformation(Class domainClass, Metamodel metamodel, + PersistenceUnitUtil persistenceUnitUtil) { + + Assert.notNull(domainClass, "Domain class must not be null"); + Assert.notNull(metamodel, "Metamodel must not be null"); + + ManagedType type = metamodel.managedType(domainClass); + + if (type instanceof EntityType entityType) { + if (Persistable.class.isAssignableFrom(domainClass)) { + return new JpaPersistableEntityInformation(entityType, metamodel, persistenceUnitUtil); + } else { + return new JpaMetamodelEntityInformation(entityType, metamodel, persistenceUnitUtil); + } + } if (Persistable.class.isAssignableFrom(domainClass)) { return new JpaPersistableEntityInformation(domainClass, metamodel, persistenceUnitUtil); 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 66994749da..b1b8bb784c 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 @@ -37,11 +37,12 @@ import java.util.Set; import java.util.function.Function; -import org.springframework.beans.BeanWrapper; - import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeanWrapper; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.JpaMetamodelEntityMetadata; import org.springframework.data.jpa.util.JpaMetamodel; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.util.Assert; @@ -99,6 +100,28 @@ public JpaMetamodelEntityInformation(Class domainClass, Metamodel metamodel, this.persistenceUnitUtil = persistenceUnitUtil; } + /** + * Creates a new {@link JpaMetamodelEntityInformation} for the given {@link Metamodel}. + * + * @param entityType must not be {@literal null}. + * @param metamodel must not be {@literal null}. + * @param persistenceUnitUtil must not be {@literal null}. + * @since 4.0 + */ + JpaMetamodelEntityInformation(EntityType entityType, Metamodel metamodel, + PersistenceUnitUtil persistenceUnitUtil) { + + super(new JpaMetamodelEntityMetadata<>(entityType)); + + this.metamodel = metamodel; + this.entityName = entityType.getName(); + this.idMetadata = new IdMetadata<>(entityType, PersistenceProvider.fromMetamodel(metamodel)); + this.versionAttribute = findVersionAttribute(entityType, metamodel); + + Assert.notNull(persistenceUnitUtil, "PersistenceUnitUtil must not be null"); + this.persistenceUnitUtil = persistenceUnitUtil; + } + @Override public String getEntityName() { return entityName != null ? entityName : super.getEntityName(); 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 5832047303..9b214a76ff 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 @@ -16,12 +16,13 @@ package org.springframework.data.jpa.repository.support; import jakarta.persistence.PersistenceUnitUtil; +import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.Metamodel; -import org.springframework.data.domain.Persistable; - import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Persistable; + /** * Extension of {@link JpaMetamodelEntityInformation} that consideres methods of {@link Persistable} to lookup the id. * @@ -34,7 +35,7 @@ public class JpaPersistableEntityInformation, ID> /** * Creates a new {@link JpaPersistableEntityInformation} for the given domain class and {@link Metamodel}. - * + * * @param domainClass must not be {@literal null}. * @param metamodel must not be {@literal null}. * @param persistenceUnitUtil must not be {@literal null}. @@ -44,6 +45,19 @@ public JpaPersistableEntityInformation(Class domainClass, Metamodel metamodel super(domainClass, metamodel, persistenceUnitUtil); } + /** + * Creates a new {@link JpaPersistableEntityInformation} for the given {@link Metamodel}. + * + * @param entityType must not be {@literal null}. + * @param metamodel must not be {@literal null}. + * @param persistenceUnitUtil must not be {@literal null}. + * @since 4.0 + */ + JpaPersistableEntityInformation(EntityType entityType, Metamodel metamodel, + PersistenceUnitUtil persistenceUnitUtil) { + super(entityType, metamodel, persistenceUnitUtil); + } + @Override public boolean isNew(T entity) { return entity.isNew(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java index 97334b00cb..c6b8a8ad19 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/JpaRepositoryContributorConfigurationTests.java @@ -68,7 +68,7 @@ void shouldConsiderConfiguration() throws IOException { assertThatJson(json).inPath("$.methods[?(@.name == 'streamByLastnameLike')].query").isArray().first().isObject() .containsEntry("query", - "SELECT u FROM org.springframework.data.jpa.domain.sample.User u WHERE u.lastname LIKE :lastname ESCAPE 'ö'"); + "SELECT u FROM User u WHERE u.lastname LIKE :lastname ESCAPE 'ö'"); } } 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 0a65cd5c32..1146e06306 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 @@ -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 = :lastname"); + "SELECT COUNT(u) FROM User u WHERE u.lastname = :lastname"); } @Test // GH-3830 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 dd180bab52..b6612bfb71 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,7 +77,7 @@ 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 :firstname ESCAPE '\\') + SELECT u FROM 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) 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 a64c4f9fd1..b59a44a3a1 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 @@ -62,6 +62,7 @@ * Unit tests for {@link JpaQueryCreator}. * * @author Christoph Strobl + * @author Mark Paluch */ class JpaQueryCreatorTests { @@ -80,7 +81,7 @@ void simpleProperty() { .forTree(Order.class, "findOrderByCountry") // .withParameters("AT") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -91,7 +92,7 @@ void simpleNullProperty() { .forTree(Order.class, "findOrderByCountry") // .withParameterTypes(String.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.country IS NULL", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.country IS NULL", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -102,7 +103,7 @@ void negatingSimpleProperty() { .forTree(Order.class, "findOrderByCountryNot") // .withParameters("US") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.country != ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.country != ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -113,7 +114,7 @@ void negatingSimpleNullProperty() { .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()) // + .expectJpql("SELECT o FROM %s o WHERE o.country IS NOT NULL", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -124,7 +125,8 @@ void simpleAnd() { .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()) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 AND o.date = ?2", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -135,7 +137,8 @@ void simpleOr() { .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()) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 OR o.date = ?2", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -147,7 +150,7 @@ void simpleAndOr() { .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()) // + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -158,7 +161,7 @@ void distinct() { .forTree(Order.class, "findDistinctOrderByCountry") // .withParameters("AU") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT DISTINCT o FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .expectJpql("SELECT DISTINCT o FROM %s o WHERE o.country = ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -170,7 +173,7 @@ void count() { .returing(Long.class) // .withParameters("AU") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT COUNT(o) FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .expectJpql("SELECT COUNT(o) FROM %s o WHERE o.country = ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -182,7 +185,8 @@ void countWithJoins() { .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()) // + .expectJpql("SELECT COUNT(o) FROM %s o LEFT JOIN o.lineItems l WHERE l.quantity > ?1", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -194,7 +198,8 @@ void countDistinct() { .returing(Long.class) // .withParameters("AU") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT COUNT(DISTINCT o) FROM %s o WHERE o.country = ?1", Order.class.getName()) // + .expectJpql("SELECT COUNT(DISTINCT o) FROM %s o WHERE o.country = ?1", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -207,7 +212,7 @@ void simplePropertyIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .ingnoreCaseAs(ingnoreCaseTemplate) // .withParameters("BB") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE %s(o.country) = %s(?1)", Order.class.getName(), + .expectJpql("SELECT o FROM %s o WHERE %s(o.country) = %s(?1)", DefaultJpaEntityMetadata.unqualify(Order.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .validateQuery(); } @@ -222,7 +227,7 @@ void simplePropertyAllIgnoreCase(JpqlQueryTemplates 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(), + DefaultJpaEntityMetadata.unqualify(Product.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .validateQuery(); @@ -237,7 +242,8 @@ void simplePropertyMixedCase(JpqlQueryTemplates ingnoreCaseTemplate) { .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(), + .expectJpql("SELECT p FROM %s p WHERE p.name = ?1 AND %s(p.productType) = %s(?2)", + DefaultJpaEntityMetadata.unqualify(Product.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .validateQuery(); @@ -250,7 +256,7 @@ void lessThan() { .forTree(Order.class, "findOrderByDateLessThan") // .withParameterTypes(Date.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.date < ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.date < ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -261,7 +267,7 @@ void lessThanEqual() { .forTree(Order.class, "findOrderByDateLessThanEqual") // .withParameterTypes(Date.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.date <= ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.date <= ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -272,7 +278,7 @@ void greaterThan() { .forTree(Order.class, "findOrderByDateGreaterThan") // .withParameterTypes(Date.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.date > ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.date > ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -283,7 +289,7 @@ void before() { .forTree(Order.class, "findOrderByDateBefore") // .withParameterTypes(Date.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.date < ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.date < ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -294,7 +300,7 @@ void after() { .forTree(Order.class, "findOrderByDateAfter") // .withParameterTypes(Date.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.date > ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.date > ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -305,7 +311,8 @@ void between() { .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()) // + .expectJpql("SELECT o FROM %s o WHERE o.date BETWEEN ?1 AND ?2", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -315,7 +322,7 @@ 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()) // + .expectJpql("SELECT o FROM %s o WHERE o.date IS NULL", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -325,7 +332,7 @@ 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()) // + .expectJpql("SELECT o FROM %s o WHERE o.date IS NOT NULL", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -337,7 +344,8 @@ void like(String parameterValue) { .forTree(Product.class, "findProductByNameLike") // .withParameters(parameterValue) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", parameterValue) // .validateQuery(); } @@ -349,7 +357,8 @@ void containingString() { .forTree(Product.class, "findProductByNameContaining") // .withParameters("spring") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", "%spring%") // .validateQuery(); } @@ -361,7 +370,8 @@ void notContainingString() { .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()) // + .expectJpql("SELECT p FROM %s p WHERE p.name NOT LIKE ?1 ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", "%spring%") // .validateQuery(); } @@ -373,7 +383,7 @@ void in() { .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()) // + .expectJpql("SELECT p FROM %s p WHERE p.name IN (?1)", DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", List.of("spring", "data")) // .validateQuery(); } @@ -385,7 +395,7 @@ void notIn() { .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()) // + .expectJpql("SELECT p FROM %s p WHERE p.name NOT IN (?1)", DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", List.of("spring", "data")) // .validateQuery(); } @@ -397,7 +407,8 @@ void containingSingleEntryElementCollection() { .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()) // + .expectJpql("SELECT p FROM %s p WHERE ?1 MEMBER OF p.categories", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .validateQuery(); } @@ -408,7 +419,8 @@ void notContainingSingleEntryElementCollection() { .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()) // + .expectJpql("SELECT p FROM %s p WHERE ?1 NOT MEMBER OF p.categories", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .validateQuery(); } @@ -421,7 +433,8 @@ void likeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .ingnoreCaseAs(ingnoreCaseTemplate) // .withParameters("%spring%") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .expectPlaceholderValue("?1", "%spring%") // .validateQuery(); @@ -435,7 +448,8 @@ void notLike(String parameterValue) { .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()) // + .expectJpql("SELECT p FROM %s p WHERE p.name NOT LIKE ?1 ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", parameterValue) // .validateQuery(); } @@ -449,7 +463,8 @@ void notLikeWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .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(), + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) NOT LIKE %s(?1) ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .expectPlaceholderValue("?1", "%spring%") // .validateQuery(); @@ -462,7 +477,8 @@ void startingWith() { .forTree(Product.class, "findProductByNameStartingWith") // .withParameters("spring") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", "spring%") // .validateQuery(); } @@ -476,7 +492,8 @@ void startingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .ingnoreCaseAs(ingnoreCaseTemplate) // .withParameters("spring") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .expectPlaceholderValue("?1", "spring%") // .validateQuery(); @@ -489,7 +506,8 @@ void endingWith() { .forTree(Product.class, "findProductByNameEndingWith") // .withParameters("spring") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", Product.class.getName()) // + .expectJpql("SELECT p FROM %s p WHERE p.name LIKE ?1 ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class)) // .expectPlaceholderValue("?1", "%spring") // .validateQuery(); } @@ -503,7 +521,8 @@ void endingWithIgnoreCase(JpqlQueryTemplates ingnoreCaseTemplate) { .ingnoreCaseAs(ingnoreCaseTemplate) // .withParameters("spring") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", Product.class.getName(), + .expectJpql("SELECT p FROM %s p WHERE %s(p.name) LIKE %s(?1) ESCAPE '\\'", + DefaultJpaEntityMetadata.unqualify(Product.class), ingnoreCaseTemplate.getIgnoreCaseOperator(), ingnoreCaseTemplate.getIgnoreCaseOperator()) // .expectPlaceholderValue("?1", "%spring") // .validateQuery(); @@ -516,7 +535,7 @@ void greaterThanEqual() { .forTree(Order.class, "findOrderByDateGreaterThanEqual") // .withParameterTypes(Date.class) // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.date >= ?1", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.date >= ?1", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -526,7 +545,7 @@ void isTrue() { queryCreator(ORDER) // .forTree(Order.class, "findOrderByCompletedIsTrue") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.completed = TRUE", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.completed = TRUE", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -536,7 +555,7 @@ void isFalse() { queryCreator(ORDER) // .forTree(Order.class, "findOrderByCompletedIsFalse") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT o FROM %s o WHERE o.completed = FALSE", Order.class.getName()) // + .expectJpql("SELECT o FROM %s o WHERE o.completed = FALSE", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -546,7 +565,7 @@ 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()) // + .expectJpql("SELECT o FROM %s o WHERE o.lineItems IS EMPTY", DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -556,7 +575,8 @@ 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()) // + .expectJpql("SELECT o FROM %s o WHERE o.lineItems IS NOT EMPTY", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -567,7 +587,8 @@ void sortBySingle() { .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()) // + .expectJpql("SELECT o FROM %s o WHERE o.country = ?1 ORDER BY o.date asc", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -578,7 +599,8 @@ void sortByMulti() { .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()) // + .expectJpql("SELECT o FROM %s o ORDER BY o.country asc, o.date desc", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -591,7 +613,8 @@ void sortBySingleIngoreCase(JpqlQueryTemplates ingoreCase) { .forTree(Order.class, "findOrderByOrderByCountryAscAllIgnoreCase") // .render(); - assertThat(jpql).isEqualTo("SELECT o FROM %s o ORDER BY %s(o.date) asc", Order.class.getName(), + assertThat(jpql).isEqualTo("SELECT o FROM %s o ORDER BY %s(o.date) asc", + DefaultJpaEntityMetadata.unqualify(Order.class), ingoreCase.getIgnoreCaseOperator()); } @@ -602,7 +625,8 @@ void matchSimpleJoin() { .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()) // + .expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l WHERE l.quantity > ?1", + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -614,7 +638,7 @@ void matchSimpleNestedJoin() { .withParameters("spring") // .as(QueryCreatorTester::create) // .expectJpql("SELECT o FROM %s o LEFT JOIN o.lineItems l LEFT JOIN l.product p WHERE p.name = ?1", - Order.class.getName()) // + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -627,7 +651,7 @@ void matchMultiOnNestedJoin() { .as(QueryCreatorTester::create) // .expectJpql( "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()) // + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -640,7 +664,7 @@ void matchSameEntityMultipleTimes() { .as(QueryCreatorTester::create) // .expectJpql( "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()) // + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -653,7 +677,7 @@ void matchSameEntityMultipleTimesViaDifferentProperties() { .as(QueryCreatorTester::create) // .expectJpql( "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()) // + DefaultJpaEntityMetadata.unqualify(Order.class)) // .validateQuery(); } @@ -666,7 +690,7 @@ void dtoProjection() { .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()) // + DtoProductProjection.class.getName(), DefaultJpaEntityMetadata.unqualify(Product.class)) // .validateQuery(); } @@ -679,7 +703,7 @@ void interfaceProjection() { .withParameters("spring") // .as(QueryCreatorTester::create) // .expectJpql("SELECT p.name name, p.productType productType FROM %s p WHERE p.name = ?1", - Product.class.getName()) // + DefaultJpaEntityMetadata.unqualify(Product.class)) // .validateQuery(); } @@ -693,7 +717,7 @@ void tupleProjection(Class 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()) // + DefaultJpaEntityMetadata.unqualify(Person.class)) // .validateQuery(); } @@ -706,7 +730,7 @@ void delete(Class resultType) { .returing(resultType) // .withParameters("chris") // .as(QueryCreatorTester::create) // - .expectJpql("SELECT p FROM %s p WHERE p.firstname = ?1", Person.class.getName()) // + .expectJpql("SELECT p FROM %s p WHERE p.firstname = ?1", DefaultJpaEntityMetadata.unqualify(Person.class)) // .validateQuery(); } @@ -717,7 +741,7 @@ void exists() { .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()) // + .expectJpql("SELECT p.id id FROM %s p WHERE p.firstname = ?1", DefaultJpaEntityMetadata.unqualify(Person.class)) // .validateQuery(); } @@ -729,7 +753,7 @@ void doesNotCreateJoinForRelationshipEmbeddedId() { .withParameters(1L) // .as(QueryCreatorTester::create) // .expectJpql( - "SELECT r FROM org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee r WHERE r.employee.employeePk.employeeId = ?1") // + "SELECT r FROM ReferencingEmbeddedIdExampleEmployee r WHERE r.employee.employeePk.employeeId = ?1") // .validateQuery(); } @@ -741,7 +765,7 @@ void createsJoinForReferenceName() { .withParameters("foo") // .as(QueryCreatorTester::create) // .expectJpql( - "SELECT r FROM org.springframework.data.jpa.domain.sample.ReferencingEmbeddedIdExampleEmployee r LEFT JOIN r.employee e LEFT JOIN e.department d WHERE d.name = ?1") // + "SELECT r FROM ReferencingEmbeddedIdExampleEmployee r LEFT JOIN r.employee e LEFT JOIN e.department d WHERE d.name = ?1") // .validateQuery(); } 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 fa6cde1bc9..78721ae6fe 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 @@ -69,16 +69,16 @@ void stringLiteralRendersAsQuotedString() { @Test // GH-3588 void entity() { - Entity entity = JpqlQueryBuilder.entity(Order.class); + Entity entity = entity(Order.class); assertThat(entity.getAlias()).isEqualTo("o"); - assertThat(entity.getName()).isEqualTo(Order.class.getName()); + assertThat(entity.getName()).isEqualTo(getClass().getSimpleName() + "$" + Order.class.getSimpleName()); } @Test // GH-4032 void considersEntityName() { - Entity entity = JpqlQueryBuilder.entity(Product.class); + Entity entity = entity(Product.class); assertThat(entity.getAlias()).isEqualTo("p"); assertThat(entity.getName()).isEqualTo("my_product"); @@ -102,7 +102,7 @@ void aliasedExpression() { @Test // GH-3961 void shouldRenderDateAsJpqlLiteral() { - Entity entity = JpqlQueryBuilder.entity(Order.class); + Entity entity = entity(Order.class); PathAndOrigin orderDate = JpqlQueryBuilder.path(entity, "date"); String fragment = JpqlQueryBuilder.where(orderDate).eq(expression("{d '2024-11-05'}")).render(ctx(entity)); @@ -113,7 +113,7 @@ void shouldRenderDateAsJpqlLiteral() { @Test // GH-3588 void predicateRendering() { - Entity entity = JpqlQueryBuilder.entity(Order.class); + Entity entity = entity(Order.class); WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); ContextualAssert ctx = contextual(ctx(entity)); @@ -150,7 +150,7 @@ void predicateRendering() { @Test // GH-3961 void inPredicateWithNestedExpression() { - Entity entity = JpqlQueryBuilder.entity(Order.class); + Entity entity = entity(Order.class); WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); ContextualAssert ctx = contextual(ctx(entity)); @@ -176,20 +176,21 @@ void inPredicateWithNestedExpression() { void selectRendering() { // make sure things are immutable - SelectStep select = JpqlQueryBuilder.selectFrom(Order.class); // the select step is mutable - not sure i like it + SelectStep select = JpqlQueryBuilder.selectFrom(entity(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()) + assertThat(JpqlQueryBuilder.selectFrom(entity(Order.class)) + .select(JpqlQueryBuilder.path(entity(Order.class), "country")).render()) .startsWith("SELECT o.country "); } @Test // GH-3588 void joins() { - Entity entity = JpqlQueryBuilder.entity(LineItem.class); + Entity entity = entity(LineItem.class); Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); Join li_pr2 = JpqlQueryBuilder.innerJoin(entity, "product2"); @@ -205,7 +206,7 @@ void joins() { @Test // GH-3588 void joinOnPaths() { - Entity entity = JpqlQueryBuilder.entity(LineItem.class); + Entity entity = entity(LineItem.class); Join li_pr = JpqlQueryBuilder.innerJoin(entity, "product"); Join li_pe = JpqlQueryBuilder.innerJoin(entity, "person"); @@ -247,6 +248,16 @@ static RenderContext ctx(Entity... entities) { return new RenderContext(aliases); } + /** + * Create an {@link Entity} from the given {@link Class entity class}. + * + * @param from the entity type to select from. + * @return + */ + static Entity entity(Class from) { + return JpqlQueryBuilder.entity(new DefaultJpaEntityMetadata<>(from)); + } + @jakarta.persistence.Entity static class Order { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupportUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupportUnitTests.java index 6b36813294..fd66850d50 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupportUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaEntityInformationSupportUnitTests.java @@ -53,10 +53,10 @@ class JpaEntityInformationSupportUnitTests { @Mock PersistenceUnitUtil persistenceUnitUtil; @Test - void usesSimpleClassNameIfNoEntityNameGiven() { + void usesUnqualifiedClassNameIfNoEntityNameGiven() { JpaEntityInformation information = new DummyJpaEntityInformation<>(User.class); - assertThat(information.getEntityName()).isEqualTo("User"); + assertThat(information.getEntityName()).isEqualTo(getClass().getSimpleName() + "$" + User.class.getSimpleName()); JpaEntityInformation second = new DummyJpaEntityInformation(NamedUser.class); assertThat(second.getEntityName()).isEqualTo("AnotherNamedUser"); From ad6ae48c2d5bacb6170f66ac91163a067ff8fc92 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 7 Oct 2025 16:26:54 +0200 Subject: [PATCH 223/224] Polishing. Use simple class name for named stored procedure derivation. See #4032 --- .../repository/query/StoredProcedureAttributeSource.java | 6 +++--- .../query/StoredProcedureAttributeSourceUnitTests.java | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 2463b64c6a..bb9a8ba69c 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 @@ -26,9 +26,9 @@ import java.util.Collections; import java.util.List; -import org.springframework.core.annotation.AnnotatedElementUtils; - import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -213,7 +213,7 @@ private String derivedNamedProcedureNameFrom(Method method, JpaEntityMetadata return StringUtils.hasText(procedure.name()) // ? procedure.name() // - : entityMetadata.getEntityName() + "." + method.getName(); + : entityMetadata.getJavaType().getSimpleName() + "." + method.getName(); } /** diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java index 9e0363671d..bb0568cb28 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StoredProcedureAttributeSourceUnitTests.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.doReturn; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.EntityManager; import jakarta.persistence.Id; @@ -42,6 +41,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.core.annotation.AliasFor; import org.springframework.data.jpa.domain.sample.Dummy; import org.springframework.data.jpa.domain.sample.User; @@ -74,7 +74,7 @@ void setup() { creator = StoredProcedureAttributeSource.INSTANCE; doReturn(User.class).when(entityMetadata).getJavaType(); - when(entityMetadata.getEntityName()).thenReturn("User"); + when(entityMetadata.getEntityName()).thenReturn("Some$User"); } @Test // DATAJPA-455 From c1829998aed229f9d84aaea338345902eea32f6c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 10 Oct 2025 09:26:18 +0200 Subject: [PATCH 224/224] Remove accidental `org.jetbrains:annotations` usage. See spring-projects/spring-data-build#2670 --- .../data/jpa/repository/support/JpaRepositoryTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java index 563c0c7a67..535d8a4294 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java @@ -24,7 +24,6 @@ import java.util.Iterator; import java.util.List; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -151,7 +150,6 @@ void deleteAllByIdInBatchShouldConvertAnIterableToACollection() { private List ids = Arrays.asList(new SampleEntityPK("one", "eins"), new SampleEntityPK("three", "drei")); - @NotNull @Override public Iterator iterator() { return ids.iterator();