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 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8eb4cb3b3d..8bd014755d 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 14:00:49 CEST 2025 +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/Jenkinsfile b/Jenkinsfile index 915e46ddb7..c9dadd6a44 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { triggers { pollSCM 'H/10 * * * *' - upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS) + upstream(upstreamProjects: "spring-data-commons/3.5.x", threshold: hudson.model.Result.SUCCESS) } options { @@ -124,28 +124,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/README.adoc b/README.adoc index 3c5597973a..82e05e71d2 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. @@ -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 @@ -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 diff --git a/SECURITY.adoc b/SECURITY.adoc index de17b3ec0c..654bfbea58 100644 --- a/SECURITY.adoc +++ b/SECURITY.adoc @@ -1,9 +1,15 @@ = Security Policy -== Supported Versions +== Reporting a Vulnerability -Please see the https://spring.io/projects/spring-data-jpa[Spring Data JPA] project page for supported versions. +Please, https://github.com/spring-projects/security-advisories/security/advisories/new[open a draft security advisory] if you need to disclose and discuss a security issue in private with the Spring Data team. +Note that we only accept reports against https://spring.io/projects/spring-data#support[supported versions]. -== Reporting a Vulnerability +For more details, check out our https://spring.io/security-policy[security policy]. + +== JAR signing + +Spring Data JARs released on Maven Central are signed. +You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt -Please don't raise security vulnerabilities here. Head over to https://pivotal.io/security to learn how to disclose them responsibly. +Versions released prior to 2023 may be signed with a different key. diff --git a/pom.xml b/pom.xml index e5fefdec28..351f148c23 100755 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ - + 4.0.0 org.springframework.data spring-data-jpa-parent - 3.5.0 + 3.5.7-SNAPSHOT pom Spring Data JPA Parent @@ -23,16 +23,16 @@ org.springframework.data.build spring-data-parent - 3.5.0 + 3.5.7-SNAPSHOT 4.13.0 - 4.0.6 - 4.0.7-SNAPSHOT - 6.6.15.Final - 6.2.36.Final - 6.6.16-SNAPSHOT + 4.0.8 + 4.0.8-SNAPSHOT + 6.6.35.Final + 6.2.38.Final + 6.6.26.Final 7.0.0.Beta5 7.0.0-SNAPSHOT 2.7.4 @@ -40,8 +40,8 @@ 3.1.0 5.2 9.2.0 - 42.7.5 - 3.5.0 + 42.7.7 + 3.5.7-SNAPSHOT 0.10.3 org.hibernate @@ -173,8 +173,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 40c50b7016..d5f5762cae 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 + 3.5.7-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.0 + 3.5.7-SNAPSHOT ../pom.xml diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/EnableEnversRepositories.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/EnableEnversRepositories.java index feb8782229..47feff9a1d 100644 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/EnableEnversRepositories.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/config/EnableEnversRepositories.java @@ -138,7 +138,7 @@ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** - * Configure a specific {@link BeanNameGenerator} to be used when creating the repositoy beans. + * 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. * @since 3.4 * @see EnableJpaRepositories#nameGenerator() 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..aeb84f6455 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 @@ -93,6 +93,7 @@ public EnversRevisionRepositoryImpl(JpaEntityInformation entityInformation this.entityManager = entityManager; } + @Override @SuppressWarnings("unchecked") public Optional> findLastChangeRevision(ID id) { @@ -131,6 +132,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) { @@ -171,6 +173,7 @@ private List mapPropertySort(Sort sort) { return result; } + @Override @SuppressWarnings("unchecked") public Page> findRevisions(ID id, Pageable pageable) { @@ -182,9 +185,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(); diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 9f3f2ad5c4..6853801618 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 + 3.5.7-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 2628916dc8..4f37306665 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 + 3.5.7-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0 + 3.5.7-SNAPSHOT ../pom.xml 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..1baf31e7a7 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 @@ -214,8 +214,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 ; @@ -583,10 +583,7 @@ datetime_part ; function_arg - : literal - | state_valued_path_expression - | input_parameter - | scalar_expression + : simple_select_expression ; case_expression @@ -859,6 +856,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 4ed7a44554..b8e06ee253 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 @@ -1561,6 +1561,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 e0e2a0a35c..96bfeb90e3 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 @@ -208,8 +208,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 ; @@ -571,10 +571,7 @@ datetime_part ; function_arg - : literal - | state_valued_path_expression - | input_parameter - | scalar_expression + : simple_select_expression ; case_expression @@ -846,6 +843,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/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 9755052ed0..50b7095547 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 @@ -29,6 +29,13 @@ /** * 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}, 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 @@ -38,6 +45,7 @@ * @author Jens Schauder * @author Daniel Shuy * @author Sergey Rukin + * @author Peter Aisher */ @FunctionalInterface public interface Specification extends Serializable { @@ -47,6 +55,7 @@ public interface Specification extends Serializable { /** * Negates the given {@link Specification}. * + * @apiNote with 4.0, this method will no longer accept {@literal null} specifications. * @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}. @@ -58,28 +67,49 @@ static Specification not(@Nullable Specification spec) { ? (root, query, builder) -> null // : (root, query, builder) -> { - Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); - }; + Predicate predicate = spec.toPredicate(root, query, builder); + return predicate != null ? builder.not(predicate) : null; + }; + } + + /** + * 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. + * + *

+	 * 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}. + * @since 3.5.2 + */ + static Specification unrestricted() { + return (root, query, builder) -> null; } /** * Simple static factory method to add some syntactic sugar around a {@link Specification}. * + * @apiNote with 4.0, this method will no longer accept {@literal null} specifications. * @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 - * @deprecated since 3.5. + * @deprecated since 3.5, to be removed with 4.0 as we no longer want to support {@literal null} specifications. Use + * {@link #unrestricted()} instead. */ @Deprecated(since = "3.5.0", forRemoval = true) static Specification where(@Nullable Specification spec) { - return spec == null ? (root, query, builder) -> null : spec; + return spec == null ? unrestricted() : spec; } /** * ANDs the given {@link Specification} to the current one. * + * @apiNote with 4.0, this method will no longer accept {@literal null} specifications. * @param other can be {@literal null}. * @return The conjunction of the specifications * @since 2.0 @@ -91,6 +121,7 @@ default Specification and(@Nullable Specification other) { /** * ORs the given specification to the current one. * + * @apiNote with 4.0, this method will no longer accept {@literal null} specifications. * @param other can be {@literal null}. * @return The disjunction of the specifications * @since 2.0 @@ -104,7 +135,9 @@ default Specification or(@Nullable Specification other) { * {@link Root} and {@link CriteriaQuery}. * * @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 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}. */ 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..a246ccbade 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 @@ -31,8 +31,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/provider/JpaClassUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java index f00f4b849d..1ae908e375 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 @@ -58,7 +58,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) { + 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 2b5e0abbeb..9cb6744e33 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,16 +19,19 @@ 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; 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.stream.Stream; import org.eclipse.persistence.config.QueryHints; import org.eclipse.persistence.jpa.JpaQuery; @@ -36,6 +39,9 @@ import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.proxy.HibernateProxy; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -52,26 +58,20 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yuriy Tsarkov + * @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_INTERFACE), // - Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) { + HIBERNATE(List.of(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // + List.of(HIBERNATE_JPA_METAMODEL_TYPE)) { @Override public String extractQueryString(Query query) { return HibernateUtils.getHibernateQuery(query); } - /** * Return custom placeholder ({@code *}) as Hibernate does create invalid queries for count queries for objects with * compound keys. @@ -114,8 +114,7 @@ public String getCommentHintKey() { /** * EclipseLink persistence provider. */ - ECLIPSELINK(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(Query query) { @@ -147,12 +146,13 @@ public String getCommentHintKey() { public String getCommentHintValue(String comment) { return "/* " + comment + " */"; } + }, /** * Unknown special provider. Use standard JPA. */ - GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { + GENERIC_JPA(List.of(GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE), Collections.emptySet()) { @Nullable @Override @@ -199,7 +199,7 @@ public String getCommentHintKey() { private static final Collection ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA); private static final ConcurrentReferenceHashMap, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>(); - private final Iterable entityManagerClassNames; + final Iterable entityManagerFactoryClassNames; private final Iterable metamodelClassNames; private final boolean present; @@ -207,25 +207,17 @@ public String getCommentHintKey() { /** * Creates a new {@link PersistenceProvider}. * - * @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 entityManagerFactoryClassNames the names of the provider specific + * {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty. + * @param metamodelClassNames the names of the provider specific {@link Metamodel} implementations. Must not be + * {@literal null} or empty. */ - PersistenceProvider(Iterable entityManagerClassNames, Iterable metamodelClassNames) { + PersistenceProvider(Collection entityManagerFactoryClassNames, Collection metamodelClassNames) { - this.entityManagerClassNames = entityManagerClassNames; + this.entityManagerFactoryClassNames = entityManagerFactoryClassNames; this.metamodelClassNames = metamodelClassNames; - - boolean present = false; - 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())); } /** @@ -241,17 +233,52 @@ 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(); + return fromEntityManagerFactory(em.getEntityManagerFactory()); + } + + /** + * 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}. + * @return will never be {@literal null}. + * @since 3.5.1 + */ + public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) { + + Assert.notNull(emf, "EntityManagerFactory must not be null"); + + EntityManagerFactory unwrapped = emf; + + while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) { + + if (Proxy.isProxyClass(unwrapped.getClass())) { + + 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); + } + } + + Class entityManagerType = unwrapped.getClass(); PersistenceProvider cachedProvider = CACHE.get(entityManagerType); if (cachedProvider != null) { @@ -259,8 +286,8 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { } for (PersistenceProvider provider : ALL) { - for (String entityManagerClassName : provider.entityManagerClassNames) { - if (isEntityManagerOfType(em, entityManagerClassName)) { + for (String emfClassName : provider.entityManagerFactoryClassNames) { + if (isOfType(unwrapped, emfClassName, unwrapped.getClass().getClassLoader())) { return cacheAndReturn(entityManagerType, provider); } } @@ -354,13 +381,18 @@ 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_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManagerFactory"; 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_INTERFACE = "org.hibernate.engine.spi.SessionImplementor"; + 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.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"; + } public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { @@ -465,5 +497,7 @@ public void close() { scrollableCursor.close(); } } + } + } 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..15467a84b5 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 @@ -56,8 +56,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 {}; 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..6dabea173a 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.Arrays; import java.util.Collection; @@ -160,7 +161,7 @@ private Object doExecute(JpaQueryExecution execution, Object[] values) { ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor); return withDynamicProjection.processResult(result, - new TupleConverter(withDynamicProjection.getReturnedType(), method.isNativeQuery())); + new LazyTupleConverter(withDynamicProjection.getReturnedType(), method.isNativeQuery())); } private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) { @@ -290,7 +291,7 @@ protected Class getTypeToRead(ReturnedType returnedType) { return null; } - return returnedType.isProjecting() && returnedType.getReturnedType().isInterface() + return returnedType.isInterfaceProjection() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) // ? Tuple.class // : null; @@ -312,6 +313,23 @@ protected Class getTypeToRead(ReturnedType returnedType) { */ protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor); + /** + * Lazy variant of {@link TupleConverter} to avoid early instantiation. + */ + private static class LazyTupleConverter implements Converter { + + private final Lazy delegate; + + public LazyTupleConverter(ReturnedType type, boolean nativeQuery) { + this.delegate = Lazy.of(() -> new TupleConverter(type, nativeQuery)); + } + + @Override + public Object convert(Object source) { + return delegate.get().convert(source); + } + } + public static class TupleConverter implements Converter { private final ReturnedType type; @@ -344,8 +362,8 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) { this.type = type; this.tupleWrapper = nativeQuery ? FallbackTupleWrapper::new : UnaryOperator.identity(); - this.dtoProjection = type.isProjecting() && !type.getReturnedType().isInterface() - && !type.getInputProperties().isEmpty(); + this.dtoProjection = type.isDtoProjection() + && type.needsCustomConstruction(); if (this.dtoProjection) { this.preferredConstructor = PreferredConstructorDiscoverer.discover(type.getReturnedType()); @@ -474,7 +492,7 @@ private static boolean areAssignmentCompatible(Class to, Class from) { * * @author Jens Schauder */ - private static class TupleBackedMap implements Map { + private static class TupleBackedMap extends AbstractMap implements Map { private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified"; 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..ad9252ba5a 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 @@ -27,8 +27,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; @@ -85,15 +83,12 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri this.valueExpressionDelegate = valueExpressionDelegate; this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); - this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), valueExpressionDelegate, - method.isNativeQuery()); + this.query = new ExpressionBasedStringQuery(queryString, method, valueExpressionDelegate); this.countQuery = Lazy.of(() -> { if (StringUtils.hasText(countQueryString)) { - - return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), valueExpressionDelegate, - method.isNativeQuery()); + return new ExpressionBasedStringQuery(countQueryString, method, valueExpressionDelegate); } return query.deriveCountQuery(method.getCountQueryProjection()); @@ -144,12 +139,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(); + Class returnedJavaType = returnedType.getReturnedType(); - if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface() + if (query.hasConstructorExpression() || !returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNativeQuery()) { return returnedType; } @@ -160,55 +155,15 @@ private ReturnedType getReturnedType(ResultProcessor processor) { return returnedType; } - if ((known != null && !known) || returnedJavaType.isArray()) { + if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType) + || !returnedType.needsCustomConstruction()) { if (known == null) { knownProjections.put(returnedJavaType, false); } return new NonProjectingReturnedType(returnedType); } - String alias = query.getAlias(); - String projection = query.getProjection(); - - // we can handle single-column and no function projections here only - if (StringUtils.hasText(projection) && (projection.indexOf(',') != -1 || projection.indexOf('(') != -1)) { - return returnedType; - } - - if (StringUtils.hasText(alias) && StringUtils.hasText(projection)) { - alias = alias.trim(); - projection = projection.trim(); - if (projection.startsWith(alias + ".")) { - projection = projection.substring(alias.length() + 1); - } - } - - if (StringUtils.hasText(projection)) { - - int space = projection.indexOf(' '); - - if (space != -1) { - projection = projection.substring(0, space); - } - - Class propertyType; - - try { - PropertyPath from = PropertyPath.from(projection, 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 4593697a4d..ea4a0848e4 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,7 +17,13 @@ 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; +import org.springframework.data.util.Lazy; /** * HQL Query Transformer that rewrites the query using constructor expressions. @@ -25,7 +31,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 +41,94 @@ class DtoProjectionTransformerDelegate { private final ReturnedType returnedType; + private final Lazy applyRewriting; + private final List selectItems = new ArrayList<>(); public DtoProjectionTransformerDelegate(ReturnedType returnedType) { this.returnedType = returnedType; + this.applyRewriting = Lazy.of(() -> returnedType.isDtoProjection() + && returnedType.needsCustomConstruction()); + } + + public boolean applyRewriting() { + return applyRewriting.get(); + } + + public boolean canRewrite() { + return !selectItems.isEmpty() && applyRewriting(); + } + + 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() - || 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.getFirst().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/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 22b264cc16..5babfca203 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 @@ -292,8 +292,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(); @@ -451,8 +450,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(); @@ -641,6 +639,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())); @@ -649,8 +656,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; } @@ -761,7 +766,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) { @@ -770,13 +775,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())); @@ -2080,16 +2079,7 @@ public QueryTokenStream visitDatetime_part(EqlParser.Datetime_partContext ctx) { @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()); - } + return visit(ctx.simple_select_expression()); } @Override @@ -2448,8 +2438,7 @@ 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())); 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..1d68ad9595 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 @@ -91,17 +91,53 @@ public QueryTokenStream visitSelect_clause(EqlParser.Select_clauseContext ctx) { return super.visitSelect_clause(ctx); } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = prepareSelectClause(ctx); - builder.append(QueryTokens.expression(ctx.SELECT())); + QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (dtoDelegate != null && dtoDelegate.canRewrite()) { + builder.append(dtoDelegate.getRewrittenSelectionList()); + } else { + builder.append(selectItems); } - QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + return builder; + } + + @Override + public QueryTokenStream visitSelect_item(EqlParser.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(EqlParser.Select_expressionContext ctx) { + + QueryTokenStream selectItem = super.visitSelect_expression(ctx); + + if (ctx.constructor_expression() == null && dtoDelegate != null && dtoDelegate.applyRewriting()) { + dtoDelegate.appendSelectItem(selectItem); + } + + 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 builder.append(dtoDelegate.transformSelectionList(tokenStream)); + return tokens; } private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) { @@ -131,28 +167,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.getLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(EqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); - } - - return tokens; - } - } 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..218e88b998 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 @@ -52,6 +52,17 @@ class ExpressionBasedStringQuery extends StringQuery { private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME; private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE; + /** + * Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link JpaQueryMethod}. + * + * @param query must not be {@literal null} or empty. + * @param queryMethod must not be {@literal null} or empty. + * @param parser must not be {@literal null}. + */ + public ExpressionBasedStringQuery(String query, JpaQueryMethod queryMethod, ValueExpressionParser parser) { + this(query, queryMethod.getEntityInformation(), parser, queryMethod.isNativeQuery()); + } + /** * Creates a new {@link ExpressionBasedStringQuery} for the given query and {@link EntityMetadata}. * @@ -62,7 +73,7 @@ class ExpressionBasedStringQuery extends StringQuery { */ public ExpressionBasedStringQuery(String query, JpaEntityMetadata metadata, ValueExpressionParser parser, boolean nativeQuery) { - super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery && !containsExpression(query)); + super(renderQueryIfExpressionOrReturnQuery(query, metadata, parser), nativeQuery); } /** 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..85e4d1d248 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 @@ -103,13 +103,12 @@ 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); } } - if (ctx.whereClause() != null) { builder.appendExpression(visit(ctx.whereClause())); } 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..9a523d2bb0 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,8 @@ * * @author Greg Turnquist * @author Christoph Strobl + * @author Mark Paluch + * @author TaeHyun Kang * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode", "UnreachableCode" }) @@ -39,23 +41,27 @@ class HqlQueryRenderer extends HqlBaseVisitor { /** * Is this select clause a {@literal subquery}? * - * @return boolean + * @return {@literal true} is the query is a subquery; {@literal false} otherwise. */ 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; } @Override @@ -195,22 +201,22 @@ public QueryTokenStream visitCycleClause(HqlParser.CycleClauseContext ctx) { QueryRendererBuilder builder = QueryRenderer.builder(); builder.append(QueryTokens.expression(ctx.CYCLE().getText())); - builder.append(visit(ctx.cteAttributes())); + builder.appendExpression(visit(ctx.cteAttributes())); builder.append(QueryTokens.expression(ctx.SET().getText())); - builder.append(visit(ctx.identifier(0))); + builder.appendExpression(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))); + builder.appendExpression(visit(ctx.literal(1))); } if (ctx.USING() != null) { builder.append(QueryTokens.expression(ctx.USING().getText())); - builder.append(visit(ctx.identifier(1))); + builder.appendExpression(visit(ctx.identifier(1))); } return builder; @@ -830,7 +836,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 eadee9496d..518924630c 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 @@ -91,13 +91,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 (ctx.instantiation() == null && !isSubquery(ctx) && dtoDelegate != null && dtoDelegate.applyRewriting()) { + dtoDelegate.appendSelectItem(QueryRenderer.expression(selectItem)); + } + + return selectItem; + } + @Override public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) { 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..7890d388df 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 @@ -73,7 +73,6 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { private final DeclaredQuery query; - private final Statement statement; private final ParsedType parsedType; private final boolean hasConstructorExpression; private final @Nullable String primaryAlias; @@ -88,15 +87,15 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { public JSqlParserQueryEnhancer(DeclaredQuery 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); } /** @@ -215,8 +214,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) { @@ -236,8 +235,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) { 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..5c20838e88 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 @@ -63,7 +63,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}. 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..9dd6a81efc 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 @@ -238,7 +238,7 @@ public Set getJoinAliases() { */ @Override public DeclaredQuery getQuery() { - throw new UnsupportedOperationException(); + return DeclaredQuery.of(applySorting(Sort.unsorted()), false); } /** 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..de751632d2 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 @@ -29,6 +29,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.ScrollPosition; @@ -43,6 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** @@ -246,11 +248,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 isVoid = org.springframework.data.util.ReflectionUtils.isVoid(returnType); + boolean isNumber = ClassUtils.isAssignable(Number.class, returnType); - Assert.isTrue(isInt || 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(); @@ -290,16 +292,34 @@ 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(); + Class returnType = 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) { + + 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/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..0bc09afb6e 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 @@ -572,6 +572,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())); @@ -580,8 +589,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; } @@ -695,7 +702,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) { @@ -704,13 +711,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())); @@ -1975,16 +1977,7 @@ public QueryTokenStream visitDatetime_part(JpqlParser.Datetime_partContext ctx) @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()); - } + return visit(ctx.simple_select_expression()); } @Override @@ -2114,7 +2107,7 @@ public QueryTokenStream visitIdentification_variable(JpqlParser.Identification_v if (ctx.IDENTIFICATION_VARIABLE() != null) { return QueryRenderer.from(QueryTokens.expression(ctx.IDENTIFICATION_VARIABLE())); } else if (ctx.f != null) { - return QueryRenderer.from(QueryTokens.token(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 41d0661d2c..48ca088a9e 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 @@ -83,17 +83,53 @@ public QueryTokenStream visitSelect_clause(JpqlParser.Select_clauseContext ctx) return super.visitSelect_clause(ctx); } - QueryRendererBuilder builder = QueryRenderer.builder(); + QueryRendererBuilder builder = prepareSelectClause(ctx); - builder.append(QueryTokens.expression(ctx.SELECT())); + QueryTokenStream selectItems = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); - if (ctx.DISTINCT() != null) { - builder.append(QueryTokens.expression(ctx.DISTINCT())); + if (dtoDelegate != null && dtoDelegate.canRewrite()) { + builder.append(dtoDelegate.getRewrittenSelectionList()); + } else { + builder.append(selectItems); } - QueryTokenStream tokenStream = QueryTokenStream.concat(ctx.select_item(), this::visit, TOKEN_COMMA); + 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) { + + QueryTokenStream selectItem = super.visitSelect_expression(ctx); + + if (ctx.constructor_expression() == null && dtoDelegate != null && dtoDelegate.applyRewriting()) { + dtoDelegate.appendSelectItem(selectItem); + } + + return selectItem; + } + + @Override + public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { + + QueryTokenStream tokens = super.visitJoin(ctx); + + if (ctx.identification_variable() != null) { + transformerSupport.registerAlias(ctx.identification_variable().getText()); + } - return builder.append(dtoDelegate.transformSelectionList(tokenStream)); + return tokens; } private void doVisitOrderBy(QueryRendererBuilder builder, JpqlParser.Select_statementContext ctx) { @@ -122,28 +158,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.getLast()); - } - - return tokens; - } - - @Override - public QueryTokenStream visitJoin(JpqlParser.JoinContext ctx) { - - QueryTokenStream tokens = super.visitJoin(ctx); - - if (!tokens.isEmpty()) { - transformerSupport.registerAlias(tokens.getLast()); - } - - return tokens; - } } 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..6ea7548fb4 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 @@ -95,7 +95,7 @@ private Class getTypeToQueryFor(ReturnedType returnedType) { if (returnedType.isProjecting()) { - if (returnedType.getReturnedType().isInterface()) { + if (returnedType.isInterfaceProjection()) { return Tuple.class; } 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..53a3def793 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 @@ -326,12 +326,8 @@ static Type getLikeTypeFrom(String expression) { Assert.hasText(expression, "Expression must not be null or empty"); - if (expression.matches("%.*%")) { - return Type.CONTAINING; - } - if (expression.startsWith("%")) { - return Type.ENDING_WITH; + return expression.endsWith("%") ? Type.CONTAINING : Type.ENDING_WITH; } if (expression.endsWith("%")) { 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..c82f0519b7 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 @@ -190,6 +190,10 @@ public static QueryRenderer expression(QueryTokenStream tokenStream) { return EmptyQueryRenderer.INSTANCE; } + if (!(tokenStream instanceof QueryRenderer)) { + tokenStream = QueryRenderer.from(tokenStream); + } + if (tokenStream.isExpression()) { return (QueryRenderer) tokenStream; } @@ -207,6 +211,10 @@ public static QueryRenderer inline(QueryTokenStream tokenStream) { return EmptyQueryRenderer.INSTANCE; } + if (!(tokenStream instanceof QueryRenderer)) { + tokenStream = QueryRenderer.from(tokenStream); + } + if (!tokenStream.isExpression()) { return (QueryRenderer) tokenStream; } @@ -341,6 +349,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/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..752a8c8968 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 @@ -50,6 +50,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.lang.Nullable; @@ -91,7 +92,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 @@ -193,17 +194,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+"); // at least one space builder.append("[^\\s\\(\\)]+"); // No white char no bracket - builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); // the potential alias - - FIELD_ALIAS_PATTERN = compile(builder.toString()); + builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); // the potential alias + FIELD_ALIAS_PATTERN = compile(builder.toString(), CASE_INSENSITIVE); } /** @@ -389,7 +388,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); @@ -530,6 +529,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"); @@ -541,9 +555,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/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..f2634c1620 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 @@ -40,13 +40,13 @@ final class SimpleJpaQuery extends AbstractStringBasedJpaQuery { * * @param method must not be {@literal null} * @param em must not be {@literal null} - * @param countQueryString + * @param sourceQuery the original source query, must not be {@literal null} or empty. * @param queryRewriter must not be {@literal null} * @param valueExpressionDelegate must not be {@literal null} */ - public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String countQueryString, + public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable String sourceQuery, QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { - this(method, em, method.getRequiredAnnotatedQuery(), countQueryString, queryRewriter, valueExpressionDelegate); + this(method, em, method.getRequiredAnnotatedQuery(), sourceQuery, queryRewriter, valueExpressionDelegate); } /** @@ -54,21 +54,20 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, @Nullable 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 sourceQuery the original source query, must not be {@literal null} or empty * @param countQueryString * @param queryRewriter * @param valueExpressionDelegate 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 sourceQuery, @Nullable String countQueryString, + QueryRewriter queryRewriter, ValueExpressionDelegate valueExpressionDelegate) { - super(method, em, queryString, countQueryString, queryRewriter, valueExpressionDelegate); + super(method, em, sourceQuery, countQueryString, queryRewriter, valueExpressionDelegate); - validateQuery(getQuery().getQueryString(), "Validation failed for query for method %s", method); + validateQuery(getQuery(), "Query '%s' validation failed 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); } } @@ -78,29 +77,20 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin * @param query * @param errorMessage */ - private void validateQuery(String query, String errorMessage, Object... arguments) { + private void validateQuery(DeclaredQuery query, String errorMessage, JpaQueryMethod method) { if (getQueryMethod().isProcedureQuery()) { return; } - EntityManager validatingEm = null; - - try { - validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager(); - validatingEm.createQuery(query); - + 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://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(); - } + // https://download.oracle.com/javaee-archive/jpa-spec.java.net/users/2012/07/0404.html + throw new IllegalArgumentException(errorMessage.formatted(queryString, method), 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..a142160032 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 @@ -145,11 +145,10 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) { for (ParameterBinding binding : bindings) { Predicate identifier = binding::bindsTo; - Predicate notCompatible = Predicate.not(binding::isCompatibleWith); + Predicate notCompatible = Predicate.not(binding::isCompatibleWith); - // replace incompatible bindings - if ( derivedBindings.removeIf( - it -> identifier.test(it) && notCompatible.test(it))) { + // replace incompatible bindings + if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) { derivedBindings.add(binding); } } @@ -282,7 +281,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. @@ -366,7 +365,9 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que 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()) { queryMeta.usesJdbcStyleParameters = true; } 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..cfaa259bac 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 @@ -231,7 +231,7 @@ private void applyQuerySettings(ReturnedType returnedType, int limit, AbstractJP if (returnedType.needsCustomConstruction()) { Collection requiredSelection; - if (scrollPosition instanceof KeysetScrollPosition && returnedType.getReturnedType().isInterface()) { + if (scrollPosition instanceof KeysetScrollPosition && returnedType.isInterfaceProjection()) { requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort); } else { requiredSelection = inputProperties; @@ -240,7 +240,7 @@ private void applyQuerySettings(ReturnedType returnedType, int limit, AbstractJP PathBuilder builder = new PathBuilder<>(entityPath.getType(), entityPath.getMetadata()); Expression[] projection = requiredSelection.stream().map(builder::get).toArray(Expression[]::new); - if (returnedType.getReturnedType().isInterface()) { + if (returnedType.isInterfaceProjection()) { query.select(new JakartaTuple(projection)); } else { query.select(new DtoProjection(returnedType.getReturnedType(), projection)); 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..e514b5f27e 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 @@ -22,7 +22,7 @@ import org.springframework.lang.Nullable; /** - * Extension of {@link JpaMetamodelEntityInformation} that consideres methods of {@link Persistable} to lookup the id. + * Extension of {@link JpaMetamodelEntityInformation} that considers methods of {@link Persistable} to lookup the id. * * @author Oliver Gierke * @author Christoph Strobl @@ -53,4 +53,5 @@ public boolean isNew(T entity) { public ID getId(T entity) { return entity.getId(); } + } 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..7a0140d742 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 @@ -37,9 +37,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; @@ -71,6 +73,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.Nullable; @@ -101,6 +104,7 @@ * @author Diego Krupitza * @author Seol-JY * @author Joshua Chen + * @author Giheon Do */ @Repository @Transactional(readOnly = true) @@ -118,6 +122,9 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation deleteAllQueryString; + private final Lazy countQueryString; + private @Nullable CrudMethodMetadata metadata; private ProjectionFactory projectionFactory; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; @@ -137,6 +144,12 @@ public SimpleJpaRepository(JpaEntityInformation entityInformation, EntityM this.entityManager = entityManager; this.provider = PersistenceProvider.fromEntityManager(entityManager); this.projectionFactory = new SpelAwareProxyProjectionFactory(); + + 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())); } /** @@ -179,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) { @@ -307,7 +310,7 @@ public void deleteAll() { @Transactional public void deleteAllInBatch() { - Query query = entityManager.createQuery(getDeleteAllQueryString()); + Query query = entityManager.createQuery(deleteAllQueryString.get()); applyQueryHints(query); @@ -629,7 +632,7 @@ public R findBy(Example example, Function query = entityManager.createQuery(getCountQueryString(), Long.class); + TypedQuery query = entityManager.createQuery(countQueryString.get(), Long.class); applyQueryHintsForCount(query); @@ -791,7 +794,7 @@ private TypedQuery getQuery(ReturnedType returnedType, @Nullabl CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query; - boolean interfaceProjection = returnedType.getReturnedType().isInterface(); + boolean interfaceProjection = returnedType.isInterfaceProjection(); if (returnedType.needsCustomConstruction() && (inputProperties.isEmpty() || !interfaceProjection)) { inputProperties = returnedType.getInputProperties(); @@ -817,11 +820,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(); 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..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; @@ -31,7 +31,7 @@ */ class Jsr310JpaConvertersUnitTests { - static Iterable data() { + static Iterable data() { return Arrays.asList(new Jsr310JpaConverters.InstantConverter(), // new Jsr310JpaConverters.LocalDateConverter(), // @@ -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/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index eba6ed8851..7ee4782527 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 @@ -45,8 +45,9 @@ * @author Jens Schauder * @author Mark Paluch * @author Daniel Shuy + * @author Peter Aisher */ -@SuppressWarnings("removal") +@SuppressWarnings({ "unchecked", "deprecation", "removal" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class SpecificationUnitTests { @@ -55,7 +56,6 @@ class SpecificationUnitTests { @Mock(serializable = true) Root root; @Mock(serializable = true) CriteriaQuery query; @Mock(serializable = true) CriteriaBuilder builder; - @Mock(serializable = true) Predicate predicate; @BeforeEach @@ -163,7 +163,6 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation" }) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -178,7 +177,6 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation" }) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -191,7 +189,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); @@ -213,15 +210,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((r, q, cb) -> null); + Specification notSpec = Specification.not(Specification.unrestricted()); - assertThat(notSpec.toPredicate(root, query, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, query, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, Specification { 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/Customer.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Customer.java index f8442a9ae4..d1c3d3e0f1 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 @@ -25,7 +25,7 @@ @Entity public class Customer { - @Id Long id; + @Id Long id; - String name; + String name; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContextUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContextUnitTests.java index 03962c2aa6..cae588d2ea 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContextUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/mapping/JpaMetamodelMappingContextUnitTests.java @@ -32,7 +32,7 @@ class JpaMetamodelMappingContextUnitTests { @Test // DATAJPA-775 - void jpaPersistentEntityRejectsSprignDataAtVersionAnnotation() { + void jpaPersistentEntityRejectsSpringDataAtVersionAnnotation() { Metamodel metamodel = mock(Metamodel.class); 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..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 @@ -16,23 +16,28 @@ 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 jakarta.persistence.PersistenceException; +import java.lang.reflect.Proxy; 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; 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; @@ -42,6 +47,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Jens Schauder + * @author Mark Paluch */ class PersistenceProviderUnitTests { @@ -56,12 +62,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,33 +96,40 @@ 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"); + @Test // DATAJPA-1379 + void detectsProviderFromProxiedEntityManager() throws Exception { - shadowingClassLoader.excludePackage("org.hibernate"); + shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa"); - EntityManager em = mockProviderSpecificEntityManagerInterface(HIBERNATE_ENTITY_MANAGER_INTERFACE); + EntityManager emProxy = Mockito.mock(EntityManager.class); + when(emProxy.getEntityManagerFactory()) + .thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE)); - assertThat(fromEntityManager(em)).isEqualTo(HIBERNATE); + assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK); } - @Test // DATAJPA-1379 - void detectsProviderFromProxiedEntityManager() throws Exception { + @Test // GH-3923 + void detectsEntityManagerFromProxiedEntityManagerFactory() throws Exception { - shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa"); + EntityManagerFactory emf = mockProviderSpecificEntityManagerFactoryInterface( + "foo.bar.unknown.jpa.JpaEntityManager"); + when(emf.unwrap(null)).thenThrow(new NullPointerException()); + when(emf.unwrap(EntityManagerFactory.class)).thenReturn(emf); - EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE); + MyEntityManagerFactoryBean factoryBean = new MyEntityManagerFactoryBean(EntityManagerFactory.class, emf); + EntityManagerFactory springProxy = factoryBean.createEntityManagerFactoryProxy(emf); - EntityManager emProxy = Mockito.mock(EntityManager.class); - Mockito.when(emProxy.getDelegate()).thenReturn(em); + Object externalProxy = Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { EntityManagerFactory.class }, (proxy, method, args) -> method.invoke(emf, args)); - assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK); + assertThat(PersistenceProvider.fromEntityManagerFactory(springProxy)).isEqualTo(GENERIC_JPA); + assertThat(PersistenceProvider.fromEntityManagerFactory((EntityManagerFactory) externalProxy)) + .isEqualTo(GENERIC_JPA); } private EntityManager mockProviderSpecificEntityManagerInterface(String interfaceName) throws ClassNotFoundException { @@ -105,13 +138,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, + EntityManagerFactory.class); + + return (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerInterface); + } + static class InterfaceGenerator implements Opcodes { static Class generate(final String interfaceName, ClassLoader parentClassLoader, final Class... interfaces) @@ -160,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); + } + } } 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/EclipseLinkUserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java index 8593c1ed3e..75cfa39d82 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,8 +36,4 @@ void executesNotInQueryCorrectly() {} @Override void executesInKeywordForPageCorrectly() {} - @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/EntityGraphRepositoryMethodsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java index 7191b85a4d..64f76c81e3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java @@ -121,7 +121,7 @@ void shouldRespectConfiguredJpaEntityGraphInFindOne() { assertThat(user).isNotNull(); assertThat(util.isLoaded(user, "colleagues")) // - .describedAs("colleages should be fetched with 'user.detail' fetchgraph") // + .describedAs("colleagues should be fetched with 'user.detail' fetchgraph") // .isTrue(); } @@ -137,7 +137,7 @@ void shouldRespectInferFetchGraphFromMethodName() { assertThat(user).isNotNull(); assertThat(util.isLoaded(user, "colleagues")) // - .describedAs("colleages should be fetched with 'user.detail' fetchgraph") // + .describedAs("colleagues should be fetched with 'user.detail' fetchgraph") // .isTrue(); } @@ -154,7 +154,7 @@ void shouldRespectDynamicFetchGraphForGetOneWithAttributeNamesById() { assertThat(user).isNotNull(); assertThat(util.isLoaded(user, "colleagues")) // - .describedAs("colleages should be fetched with 'user.detail' fetchgraph") // + .describedAs("colleagues should be fetched with 'user.detail' fetchgraph") // .isTrue(); assertThat(util.isLoaded(user, "colleagues")).isTrue(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/GreetingsFrom.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/GreetingsFrom.java index 3e31f95ffb..b2e5ae6577 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/GreetingsFrom.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/GreetingsFrom.java @@ -16,7 +16,7 @@ package org.springframework.data.jpa.repository; /** - * A trivial component registered via {@literal appication-context.xml} to be called from SpEL. + * A trivial component registered via {@literal application-context.xml} to be called from SpEL. */ public class GreetingsFrom { 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..54f062d5ef --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java @@ -0,0 +1,52 @@ +/* + * 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 java.util.Optional; + +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; + +/** + * {@code CurrentTenantIdentifierResolver} instance for testing. + * + * @author Ariel Morelli Andres + * @author Mark Paluch + */ +@SuppressWarnings("rawtypes") // Hibernate 6.2 does not specify a generic parameter +public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { + + private static final ThreadLocal CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>(); + + static void setTenantIdentifier(String tenantIdentifier) { + CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier); + } + + 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..28ebcd1765 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java @@ -0,0 +1,94 @@ +/* + * 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 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; +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; + +/** + * 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 + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +class HibernateMultitenancyTests { + + @Autowired RoleRepository roleRepository; + @Autowired EntityManager em; + + @AfterEach + void tearDown() { + HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier(); + } + + @Test // GH-3425 + void testPersistenceProviderFromFactoryWithoutTenant() { + + PersistenceProvider provider = PersistenceProvider.fromEntityManager(em); + + assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE); + } + + @Test // GH-3425 + void testRepositoryWithTenant() { + + HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id"); + + assertThatNoException().isThrownBy(() -> roleRepository.findAll()); + } + + @Test // GH-3425 + 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/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/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 7eb4e4cc6c..d18081e286 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,120 +366,4 @@ 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)); - }); - } - - @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/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 9ebddf394b..5b7f313406 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 @@ -450,7 +450,7 @@ void testInvocationOfCustomImplementation() { @Test void testOverwritingFinder() { - repository.findByOverrridingMethod(); + repository.findByOverridingMethod(); } @Test @@ -1557,6 +1557,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() { @@ -1566,12 +1598,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 @@ -3583,7 +3624,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/AbstractDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java new file mode 100644 index 0000000000..fcede5da49 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java @@ -0,0 +1,167 @@ +/* + * 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 shouldRewritePrimarySelectionToConstructorExpressionWithProperties() { + + 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, GH-3895 + void shouldRewriteSelectionToConstructorExpression() { + + P parser = parse("SELECT p.name 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.name) 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 // GH-3076 + 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"); + } + + @Test // GH-3895 + void shouldStripAliasesFromDtoProjection() { + + P parser = parse("SELECT sum(p.age) As age, p.foo as foo, p.bar AS bar 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(sum(p.age), p.foo, p.bar) from Person p"); + } + + @Test // GH-3895 + void shouldStripAliasesFromDtoProjectionWithSubquery() { + + P parser = parse( + "SELECT p.foo as foo, p.bar AS bar, cast(p.age as INTEGER) As age, (SELECT b.foo FROM Bar AS b) 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, cast(p.age as INTEGER), (SELECT b.foo FROM Bar AS b)) 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/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index 3fb97409f8..5f35722311 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; @@ -53,6 +54,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Ariel Morelli Andres */ class AbstractStringBasedJpaQueryUnitTests { @@ -135,10 +137,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/DefaultQueryUtilsUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryUtilsUnitTests.java index 0e6b4a577c..494dee24d4 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryUtilsUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryUtilsUnitTests.java @@ -361,7 +361,7 @@ void prefixesSingleNonAliasedFunctionCallRelatedSortProperty() { } @Test // DATAJPA-965, DATAJPA-970 - void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainesAliasedFunctionForDifferentProperty() { + void prefixesNonAliasedFunctionCallRelatedSortPropertyWhenSelectClauseContainsAliasedFunctionForDifferentProperty() { String query = "SELECT m.name, AVG(m.price) AS avgPrice FROM Magazine m"; Sort sort = Sort.by("name", "avgPrice"); 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..8bf5bc1ad4 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,11 +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}. @@ -63,4 +68,22 @@ void prefersFetchOverJoin() { assertThat(from.getJoins()).hasSize(1); } + @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/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/EqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java index 9ad73bae54..ded1101a54 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 @@ -952,6 +952,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/ExpressionBasedStringQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java index 2b81871822..049773d450 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 @@ -97,25 +97,13 @@ void shouldDetectBindParameterCountCorrectlyWithJDBCStyleParameters() { assertThat(query.getParameterBindings()).hasSize(8); } - @Test - void shouldDetectComplexNativeQueriesWithSpelAsNonNative() { - - StringQuery query = new ExpressionBasedStringQuery( - "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, PARSER, true); - - assertThat(query.isNativeQuery()).isFalse(); - } + @Test // GH-3979 + void shouldExpandExpressionUsingNativeQueries() { - @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, PARSER, true); - assertThat(query.isNativeQuery()).isFalse(); + assertThat(query.isNativeQuery()).isTrue(); + assertThat(query.getQueryString()).isEqualTo("select n.* from User n"); } @Test 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/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 6abc8b5048..d064fd4d3e 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 @@ -1713,6 +1713,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() { @@ -1724,6 +1730,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() { 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..6259ac88e0 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 @@ -115,7 +115,7 @@ void shouldCreateGraphWithDeepSubGraphCorrectly() { } @Test // DATAJPA-1041, DATAJPA-1075 - void shouldIgnoreIntermedeateSubGraphNodesThatAreNotNeeded() { + void shouldIgnoreIntermediateSubGraphNodesThatAreNotNeeded() { assumeThat(currentEntityManagerIsAJpa21EntityManager(em)).isTrue(); 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..d404a7438e --- /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(DeclaredQuery.of("SELECT /* foo */ some_alias FROM /* some other */ table_name some_alias", false)); + + assertThat(enhancer.getQuery().getQueryString()) + .isEqualToIgnoringCase("SELECT some_alias FROM table_name some_alias"); + + enhancer = enhancerFunction.apply(DeclaredQuery.of(""" + SELECT /* multi + line + comment + */ some_alias FROM /* some other */ table_name some_alias + """, false)); + + 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); + } +} 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..fbb6f8ae46 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 @@ -169,7 +169,7 @@ void allowsMethodReturnTypesForModifyingQuery() { @Test void modifyingExecutionRejectsNonIntegerOrVoidReturnType() { - when(method.getReturnType()).thenReturn((Class) Long.class); + when(method.getReturnType()).thenReturn((Class) String.class); assertThatIllegalArgumentException().isThrownBy(() -> new ModifyingExecution(method, em)); } 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) { - - } } 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..7b46397124 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 @@ -953,6 +953,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/QueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java index 1d4f917a5d..6a01b8ad29 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 @@ -44,6 +44,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import org.hibernate.query.hql.spi.SqmQueryImplementor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; @@ -59,6 +60,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}. @@ -384,6 +386,29 @@ void demonstrateDifferentBehaviorOfGetJoin() { assertThat(root.getJoins()).hasSize(getNumberOfJoinsAfterCreatingAPath()); } + @Test // GH-3983, GH-2870 + @Transactional + void applyAndBindOptimizesIn() { + + em.getCriteriaBuilder(); + SqmQueryImplementor query = (SqmQueryImplementor) 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(); + SqmQueryImplementor query = (SqmQueryImplementor) 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; } 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"); } } 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..546de1c49c 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.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,10 +40,11 @@ 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; +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; @@ -50,11 +53,13 @@ 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; import org.springframework.lang.Nullable; /** @@ -84,7 +89,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; @@ -100,12 +105,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); @@ -156,7 +157,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 +176,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" })); @@ -193,15 +192,16 @@ void doesNotValidateCountQueryIfNotPagingMethod() throws Exception { createJpaQuery(method); } - @Test // DATAJPA-352 - @SuppressWarnings("unchecked") + @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().isThrownBy(() -> createJpaQuery(method)).withMessageContaining("Count") + assertThatIllegalArgumentException() // + .isThrownBy(() -> createJpaQuery(method)) // + .withMessageContaining("User u") // .withMessageContaining(method.getName()); } @@ -239,10 +239,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)); @@ -263,6 +264,57 @@ 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")); + + String queryString = createQuery(jpaQuery); + + assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd"); + } + + @Test // GH-3895 + void rewriteQueryReturningDto() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("selectWithJoin")); + + String queryString = createQuery(jpaQuery); + + assertThat(queryString).startsWith( + "SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)"); + } + + @Test // GH-3895 + void rewritesQueryForUnknownProperty() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("projectWithUnknownPaths")); + + String queryString = createQuery(jpaQuery); + + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(u.unknown)"); + } + + @Test // GH-3895 + void rewritesQueryForJoinPath() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("projectWithJoinPaths")); + + String queryString = createQuery(jpaQuery); + + 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 void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception { @@ -274,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 { @@ -293,22 +356,31 @@ void resolvesExpressionInCountQuery() throws Exception { } private AbstractJpaQuery createJpaQuery(Method method) { - return createJpaQuery(method, null); + return createJpaQuery(method, Optional.empty()); } - private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) { + private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, String queryString, + @Nullable String countQueryString) { return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString, QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); } - private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional countQueryString) { + private AbstractJpaQuery createJpaQuery(Method method, 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.orElse(queryMethod.getCountQuery())); + } + + private String createQuery(AbstractStringBasedJpaQuery jpaQuery) { + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + return jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); } - interface SampleRepository { + interface SampleRepository extends Repository { @Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true) List findNativeByLastname(String lastname); @@ -334,11 +406,28 @@ 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("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); - - @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 @@ -347,4 +436,10 @@ interface SampleRepository { } interface UserProjection {} + + static class UnrelatedType { + + public UnrelatedType(String name) {} + + } } 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..313df7119e 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 @@ -236,6 +236,21 @@ void rewritesPositionalLikeToUniqueParametersIfNecessary() { assertThat(bindings).hasSize(3); } + @Test // GH-3907 + void rewritesPositionalLikeToUniqueParametersIfNecessaryUsingPostgresJsonbOperator() { + + StringQuery query = new StringQuery( + "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() { @@ -538,7 +553,6 @@ void treatsGreaterThanBindingAsSimpleBinding() { assertThat(bindings).hasSize(1); assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0)); - } @Test // DATAJPA-473 @@ -634,6 +648,19 @@ 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() { + + StringQuery query = new StringQuery( + "select '[\"x\", \"c\"]'::jsonb ?| array[?1]::text[] FROM foo WHERE foo BETWEEN ?1 and ?2", true); + + assertThat(query.getParameterBindings()).hasSize(2); + + query = new StringQuery("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() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java index 3321c2584d..65ffd8b055 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java @@ -73,7 +73,6 @@ void setUp() throws Exception { } @Test // DATAJPA-984 - @SuppressWarnings("unchecked") void returnsSingleTupleElementIfItMatchesExpectedType() { doReturn(Collections.singletonList(element)).when(tuple).getElements(); @@ -85,7 +84,6 @@ void returnsSingleTupleElementIfItMatchesExpectedType() { } @Test // DATAJPA-1024 - @SuppressWarnings("unchecked") void returnsNullForSingleElementTupleWithNullValue() { doReturn(Collections.singletonList(element)).when(tuple).getElements(); @@ -130,13 +128,15 @@ void dealsWithNullsInArguments() { assertThat(result).isInstanceOf(WithPC.class); } - @Test // GH-3076 + @Test // GH-3076, GH-4088 void fallsBackToCompatibleConstructor() { ReturnedType returnedType = spy( ReturnedType.of(MultipleConstructors.class, DomainType.class, new SpelAwareProxyProjectionFactory())); when(returnedType.isProjecting()).thenReturn(true); + when(returnedType.isDtoProjection()).thenReturn(true); when(returnedType.getInputProperties()).thenReturn(Arrays.asList("one", "two", "three")); + when(returnedType.hasInputProperties()).thenReturn(true); doReturn(List.of(element, element, element)).when(tuple).getElements(); when(tuple.get(eq(0))).thenReturn("one"); @@ -163,13 +163,15 @@ void fallsBackToCompatibleConstructor() { assertThat(result.three).isEqualTo(97); } - @Test // GH-3076 + @Test // GH-3076, GH-4088 void acceptsConstructorWithCastableType() { ReturnedType returnedType = spy( ReturnedType.of(MultipleConstructors.class, DomainType.class, new SpelAwareProxyProjectionFactory())); when(returnedType.isProjecting()).thenReturn(true); + when(returnedType.isDtoProjection()).thenReturn(true); when(returnedType.getInputProperties()).thenReturn(Arrays.asList("one", "two", "three", "four")); + when(returnedType.hasInputProperties()).thenReturn(true); doReturn(List.of(element, element, element, element)).when(tuple).getElements(); when(tuple.get(eq(0))).thenReturn("one"); @@ -185,13 +187,15 @@ void acceptsConstructorWithCastableType() { assertThat(result.four).isEqualTo(2, offset(0.1d)); } - @Test // GH-3076 + @Test // GH-3076, GH-4088 void failsForNonResolvableConstructor() { ReturnedType returnedType = spy( ReturnedType.of(MultipleConstructors.class, DomainType.class, new SpelAwareProxyProjectionFactory())); when(returnedType.isProjecting()).thenReturn(true); + when(returnedType.isDtoProjection()).thenReturn(true); when(returnedType.getInputProperties()).thenReturn(Arrays.asList("one", "two")); + when(returnedType.hasInputProperties()).thenReturn(true); doReturn(List.of(element, element)).when(tuple).getElements(); when(tuple.get(eq(0))).thenReturn(1); 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/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..5cbdb6f7bb 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 @@ -296,9 +296,17 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 Long removeByLastname(String lastname); + long removeOneByLastname(String lastname); + + int removeOneMoreByLastname(String lastname); + // DATAJPA-460 List deleteByLastname(String lastname); + User deleteOneByLastname(String lastname); + + Optional deleteOneOptionalByLastname(String lastname); + /** * @see OPENJPA-2484 */ @@ -613,7 +621,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 @@ -724,9 +732,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 +824,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) { } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryCustom.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryCustom.java index 86f454c0de..9ce9d3542c 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryCustom.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryCustom.java @@ -27,7 +27,7 @@ public interface UserRepositoryCustom { /** * Method actually triggering a finder but being overridden. */ - void findByOverrridingMethod(); + void findByOverridingMethod(); /** * Some custom method to implement. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryImpl.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryImpl.java index 965834876f..2603a724df 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryImpl.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepositoryImpl.java @@ -42,7 +42,7 @@ public void someCustomMethod(User u) { } @Override - public void findByOverrridingMethod() { + public void findByOverridingMethod() { LOG.debug("A method overriding a finder was invoked"); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextIntegrationTests.java index 6018de0c11..ff668241d0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/DefaultJpaContextIntegrationTests.java @@ -102,7 +102,7 @@ void rejectsUnmanagedType() { } @Test // DATAJPA-669 - void returnsEntitymanagerForUniqueType() { + void returnsEntityManagerForUniqueType() { assertThat(jpaContext.getEntityManagerByManagedType(Category.class)).isEqualTo(firstEm); } 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 } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBeanEntityPathResolverIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBeanEntityPathResolverIntegrationTests.java index d5f1f891af..eb30dbb634 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBeanEntityPathResolverIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBeanEntityPathResolverIntegrationTests.java @@ -77,7 +77,7 @@ void usesExplicitlyRegisteredEntityPathResolver() { } @Test // DATAJPA-1234, DATAJPA-1394 - void rejectsMulitpleEntityPathResolvers() { + void rejectsMultipleEntityPathResolvers() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> { 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..7613dd94c1 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 @@ -60,6 +60,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yanming Zhou + * @author Ariel Morelli Andres */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -84,6 +85,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); @@ -186,7 +190,6 @@ void doNothingWhenNewInstanceGetsDeleted() { newUser.setId(null); when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory); - when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); repo.delete(newUser); @@ -203,7 +206,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); 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 + + + + + + + + + + + + + + + + + + + + + + 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/modules/ROOT/pages/jpa/misc-merging-persistence-units.adoc b/src/main/antora/modules/ROOT/pages/jpa/misc-merging-persistence-units.adoc index 7b2165b665..5b09d6629e 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/misc-merging-persistence-units.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/misc-merging-persistence-units.adoc @@ -3,7 +3,7 @@ Spring supports having multiple persistence units. Sometimes, however, you might want to modularize your application but still make sure that all these modules run inside a single persistence unit. To enable that behavior, Spring Data JPA offers a `PersistenceUnitManager` implementation that automatically merges persistence units based on their name, as shown in the following example: -.Using MergingPersistenceUnitmanager +.Using MergingPersistenceUnitManager ==== [source, xml] ---- 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..69f5104580 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!) @@ -394,7 +394,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 <> for your particular arrangement. 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 diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index eedc4999e3..b0a1b58fdb 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -3,20 +3,21 @@ prerelease: ${antora-component.prerelease} asciidoc: attributes: - version: ${project.version} - copyright-year: ${current.year} - springversionshort: ${spring.short} - springversion: ${spring} attribute-missing: 'warn' - commons: ${springdata.commons.docs} + chomp: 'all' + 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 - spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/ - springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api + 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: '${documentation.baseurl}/spring-framework/reference/{springversionshort}' spring-framework-docs: '{springdocsurl}' + 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