From 33d532b4ebdc4d321998f7e2a3d48afdc98ca3a6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:31:35 +0200 Subject: [PATCH 01/93] Prepare next development iteration. See #3853 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index e5fefdec28..4b303a849a 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.0 + 3.5.1-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 40c50b7016..aabe6d3a97 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.1-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.0 + 3.5.1-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 9f3f2ad5c4..1ea8eaa775 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.1-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 2628916dc8..4f08ee7886 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.1-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.1-SNAPSHOT ../pom.xml From 42463aee139412812fb5e1b0d93c2f04c4a150e9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:31:36 +0200 Subject: [PATCH 02/93] After release cleanups. See #3853 --- Jenkinsfile | 2 +- pom.xml | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 915e46ddb7..673b2370bf 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 { diff --git a/pom.xml b/pom.xml index 4b303a849a..c756b9f1d6 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.0 + 3.5.1-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.5 - 3.5.0 + 3.5.1-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 + From a9813a7fc0ea60d94699c533e1ac506e031a4509 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 19 May 2025 08:44:36 +0200 Subject: [PATCH 03/93] =?UTF-8?q?Document=20deprecation=20of=20`Specificat?= =?UTF-8?q?ion.where(=E2=80=A6)`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, document the intent to make methods accepting `Specifications` to be non-nullable. Closes #3893 --- .../data/jpa/domain/Specification.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 9755052ed0..c8f67bc0bf 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 @@ -47,6 +47,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,19 +59,20 @@ static Specification not(@Nullable Specification spec) { ? (root, query, builder) -> null // : (root, query, builder) -> { - Predicate predicate = spec.toPredicate(root, query, builder); + Predicate predicate = spec.toPredicate(root, query, builder); return predicate != null ? builder.not(predicate) : builder.disjunction(); - }; + }; } /** * 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. */ @Deprecated(since = "3.5.0", forRemoval = true) static Specification where(@Nullable Specification spec) { @@ -80,6 +82,7 @@ static Specification where(@Nullable Specification 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 +94,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 +108,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}. */ From fa9b6815ce4854a9ac4b11415f2988225fff15ec Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Jun 2025 16:53:04 +0200 Subject: [PATCH 04/93] Avoid capturing `?&` and `?|` as bind parameter markers. We now exclude `?&` and `?|` from being matched as JDBC-style parameter bind marker. Closes #3907 --- .../jpa/repository/query/StringQuery.java | 13 +++++---- .../query/StringQueryUnitTests.java | 29 ++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) 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/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() { From c538a4fcb470866e1404c9fd35dd5862ee855d7c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 2 Jun 2025 10:54:06 +0200 Subject: [PATCH 05/93] Refrain from rewriting queries without input properties. We now no longer attempt to rewrite the query if the target type doesn't define input properties (no-args constructor or multiple constructors). Closes #3895 --- .../DtoProjectionTransformerDelegate.java | 2 +- .../AbstractDtoQueryTransformerUnitTests.java | 133 ++++++++++++++++++ .../EqlDtoQueryTransformerUnitTests.java | 92 ++---------- .../HqlDtoQueryTransformerUnitTests.java | 90 ++---------- .../JpqlDtoQueryTransformerUnitTests.java | 90 ++---------- 5 files changed, 158 insertions(+), 249 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java index 4593697a4d..4bed2ab685 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java @@ -42,7 +42,7 @@ public DtoProjectionTransformerDelegate(ReturnedType returnedType) { public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface() - || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { + || !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { return selectionList; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java new file mode 100644 index 0000000000..e72ded4fc7 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; + +import org.antlr.v4.runtime.tree.ParseTreeVisitor; +import org.junit.jupiter.api.Test; + +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; + +/** + * Support class for unit tests for {@link DtoProjectionTransformerDelegate}. + * + * @author Mark Paluch + */ +abstract class AbstractDtoQueryTransformerUnitTests

> { + + JpaQueryMethod method = getMethod("dtoProjection"); + + @Test // GH-3076 + void shouldTranslateSingleProjectionToDto() { + + P parser = parse("SELECT p from Person p"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + } + + @Test // GH-3076 + void shouldRewriteQueriesWithSubselect() { + + P parser = parse("select u from User u left outer join u.roles r where r in (select r from Role r)"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "select new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); + } + + @Test // GH-3076 + void shouldNotRewriteQueriesWithoutProperties() { + + JpaQueryMethod method = getMethod("noProjection"); + P parser = parse("select u from User u"); + + QueryTokenStream visit = getTransformer(parser, method).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("select u from User u"); + } + + @Test // GH-3076 + void shouldNotTranslateConstructorExpressionQuery() { + + P parser = parse("SELECT NEW com.foo(p) from Person p"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW com.foo(p) from Person p"); + } + + @Test + void shouldTranslatePropertySelectionToDto() { + + P parser = parse("SELECT p.foo, p.bar, sum(p.age) from Person p"); + + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); + + assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( + "SELECT new org.springframework.data.jpa.repository.query.AbstractDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + } + + private JpaQueryMethod getMethod(String name, Class... parameterTypes) { + + try { + Method method = MyRepo.class.getMethod(name, parameterTypes); + PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; + + return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), + new SpelAwareProxyProjectionFactory(), persistenceProvider); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + abstract P parse(String query); + + private ParseTreeVisitor getTransformer(P parser) { + return getTransformer(parser, method); + } + + abstract ParseTreeVisitor getTransformer(P parser, QueryMethod method); + + interface MyRepo extends Repository { + + MyRecord dtoProjection(); + + EmptyClass noProjection(); + } + + record Person(String id) { + + } + + record MyRecord(String foo, String bar) { + + } + + static class EmptyClass { + + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java index b8ac4f35a3..967af726e6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java @@ -15,102 +15,26 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; /** * Unit tests for {@link DtoProjectionTransformerDelegate}. * * @author Mark Paluch */ -class EqlDtoQueryTransformerUnitTests { - - JpaQueryMethod method = getMethod("dtoProjection"); - - @Test // GH-3076 - void shouldTranslateSingleProjectionToDto() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); - } - - @Test // GH-3076 - void shouldRewriteQueriesWithSubselect() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "select new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); - } - - @Test // GH-3076 - void shouldNotTranslateConstructorExpressionQuery() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("SELECT NEW Foo(p) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p"); - } - - @Test - void shouldTranslatePropertySelectionToDto() { - - JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser - .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); +class EqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests { - EqlSortedQueryTransformer transformer = getTransformer(parser); - QueryTokenStream visit = transformer.visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); + @Override + JpaQueryEnhancer.EqlQueryParser parse(String query) { + return JpaQueryEnhancer.EqlQueryParser.parseQuery(query); } - private JpaQueryMethod getMethod(String name, Class... parameterTypes) { - - try { - Method method = MyRepo.class.getMethod(name, parameterTypes); - PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; - - return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), - new SpelAwareProxyProjectionFactory(), persistenceProvider); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private EqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.EqlQueryParser parser) { + @Override + ParseTreeVisitor getTransformer(JpaQueryEnhancer.EqlQueryParser parser, QueryMethod method) { return new EqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), method.getResultProcessor().getReturnedType()); } - - interface MyRepo extends Repository { - - MyRecord dtoProjection(); - } - - record Person(String id) { - - } - - record MyRecord(String foo, String bar) { - - } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java index 9afdcd9764..4ac9f6a9c5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java @@ -15,101 +15,27 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; /** * Unit tests for {@link DtoProjectionTransformerDelegate}. * * @author Mark Paluch */ -class HqlDtoQueryTransformerUnitTests { - - JpaQueryMethod method = getMethod("dtoProjection"); - - @Test // GH-3076 - void shouldTranslateSingleProjectionToDto() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p"); +class HqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests { - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + @Override + JpaQueryEnhancer.HqlQueryParser parse(String query) { + return JpaQueryEnhancer.HqlQueryParser.parseQuery(query); } - @Test // GH-3076 - void shouldRewriteQueriesWithSubselect() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser - .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "select new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); - } - - @Test // GH-3076 - void shouldNotTranslateConstructorExpressionQuery() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser - .parseQuery("SELECT NEW String(p) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); - } - - @Test - void shouldTranslatePropertySelectionToDto() { - - JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser - .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); - } - - private JpaQueryMethod getMethod(String name, Class... parameterTypes) { - - try { - Method method = MyRepo.class.getMethod(name, parameterTypes); - PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; - - return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), - new SpelAwareProxyProjectionFactory(), persistenceProvider); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private HqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.HqlQueryParser parser) { + @Override + ParseTreeVisitor getTransformer(JpaQueryEnhancer.HqlQueryParser parser, QueryMethod method) { return new HqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), method.getResultProcessor().getReturnedType()); } - interface MyRepo extends Repository { - - MyRecord dtoProjection(); - } - - record Person(String id) { - - } - - record MyRecord(String foo, String bar) { - - } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java index d0c8fa1305..a82aaf7581 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java @@ -15,101 +15,27 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.*; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.provider.PersistenceProvider; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; /** * Unit tests for {@link DtoProjectionTransformerDelegate}. * * @author Mark Paluch */ -class JpqlDtoQueryTransformerUnitTests { - - JpaQueryMethod method = getMethod("dtoProjection"); - - @Test // GH-3076 - void shouldTranslateSingleProjectionToDto() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("SELECT p from Person p"); +class JpqlDtoQueryTransformerUnitTests extends AbstractDtoQueryTransformerUnitTests { - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); + @Override + JpaQueryEnhancer.JpqlQueryParser parse(String query) { + return JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); } - @Test // GH-3076 - void shouldRewriteQueriesWithSubselect() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "select new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); - } - - @Test // GH-3076 - void shouldNotTranslateConstructorExpressionQuery() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("SELECT NEW Foo(p) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW Foo(p) from Person p"); - } - - @Test - void shouldTranslatePropertySelectionToDto() { - - JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser - .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); - - QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); - - assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( - "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); - } - - private JpaQueryMethod getMethod(String name, Class... parameterTypes) { - - try { - Method method = MyRepo.class.getMethod(name, parameterTypes); - PersistenceProvider persistenceProvider = PersistenceProvider.HIBERNATE; - - return new JpaQueryMethod(method, new DefaultRepositoryMetadata(MyRepo.class), - new SpelAwareProxyProjectionFactory(), persistenceProvider); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private JpqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.JpqlQueryParser parser) { + @Override + ParseTreeVisitor getTransformer(JpaQueryEnhancer.JpqlQueryParser parser, QueryMethod method) { return new JpqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), method.getResultProcessor().getReturnedType()); } - interface MyRepo extends Repository { - - MyRecord dtoProjection(); - } - - record Person(String id) { - - } - - record MyRecord(String foo, String bar) { - - } } From 64d5e70e38c15d32755846e2ea77f43a67225afb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 2 Jun 2025 11:24:04 +0200 Subject: [PATCH 06/93] Do not consider JPA-managed types projections. We now back off from rewriting queries to constructor expressions if a returned type is a JPA-managed one. See #3895 --- .../query/AbstractStringBasedJpaQuery.java | 11 +- .../query/SimpleJpaQueryUnitTests.java | 117 +++++++++++++++--- 2 files changed, 107 insertions(+), 21 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 851a3918e0..2f2cd797d8 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 @@ -144,13 +144,12 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { * @param processor * @return */ - private ReturnedType getReturnedType(ResultProcessor processor) { + ReturnedType getReturnedType(ResultProcessor processor) { ReturnedType returnedType = processor.getReturnedType(); Class returnedJavaType = processor.getReturnedType().getReturnedType(); - if (query.isDefaultProjection() || !returnedType.isProjecting() || returnedJavaType.isInterface() - || query.isNativeQuery()) { + if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNativeQuery()) { return returnedType; } @@ -160,13 +159,17 @@ private ReturnedType getReturnedType(ResultProcessor processor) { return returnedType; } - if ((known != null && !known) || returnedJavaType.isArray()) { + if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) { if (known == null) { knownProjections.put(returnedJavaType, false); } return new NonProjectingReturnedType(returnedType); } + if (query.isDefaultProjection()) { + return returnedType; + } + String alias = query.getAlias(); String projection = query.getProjection(); 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..38f109dbd2 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; @@ -42,6 +44,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.NativeQuery; @@ -50,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" })); @@ -239,10 +238,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 +263,67 @@ void projectsWithManuallyDeclaredQuery() throws Exception { verify(em, times(2)).createQuery(anyString()); } + @Test // GH-3895 + void doesNotRewriteQueryReturningEntity() throws Exception { + + EntityType entityType = mock(EntityType.class); + when(entityType.getJavaType()).thenReturn((Class) UnrelatedType.class); + when(metamodel.getManagedTypes()).thenReturn(Set.of(entityType)); + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("selectWithJoin")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd"); + } + + @Test // GH-3895 + void rewriteQueryReturningDto() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("selectWithJoin")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith( + "SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)"); + } + + @Test // GH-3895 + void doesNotRewriteQueryForUnknownProperty() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("projectWithUnknownPaths")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith("select u.unknown from User u"); + } + + @Test // GH-3895 + void doesNotRewriteQueryForJoinPath() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("projectWithJoinPaths")); + + JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( + jpaQuery.getQueryMethod().getParameters(), new Object[0]); + ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + + assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r"); + } + @Test // DATAJPA-1307 void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception { @@ -296,7 +357,8 @@ private AbstractJpaQuery createJpaQuery(Method method) { return createJpaQuery(method, null); } - private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) { + private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, + @Nullable String countQueryString) { return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString, QueryRewriter.IdentityQueryRewriter.INSTANCE, ValueExpressionDelegate.create()); @@ -305,10 +367,11 @@ private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable St private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional countQueryString) { JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor); - return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery())); + return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), + countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery())); } - interface SampleRepository { + interface SampleRepository extends Repository { @Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true) List findNativeByLastname(String lastname); @@ -334,11 +397,25 @@ interface SampleRepository { @Query("select u from User u") Collection projectWithExplicitQuery(); + @Query(""" + SELECT cd FROM CampaignDeal cd + LEFT JOIN FETCH cd.dealLibrary d + LEFT JOIN FETCH d.publisher p + WHERE cd.campaignId = :campaignId + """) + Collection selectWithJoin(); + + @Query("select u.unknown from User u") + Collection projectWithUnknownPaths(); + + @Query("select r.name from User u LEFT JOIN FETCH u.roles r") + Collection projectWithJoinPaths(); + @Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u") List findAllWithExpressionInCountQuery(Pageable pageable); - - @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 +424,10 @@ interface SampleRepository { } interface UserProjection {} + + static class UnrelatedType { + + public UnrelatedType(String name) {} + + } } From 61a1a8c2c9d1a263e9bbb02bb3b76793682a12fd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Jun 2025 11:15:01 +0200 Subject: [PATCH 07/93] Fix potential class-cast exception. See #3895 --- .../data/jpa/repository/query/QueryRenderer.java | 14 ++++++++++++++ .../ROOT/pages/repositories/projections.adoc | 7 +++++-- 2 files changed, 19 insertions(+), 2 deletions(-) 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/src/main/antora/modules/ROOT/pages/repositories/projections.adoc b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc index 0eb4682ff2..a9df80376a 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/projections.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/projections.adoc @@ -32,9 +32,12 @@ Support for string-based queries covers both, JPQL queries(`@Query`) and native ==== JPQL Queries -When using <> with JPQL, you must use *constructor expressions* in your JPQL query, e.g. `SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u`. +JPA's mechanism to return <> using JPQL is *constructor expressions*. +Therefore, your query must define a constructor expression such as `SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u`. (Note the usage of a FQDN for the DTO type!) This JPQL expression can be used in `@Query` annotations as well where you define any named queries. -As a workaround you may use named queries with `ResultSetMapping` or the Hibernate-specific javadoc:{hibernatejavadocurl}org.hibernate.query.ResultListTransformer[] +As a workaround you may use named queries with `ResultSetMapping` or the Hibernate-specific javadoc:{hibernatejavadocurl}org.hibernate.query.ResultListTransformer[]. + +Spring Data JPA can aid with rewriting your query to a constructor expression if your query selects the primary entity or a list of select items. ===== DTO Projection JPQL Query Rewriting From 5a22fbf467e0d0fbab00eff4ce4d45763346c104 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Jun 2025 11:16:40 +0200 Subject: [PATCH 08/93] Refine DTO projection rewriting. We now consider dropping aliases (count(foo) as foo), support subselects and capture individual select items to avoid contextual information loss. Also, added a series of tests to cover edgecases. See #3895 --- .../repository/query/AbstractJpaQuery.java | 3 +- .../query/AbstractStringBasedJpaQuery.java | 49 +-- .../DtoProjectionTransformerDelegate.java | 102 +++++-- .../repository/query/EqlQueryRenderer.java | 20 +- .../query/EqlSortedQueryTransformer.java | 72 +++-- .../query/HqlCountQueryTransformer.java | 3 +- .../repository/query/HqlQueryRenderer.java | 2 +- .../query/HqlSortedQueryTransformer.java | 16 +- .../repository/query/JpqlQueryRenderer.java | 13 +- .../query/JpqlSortedQueryTransformer.java | 72 +++-- .../EclipseLinkUserRepositoryFinderTests.java | 4 - ...ipseLinkUserRepositoryProjectionTests.java | 32 ++ .../repository/UserRepositoryFinderTests.java | 165 ---------- .../UserRepositoryProjectionTests.java | 286 ++++++++++++++++++ .../AbstractDtoQueryTransformerUnitTests.java | 38 ++- .../query/SimpleJpaQueryUnitTests.java | 37 ++- .../jpa/repository/sample/UserRepository.java | 17 +- 17 files changed, 592 insertions(+), 339 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryProjectionTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryProjectionTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index fb9821c184..45d5ed13b7 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; @@ -474,7 +475,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 2f2cd797d8..f447716c10 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; @@ -166,52 +164,7 @@ ReturnedType getReturnedType(ResultProcessor processor) { return new NonProjectingReturnedType(returnedType); } - if (query.isDefaultProjection()) { - return 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 4bed2ab685..28de9ba657 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DtoProjectionTransformerDelegate.java @@ -17,6 +17,11 @@ import static org.springframework.data.jpa.repository.query.QueryTokens.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + import org.springframework.data.repository.query.ReturnedType; /** @@ -25,7 +30,8 @@ * Query rewriting from a plain property/object selection towards constructor expression only works if either: *

    *
  • The query selects its primary alias ({@code SELECT p FROM Person p})
  • - *
  • The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p})
  • + *
  • The query specifies a property list ({@code SELECT p.foo, p.bar FROM Person p}, + * {@code SELECT COUNT(p.foo), p.bar AS bar FROM Person p})
  • *
* * @author Mark Paluch @@ -34,42 +40,94 @@ class DtoProjectionTransformerDelegate { private final ReturnedType returnedType; + private final boolean applyRewriting; + private final List selectItems = new ArrayList<>(); public DtoProjectionTransformerDelegate(ReturnedType returnedType) { this.returnedType = returnedType; + this.applyRewriting = returnedType.isProjecting() && !returnedType.getReturnedType().isInterface() + && returnedType.needsCustomConstruction(); + } + + public boolean applyRewriting() { + return applyRewriting; + } + + public boolean canRewrite() { + return applyRewriting() && !selectItems.isEmpty(); + } + + public void appendSelectItem(QueryTokenStream selectItem) { + + if (applyRewriting()) { + selectItems.add(new DetachedStream(selectItem)); + } } - public QueryTokenStream transformSelectionList(QueryTokenStream selectionList) { + public QueryTokenStream getRewrittenSelectionList() { + + if (canRewrite()) { + + QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); + builder.append(QueryTokens.TOKEN_NEW); + builder.append(QueryTokens.token(returnedType.getReturnedType().getName())); + builder.append(QueryTokens.TOKEN_OPEN_PAREN); + + if (selectItems.size() == 1 && selectItems.get(0).size() == 1) { - if (!returnedType.isProjecting() || returnedType.getReturnedType().isInterface() - || !returnedType.needsCustomConstruction() || selectionList.stream().anyMatch(it -> it.equals(TOKEN_NEW))) { - return selectionList; + builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> { + + QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder(); + prop.appendInline(selectItems.get(0)); + prop.append(QueryTokens.TOKEN_DOT); + prop.append(QueryTokens.token(property)); + + return prop.build(); + }, QueryTokens.TOKEN_COMMA)); + } else { + builder.append(QueryTokenStream.concat(selectItems, Function.identity(), TOKEN_COMMA)); + } + + builder.append(TOKEN_CLOSE_PAREN); + + return builder.build(); } - QueryRenderer.QueryRendererBuilder builder = QueryRenderer.builder(); - builder.append(QueryTokens.TOKEN_NEW); - builder.append(QueryTokens.token(returnedType.getReturnedType().getName())); - builder.append(QueryTokens.TOKEN_OPEN_PAREN); + return QueryTokenStream.empty(); + } + + private static class DetachedStream extends QueryRenderer { - // assume the selection points to the document - if (selectionList.size() == 1) { + private final QueryTokenStream delegate; - builder.appendInline(QueryTokenStream.concat(returnedType.getInputProperties(), property -> { + private DetachedStream(QueryTokenStream delegate) { + this.delegate = delegate; + } - QueryRenderer.QueryRendererBuilder prop = QueryRenderer.builder(); - prop.append(QueryTokens.token(selectionList.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..1da19ca211 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; } @@ -2448,8 +2453,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..e54979e008 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 (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + 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/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..a6936ddbb9 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 @@ -830,7 +830,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..43083634ff 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 (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.instantiation() == null && !isSubquery(ctx)) { + 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/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index 41ca183967..fae722d524 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; } @@ -2114,7 +2121,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..cca1ae1985 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 (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + 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/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/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/query/AbstractDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java index e72ded4fc7..fcede5da49 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractDtoQueryTransformerUnitTests.java @@ -38,7 +38,7 @@ abstract class AbstractDtoQueryTransformerUnitTests

... parameterTypes) { try { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 38f109dbd2..e2934c84df 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 @@ -273,10 +273,7 @@ void doesNotRewriteQueryReturningEntity() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("selectWithJoin")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); assertThat(queryString).startsWith("SELECT cd FROM CampaignDeal cd"); } @@ -287,41 +284,34 @@ void rewriteQueryReturningDto() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("selectWithJoin")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); assertThat(queryString).startsWith( "SELECT new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(cd.name)"); } @Test // GH-3895 - void doesNotRewriteQueryForUnknownProperty() throws Exception { + void rewritesQueryForUnknownProperty() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("projectWithUnknownPaths")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); - assertThat(queryString).startsWith("select u.unknown from User u"); + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(u.unknown)"); } @Test // GH-3895 - void doesNotRewriteQueryForJoinPath() throws Exception { + void rewritesQueryForJoinPath() throws Exception { AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( SampleRepository.class.getMethod("projectWithJoinPaths")); - JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor( - jpaQuery.getQueryMethod().getParameters(), new Object[0]); - ResultProcessor processor = jpaQuery.getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - String queryString = jpaQuery.getSortedQueryString(Sort.unsorted(), jpaQuery.getReturnedType(processor)); + String queryString = createQuery(jpaQuery); - assertThat(queryString).startsWith("select r.name from User u LEFT JOIN FETCH u.roles r"); + assertThat(queryString).startsWith( + "select new org.springframework.data.jpa.repository.query.SimpleJpaQueryUnitTests$UnrelatedType(r.name) from User u LEFT JOIN FETCH u.roles r"); } @Test // DATAJPA-1307 @@ -371,6 +361,13 @@ private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional { @Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 88efe091e0..c7ec3362f5 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 @@ -724,9 +724,18 @@ List findAllAndSortByFunctionResultNamedParameter(@Param("namedParameter @Query("select u from User u") List findRecordProjection(); - @Query("select u.firstname, LOWER(u.lastname) from User u") + @Query("select u.firstname as fn, LOWER(u.lastname) as lastname from User u") List findRecordProjectionWithFunctions(); + @Query("select u.firstname from User u") + List findStringProjection(); + + @Query("select u.firstname, LOWER(u.lastname) from User u") + List findObjectArrayProjectionWithFunctions(); + + @Query("select u.address from User u") + List findAddressProjection(); + @Query("select u from User u") List findRecordProjection(Class projectionType); @@ -807,6 +816,12 @@ record UserExcerpt(String firstname, String lastname) { } + record AddressDto(String country, String city) { + public AddressDto(Address address) { + this(address != null ? address.getCountry() : null, address != null ? address.getCity() : null); + } + } + record UserRoleCountDtoProjection(User user, Long roleCount) { } From 053a4620a04e4c1bd3c2162404b11dc2143412c6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 4 Jun 2025 11:21:55 +0200 Subject: [PATCH 09/93] Consider only top-level properties for tuple query selection. We now only consider top-level properties for tuple query selection to avoid join products caused by selecting nested relationships. Closes #3908 --- .../repository/support/SimpleJpaRepository.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 9f649069c2..12d92cef7d 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; @@ -817,11 +819,18 @@ private TypedQuery getQuery(ReturnedType returnedType, @Nullabl } List> selections = new ArrayList<>(); - + Set topLevelProperties = new HashSet<>(); for (String property : requiredSelection) { - PropertyPath path = PropertyPath.from(property, returnedType.getDomainType()); - selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(property)); + int separator = property.indexOf('.'); + String topLevelProperty = separator == -1 ? property : property.substring(0, separator); + + if (!topLevelProperties.add(topLevelProperty)) { + continue; + } + + PropertyPath path = PropertyPath.from(topLevelProperty, returnedType.getDomainType()); + selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(topLevelProperty)); } Class typeToRead = returnedType.getReturnedType(); From 15e40966bac11f64b889a6a6dc470f5b29467aba Mon Sep 17 00:00:00 2001 From: Aref Date: Tue, 27 May 2025 09:37:13 +0330 Subject: [PATCH 10/93] Suppress warnings in tests. Signed-off-by: Aref Closes: #3901 --- .../data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java index a1daf39d27..41d24a1ed6 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java @@ -31,7 +31,7 @@ */ class Jsr310JpaConvertersUnitTests { - static Iterable data() { + static Iterable data() { return Arrays.asList(new Jsr310JpaConverters.InstantConverter(), // new Jsr310JpaConverters.LocalDateConverter(), // From 4838fa104e6e396956772ff81147e3e746d43be8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 4 Jun 2025 14:43:54 +0200 Subject: [PATCH 11/93] Polishing. Move warning suppression to the class-level. See #3901 --- .../jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java | 5 ++--- .../data/jpa/domain/SpecificationUnitTests.java | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java index 41d24a1ed6..691d1a83d9 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/convert/threeten/Jsr310JpaConvertersUnitTests.java @@ -17,10 +17,10 @@ import static org.assertj.core.api.Assertions.*; -import java.util.Arrays; - import jakarta.persistence.AttributeConverter; +import java.util.Arrays; + import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -40,7 +40,6 @@ static Iterable data() { new Jsr310JpaConverters.ZoneIdConverter()); } - @ParameterizedTest @MethodSource("data") void convertersHandleNullValuesCorrectly(AttributeConverter converter) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/SpecificationUnitTests.java index eba6ed8851..4768c15947 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 @@ -46,7 +46,7 @@ * @author Mark Paluch * @author Daniel Shuy */ -@SuppressWarnings("removal") +@SuppressWarnings({ "unchecked", "deprecation", "removal" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class SpecificationUnitTests { @@ -55,7 +55,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 +162,6 @@ void specificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation" }) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -178,7 +176,6 @@ void complexSpecificationsShouldBeSerializable() { assertThat(specification).isNotNull(); - @SuppressWarnings({ "unchecked", "deprecation" }) Specification transferredSpecification = (Specification) deserialize(serialize(specification)); assertThat(transferredSpecification).isNotNull(); @@ -191,7 +188,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); From 71ef3215d7a93e5f1c62aca41907a266c276c0a3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 5 Jun 2025 09:21:44 +0200 Subject: [PATCH 12/93] Upgrade to Hibernate 6.6.17.Final. Closes #3909 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index c756b9f1d6..f5845e7679 100755 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,9 @@ 4.13.0 4.0.6 4.0.7-SNAPSHOT - 6.6.15.Final - 6.2.36.Final - 6.6.16-SNAPSHOT + 6.6.17.Final + 6.2.38.Final + 6.6.18-SNAPSHOT 7.0.0.Beta5 7.0.0-SNAPSHOT 2.7.4 From c8221aa7c1ff6cef836426259526a4ab169e97b9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 5 Jun 2025 11:02:56 +0200 Subject: [PATCH 13/93] Prevent access to `EntityManager` when looking up `PersistenceProvider`. Signed-off-by: Ariel Morelli Andres Closes: #3425 Original pull request: #3885 --- .../jpa/provider/PersistenceProvider.java | 72 +++++++++++++-- .../CrudMethodMetadataUnitTests.java | 6 +- ...ernateCurrentTenantIdentifierResolver.java | 49 +++++++++++ .../HibernateMultitenancyTests.java | 88 +++++++++++++++++++ .../AbstractStringBasedJpaQueryUnitTests.java | 4 + .../support/SimpleJpaRepositoryUnitTests.java | 5 +- .../src/test/resources/multitenancy-test.xml | 40 +++++++++ 7 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java create mode 100644 spring-data-jpa/src/test/resources/multitenancy-test.xml diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 2b5e0abbeb..c8628861fd 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -19,6 +19,7 @@ import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Query; import jakarta.persistence.metamodel.IdentifiableType; import jakarta.persistence.metamodel.Metamodel; @@ -36,6 +37,7 @@ import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.proxy.HibernateProxy; + import org.springframework.data.util.CloseableIterator; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -52,6 +54,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yuriy Tsarkov + * @author Ariel Morelli Andres (Atlassian US, Inc.) */ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment { @@ -64,6 +67,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer * @see DATAJPA-444 */ HIBERNATE(// + Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), // Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) { @@ -71,7 +75,6 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer 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,7 +117,8 @@ public String getCommentHintKey() { /** * EclipseLink persistence provider. */ - ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), + ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2), + Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) { @Override @@ -147,12 +151,14 @@ 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(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), + Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { @Nullable @Override @@ -199,6 +205,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 entityManagerFactoryClassNames; private final Iterable entityManagerClassNames; private final Iterable metamodelClassNames; @@ -207,24 +214,38 @@ public String getCommentHintKey() { /** * Creates a new {@link PersistenceProvider}. * + * @param entityManagerFactoryClassNames the names of the provider specific + * {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty. * @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not * be {@literal null} or empty. * @param metamodelClassNames must not be {@literal null}. */ - PersistenceProvider(Iterable entityManagerClassNames, Iterable metamodelClassNames) { + PersistenceProvider(Iterable entityManagerFactoryClassNames, Iterable entityManagerClassNames, + Iterable metamodelClassNames) { + this.entityManagerFactoryClassNames = entityManagerFactoryClassNames; this.entityManagerClassNames = entityManagerClassNames; this.metamodelClassNames = metamodelClassNames; boolean present = false; - for (String entityManagerClassName : entityManagerClassNames) { + for (String emfClassName : entityManagerFactoryClassNames) { - if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) { + if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) { present = true; break; } } + if (!present) { + for (String entityManagerClassName : entityManagerClassNames) { + + if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) { + present = true; + break; + } + } + } + this.present = present; } @@ -269,6 +290,36 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { return cacheAndReturn(entityManagerType, GENERIC_JPA); } + /** + * 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}. + */ + public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) { + + Assert.notNull(emf, "EntityManagerFactory must not be null"); + + Class entityManagerType = emf.getPersistenceUnitUtil().getClass(); + PersistenceProvider cachedProvider = CACHE.get(entityManagerType); + + if (cachedProvider != null) { + return cachedProvider; + } + + for (PersistenceProvider provider : ALL) { + for (String emfClassName : provider.entityManagerFactoryClassNames) { + if (isOfType(emf.getPersistenceUnitUtil(), emfClassName, + emf.getPersistenceUnitUtil().getClass().getClassLoader())) { + return cacheAndReturn(entityManagerType, provider); + } + } + } + + return cacheAndReturn(entityManagerType, GENERIC_JPA); + } + /** * Determines the {@link PersistenceProvider} from the given {@link Metamodel}. If no special one can be determined * {@link #GENERIC_JPA} will be returned. @@ -354,13 +405,20 @@ public boolean isPresent() { */ interface Constants { + String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory"; String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager"; + + String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate"; + String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl"; String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager"; + // needed as Spring only exposes that interface via the EM proxy + String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl"; String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor"; String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel"; String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; + } public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { @@ -465,5 +523,7 @@ public void close() { scrollableCursor.close(); } } + } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java index 52e217bb71..69e7379d19 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/CrudMethodMetadataUnitTests.java @@ -17,9 +17,6 @@ import static org.mockito.Mockito.*; -import java.util.Collections; -import java.util.Map; - import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.LockModeType; @@ -28,6 +25,9 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.metamodel.Metamodel; +import java.util.Collections; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java new file mode 100644 index 0000000000..436e99fb31 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2011-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository; + +import java.util.Optional; + +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.jspecify.annotations.Nullable; + +/** + * {@code CurrentTenantIdentifierResolver} instance for testing + * + * @author Ariel Morelli Andres (Atlassian US, Inc.) + */ +public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { + private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>(); + + public static void setTenantIdentifier(String tenantIdentifier) { + CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier); + } + + public static void removeTenantIdentifier() { + CURRENT_TENANT_IDENTIFIER.remove(); + } + + @Override + public String resolveCurrentTenantIdentifier() { + return Optional.ofNullable(CURRENT_TENANT_IDENTIFIER.get()) + .orElseThrow(() -> new IllegalArgumentException("Could not resolve current tenant identifier")); + } + + @Override + public boolean validateExistingCurrentSessions() { + return true; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java new file mode 100644 index 0000000000..3de19e90d8 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.sample.RoleRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; + +/** + * Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected + * despite not having a tenant available at creation time + * + * @author Ariel Morelli Andres (Atlassian US, Inc.) + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration() +class HibernateMultitenancyTests { + + @Autowired RoleRepository roleRepository; + @Autowired EntityManager em; + + @AfterEach + void tearDown() { + HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier(); + } + + @Test + void testPersistenceProviderFromFactoryWithoutTenant() { + PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory()); + assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE); + } + + @Test + void testRepositoryWithTenant() { + HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id"); + assertThatNoException().isThrownBy(() -> roleRepository.findAll()); + } + + @Test + void testRepositoryWithoutTenantFails() { + assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class); + } + + @Transactional + List insertAndQuery() { + roleRepository.save(new Role("DRUMMER")); + roleRepository.flush(); + return roleRepository.findAll(); + } + + @ImportResource({ "classpath:multitenancy-test.xml" }) + @Configuration + @EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE)) + static class TestConfig {} +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java index 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/support/SimpleJpaRepositoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index afd9634b44..eac8e8e824 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 @@ -44,7 +44,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; @@ -60,6 +59,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yanming Zhou + * @author Ariel Morelli Andres (Atlassian US, Inc.) */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -84,6 +84,9 @@ class SimpleJpaRepositoryUnitTests { void setUp() { when(em.getDelegate()).thenReturn(em); + when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory); + + when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); when(information.getJavaType()).thenReturn(User.class); when(em.getCriteriaBuilder()).thenReturn(builder); diff --git a/spring-data-jpa/src/test/resources/multitenancy-test.xml b/spring-data-jpa/src/test/resources/multitenancy-test.xml new file mode 100644 index 0000000000..d1ff786d12 --- /dev/null +++ b/spring-data-jpa/src/test/resources/multitenancy-test.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + org.springframework.data.jpa.repository.HibernateCurrentTenantIdentifierResolver + + + + + + + + + + + + + + + + + + + + + + From 5bd94409dcf2c2b12b2d14d4e42f62f0ec530702 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 5 Jun 2025 11:06:29 +0200 Subject: [PATCH 14/93] Polishing. Revise PersistenceProvider detection to a EntityManagerFactory-based variant, considering EntityManagerFactory proxying. See: #3425 Original pull request: #3885 --- .../data/jpa/provider/JpaClassUtils.java | 2 +- .../jpa/provider/PersistenceProvider.java | 112 +++++++----------- .../PersistenceProviderUnitTests.java | 61 ++++++---- ...ernateCurrentTenantIdentifierResolver.java | 15 +-- .../HibernateMultitenancyTests.java | 28 +++-- .../support/SimpleJpaRepositoryUnitTests.java | 5 +- 6 files changed, 111 insertions(+), 112 deletions(-) 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 c8628861fd..62f0c45f94 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -25,11 +25,14 @@ import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.SingularAttribute; +import java.lang.reflect.Proxy; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; +import java.util.function.LongSupplier; +import java.util.stream.Stream; import org.eclipse.persistence.config.QueryHints; import org.eclipse.persistence.jpa.JpaQuery; @@ -38,6 +41,8 @@ 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; @@ -54,22 +59,15 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yuriy Tsarkov - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment { /** * Hibernate persistence provider. - *

- * Since Hibernate 4.3 the location of the HibernateEntityManager moved to the org.hibernate.jpa package. In order to - * support both locations we interpret both classnames as a Hibernate {@code PersistenceProvider}. - * - * @see DATAJPA-444 */ - HIBERNATE(// - Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // - Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), // - Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) { + HIBERNATE(List.of(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), // + List.of(HIBERNATE_JPA_METAMODEL_TYPE)) { @Override public String extractQueryString(Query query) { @@ -117,9 +115,7 @@ public String getCommentHintKey() { /** * EclipseLink persistence provider. */ - ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2), - Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), - Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) { + ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE), List.of(ECLIPSELINK_JPA_METAMODEL_TYPE)) { @Override public String extractQueryString(Query query) { @@ -157,8 +153,7 @@ public String getCommentHintValue(String comment) { /** * Unknown special provider. Use standard JPA. */ - GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), - Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) { + GENERIC_JPA(List.of(GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE), Collections.emptySet()) { @Nullable @Override @@ -205,8 +200,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 entityManagerFactoryClassNames; - private final Iterable entityManagerClassNames; + final Iterable entityManagerFactoryClassNames; private final Iterable metamodelClassNames; private final boolean present; @@ -216,37 +210,15 @@ public String getCommentHintKey() { * * @param entityManagerFactoryClassNames the names of the provider specific * {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty. - * @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not - * be {@literal null} or empty. - * @param metamodelClassNames must not be {@literal null}. + * @param metamodelClassNames the names of the provider specific {@link Metamodel} implementations. Must not be + * {@literal null} or empty. */ - PersistenceProvider(Iterable entityManagerFactoryClassNames, Iterable entityManagerClassNames, - Iterable metamodelClassNames) { + PersistenceProvider(Collection entityManagerFactoryClassNames, Collection metamodelClassNames) { this.entityManagerFactoryClassNames = entityManagerFactoryClassNames; - this.entityManagerClassNames = entityManagerClassNames; this.metamodelClassNames = metamodelClassNames; - - boolean present = false; - for (String emfClassName : entityManagerFactoryClassNames) { - - if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) { - present = true; - break; - } - } - - if (!present) { - for (String entityManagerClassName : entityManagerClassNames) { - - if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) { - present = true; - break; - } - } - } - - this.present = present; + this.present = Stream.concat(entityManagerFactoryClassNames.stream(), metamodelClassNames.stream()) + .anyMatch(it -> ClassUtils.isPresent(it, PersistenceProvider.class.getClassLoader())); } /** @@ -262,32 +234,23 @@ private static PersistenceProvider cacheAndReturn(Class type, PersistenceProv } /** - * Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be + * Determines the {@link PersistenceProvider} from the given {@link EntityManager} by introspecting + * {@link EntityManagerFactory} via {@link EntityManager#getEntityManagerFactory()}. If no special one can be * determined {@link #GENERIC_JPA} will be returned. + *

+ * This method avoids {@link EntityManager} initialization when using + * {@link org.springframework.orm.jpa.SharedEntityManagerCreator} by accessing + * {@link EntityManager#getEntityManagerFactory()}. * * @param em must not be {@literal null}. * @return will never be {@literal null}. + * @see org.springframework.orm.jpa.SharedEntityManagerCreator */ public static PersistenceProvider fromEntityManager(EntityManager em) { Assert.notNull(em, "EntityManager must not be null"); - Class entityManagerType = em.getDelegate().getClass(); - PersistenceProvider cachedProvider = CACHE.get(entityManagerType); - - if (cachedProvider != null) { - return cachedProvider; - } - - for (PersistenceProvider provider : ALL) { - for (String entityManagerClassName : provider.entityManagerClassNames) { - if (isEntityManagerOfType(em, entityManagerClassName)) { - return cacheAndReturn(entityManagerType, provider); - } - } - } - - return cacheAndReturn(entityManagerType, GENERIC_JPA); + return fromEntityManagerFactory(em.getEntityManagerFactory()); } /** @@ -296,12 +259,24 @@ public static PersistenceProvider fromEntityManager(EntityManager em) { * * @param emf must not be {@literal null}. * @return will never be {@literal null}. + * @since 3.5.1 */ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) { Assert.notNull(emf, "EntityManagerFactory must not be null"); - Class entityManagerType = emf.getPersistenceUnitUtil().getClass(); + EntityManagerFactory unwrapped = emf; + + while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) { + + if (Proxy.isProxyClass(unwrapped.getClass())) { + unwrapped = unwrapped.unwrap(null); + } else if (AopUtils.isAopProxy(unwrapped)) { + unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped); + } + } + + Class entityManagerType = unwrapped.getClass(); PersistenceProvider cachedProvider = CACHE.get(entityManagerType); if (cachedProvider != null) { @@ -310,8 +285,7 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory for (PersistenceProvider provider : ALL) { for (String emfClassName : provider.entityManagerFactoryClassNames) { - if (isOfType(emf.getPersistenceUnitUtil(), emfClassName, - emf.getPersistenceUnitUtil().getClass().getClassLoader())) { + if (isOfType(unwrapped, emfClassName, unwrapped.getClass().getClassLoader())) { return cacheAndReturn(entityManagerType, provider); } } @@ -408,16 +382,14 @@ interface Constants { String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory"; String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager"; - String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate"; - String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl"; + String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManagerFactory"; String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager"; + String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; // needed as Spring only exposes that interface via the EM proxy - String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl"; - String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor"; - + String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.SessionFactory"; + String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.Session"; String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel"; - String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl"; } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java index ba7c3abed7..66d55e2397 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java @@ -16,18 +16,20 @@ package org.springframework.data.jpa.provider; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import static org.springframework.data.jpa.provider.PersistenceProvider.*; import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import java.util.Arrays; import java.util.Map; -import org.assertj.core.api.Assumptions; -import org.hibernate.Version; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import org.springframework.asm.ClassWriter; @@ -42,6 +44,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Jens Schauder + * @author Mark Paluch */ class PersistenceProviderUnitTests { @@ -56,12 +59,32 @@ void setup() { this.shadowingClassLoader = new ShadowingClassLoader(getClass().getClassLoader()); } + @ParameterizedTest // GH-3425 + @EnumSource(PersistenceProvider.class) + void entityManagerFactoryClassNamesAreInterfaces(PersistenceProvider provider) throws ClassNotFoundException { + + for (String className : provider.entityManagerFactoryClassNames) { + assertThat(ClassUtils.forName(className, PersistenceProvider.class.getClassLoader()).isInterface()).isTrue(); + } + } + + @ParameterizedTest // GH-3425 + @EnumSource(PersistenceProvider.class) + void metaModelNamesExist(PersistenceProvider provider) throws ClassNotFoundException { + + for (String className : provider.entityManagerFactoryClassNames) { + assertThat(ClassUtils.forName(className, PersistenceProvider.class.getClassLoader()).isInterface()).isNotNull(); + } + } + @Test void detectsEclipseLinkPersistenceProvider() throws Exception { shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa"); EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE); + when(em.getEntityManagerFactory()) + .thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE)); assertThat(fromEntityManager(em)).isEqualTo(ECLIPSELINK); } @@ -70,31 +93,19 @@ void detectsEclipseLinkPersistenceProvider() throws Exception { void fallbackToGenericJpaForUnknownPersistenceProvider() throws Exception { EntityManager em = mockProviderSpecificEntityManagerInterface("foo.bar.unknown.jpa.JpaEntityManager"); + when(em.getEntityManagerFactory()).thenReturn(mock(EntityManagerFactory.class)); assertThat(fromEntityManager(em)).isEqualTo(GENERIC_JPA); } - @Test // DATAJPA-1019 - void detectsHibernatePersistenceProviderForHibernateVersion52() throws Exception { - - Assumptions.assumeThat(Version.getVersionString()).startsWith("5.2"); - - shadowingClassLoader.excludePackage("org.hibernate"); - - EntityManager em = mockProviderSpecificEntityManagerInterface(HIBERNATE_ENTITY_MANAGER_INTERFACE); - - assertThat(fromEntityManager(em)).isEqualTo(HIBERNATE); - } - @Test // DATAJPA-1379 void detectsProviderFromProxiedEntityManager() throws Exception { shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa"); - EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE); - EntityManager emProxy = Mockito.mock(EntityManager.class); - Mockito.when(emProxy.getDelegate()).thenReturn(em); + when(emProxy.getEntityManagerFactory()) + .thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE)); assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK); } @@ -105,13 +116,23 @@ private EntityManager mockProviderSpecificEntityManagerInterface(String interfac EntityManager.class); EntityManager em = (EntityManager) Mockito.mock(providerSpecificEntityManagerInterface); - Mockito.when(em.getDelegate()).thenReturn(em); // delegate is used to determine the classloader of the provider - // specific interface, therefore we return the proxied - // EntityManager. + + // delegate is used to determine the classloader of the provider + // specific interface, therefore we return the proxied EntityManager + when(em.getDelegate()).thenReturn(em); return em; } + private EntityManagerFactory mockProviderSpecificEntityManagerFactoryInterface(String interfaceName) + throws ClassNotFoundException { + + Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, + EntityManager.class); + + return (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerInterface); + } + static class InterfaceGenerator implements Opcodes { static Class generate(final String interfaceName, ClassLoader parentClassLoader, final Class... interfaces) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java index 436e99fb31..d3b87cb004 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2025 the original author or authors. + * Copyright 2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,21 +18,21 @@ import java.util.Optional; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; -import org.jspecify.annotations.Nullable; /** - * {@code CurrentTenantIdentifierResolver} instance for testing + * {@code CurrentTenantIdentifierResolver} instance for testing. * - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { - private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>(); - public static void setTenantIdentifier(String tenantIdentifier) { + private static final ThreadLocal CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>(); + + static void setTenantIdentifier(String tenantIdentifier) { CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier); } - public static void removeTenantIdentifier() { + static void removeTenantIdentifier() { CURRENT_TENANT_IDENTIFIER.remove(); } @@ -46,4 +46,5 @@ public String resolveCurrentTenantIdentifier() { public boolean validateExistingCurrentSessions() { return true; } + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java index 3de19e90d8..28ebcd1765 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateMultitenancyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2025 the original author or authors. + * Copyright 2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,14 @@ import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assumptions.*; +import jakarta.persistence.EntityManager; + import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -36,16 +39,14 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; -import jakarta.persistence.EntityManager; - /** * Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected - * despite not having a tenant available at creation time + * despite not having a tenant available at creation time. * - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ @ExtendWith(SpringExtension.class) -@ContextConfiguration() +@ContextConfiguration class HibernateMultitenancyTests { @Autowired RoleRepository roleRepository; @@ -56,19 +57,23 @@ void tearDown() { HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier(); } - @Test + @Test // GH-3425 void testPersistenceProviderFromFactoryWithoutTenant() { - PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory()); + + PersistenceProvider provider = PersistenceProvider.fromEntityManager(em); + assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE); } - @Test + @Test // GH-3425 void testRepositoryWithTenant() { + HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id"); + assertThatNoException().isThrownBy(() -> roleRepository.findAll()); } - @Test + @Test // GH-3425 void testRepositoryWithoutTenantFails() { assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class); } @@ -80,9 +85,10 @@ List insertAndQuery() { return roleRepository.findAll(); } - @ImportResource({ "classpath:multitenancy-test.xml" }) + @ImportResource("classpath:multitenancy-test.xml") @Configuration @EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE)) static class TestConfig {} + } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index eac8e8e824..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 @@ -44,6 +44,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; @@ -59,7 +60,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yanming Zhou - * @author Ariel Morelli Andres (Atlassian US, Inc.) + * @author Ariel Morelli Andres */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -189,7 +190,6 @@ void doNothingWhenNewInstanceGetsDeleted() { newUser.setId(null); when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory); - when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil); repo.delete(newUser); @@ -206,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); From c260c2130100146d1475636e4ea5444d6c481c6c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 10 Jun 2025 10:27:13 +0200 Subject: [PATCH 15/93] Fix `QueryUtils` regex parsing field and function aliases. Remove leading space requirement, simplify group nesting and replace character class with non-capturing group to avoid a, s and | (pipe) matching. Closes #3911 --- .../data/jpa/repository/query/QueryUtils.java | 8 +-- .../repository/query/QueryUtilsUnitTests.java | 61 +++++++++++++------ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 371dc0b6cc..5de51a2f97 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 @@ -193,17 +193,15 @@ public abstract class QueryUtils { // any function call including parameters within the brackets builder.append("\\w+\\s*\\([\\w\\.,\\s'=:;\\\\?]+\\)"); // the potential alias - builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); + builder.append("\\s+(?:as|AS)+\\s+([\\w\\.]+)"); FUNCTION_PATTERN = compile(builder.toString()); builder = new StringBuilder(); - builder.append("\\s+"); // at least one space builder.append("[^\\s\\(\\)]+"); // No white char no bracket - builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); // the potential alias + builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); // the potential alias FIELD_ALIAS_PATTERN = compile(builder.toString()); - } /** @@ -389,7 +387,7 @@ static Set getOuterJoinAliases(String query) { * @param query a {@literal String} containing a query. Must not be {@literal null}. * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}. */ - private static Set getFieldAliases(String query) { + static Set getFieldAliases(String query) { Set result = new HashSet<>(); Matcher matcher = FIELD_ALIAS_PATTERN.matcher(query); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java index 647d4dfa2b..717791afec 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsUnitTests.java @@ -51,6 +51,7 @@ * @author Erik Pellizzon * @author Pranav HS * @author Eduard Dudar + * @author Mark Paluch */ class QueryUtilsUnitTests { @@ -130,13 +131,13 @@ void detectsAliasCorrectly() { .isEqualTo("u"); assertThat(detectAlias( "from Foo f left join f.bar b with type(b) = BarChild where (f.id = (select max(f.id) from Foo f2 where type(f2) = FooChild) or 1 <> 1) and 1=1")) - .isEqualTo("f"); + .isEqualTo("f"); assertThat(detectAlias( "(from Foo f max(f) ((((select * from Foo f2 (from Foo f3) max(*)) (from Foo f4)) max(f5)) (f6)) (from Foo f7))")) - .isEqualTo("f"); + .isEqualTo("f"); assertThat(detectAlias( "SELECT e FROM DbEvent e WHERE (CAST(:modifiedFrom AS date) IS NULL OR e.modificationDate >= :modifiedFrom)")) - .isEqualTo("e"); + .isEqualTo("e"); assertThat(detectAlias("from User u where (cast(:effective as date) is null) OR :effective >= u.createdAt")) .isEqualTo("u"); assertThat(detectAlias("from User u where (cast(:effectiveDate as date) is null) OR :effectiveDate >= u.createdAt")) @@ -145,7 +146,7 @@ void detectsAliasCorrectly() { .isEqualTo("u"); assertThat( detectAlias("from User u where (cast(:e1f2f3ectiveFrom as date) is null) OR :effectiveFrom >= u.createdAt")) - .isEqualTo("u"); + .isEqualTo("u"); } @Test // GH-2260 @@ -175,13 +176,13 @@ void testRemoveSubqueries() throws Exception { .isEqualTo("(select u from User u where not exists )"); assertThat(normalizeWhitespace( removeSubqueries("select u from User u where not exists (from User u2 where not exists (from User u3))"))) - .isEqualTo("select u from User u where not exists"); + .isEqualTo("select u from User u where not exists"); assertThat(normalizeWhitespace( removeSubqueries("select u from User u where not exists ((from User u2 where not exists (from User u3)))"))) - .isEqualTo("select u from User u where not exists ( )"); + .isEqualTo("select u from User u where not exists ( )"); assertThat(normalizeWhitespace( removeSubqueries("(select u from User u where not exists ((from User u2 where not exists (from User u3))))"))) - .isEqualTo("(select u from User u where not exists ( ))"); + .isEqualTo("(select u from User u where not exists ( ))"); } @Test // GH-2581 @@ -543,6 +544,32 @@ void doesNotPrefixAliasedFunctionCallNameWhenQueryStringContainsMultipleWhiteSpa assertThat(applySorting(query, sort, "m")).endsWith("order by avgPrice asc"); } + @Test // GH-3911 + void discoversFunctionAliasesCorrectly() { + + assertThat(getFunctionAliases("SELECT COUNT(1) a alias1,2 s alias2")).isEmpty(); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1,2 as alias2")).containsExactly("alias1"); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1,COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1, 2 as alias2")).containsExactly("alias1"); + assertThat(getFunctionAliases("SELECT COUNT(1) as alias1, COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("COUNT(1) as alias1,COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("COUNT(1) as alias1,COUNT(2) as alias2")).contains("alias1", "alias2"); + assertThat(getFunctionAliases("1 as alias1, COUNT(2) as alias2")).containsExactly("alias2"); + assertThat(getFunctionAliases("1 as alias1, COUNT(2) as alias2")).containsExactly("alias2"); + assertThat(getFunctionAliases("COUNT(1) as alias1,2 as alias2")).containsExactly("alias1"); + assertThat(getFunctionAliases("COUNT(1) as alias1, 2 as alias2")).containsExactly("alias1"); + } + + @Test // GH-3911 + void discoversFieldAliasesCorrectly() { + + assertThat(getFieldAliases("SELECT 1 a alias1,2 s alias2")).isEmpty(); + assertThat(getFieldAliases("SELECT 1 as alias1,2 as alias2")).contains("alias1", "alias2"); + assertThat(getFieldAliases("SELECT 1 as alias1,2 as alias2")).contains("alias1", "alias2"); + assertThat(getFieldAliases("1 as alias1,2 as alias2")).contains("alias1", "alias2"); + assertThat(getFieldAliases("1 as alias1, 2 as alias2")).contains("alias1", "alias2"); + } + @Test // DATAJPA-1000 void discoversCorrectAliasForJoinFetch() { @@ -564,7 +591,7 @@ void discoversAliasWithComplexFunction() { assertThat( QueryUtils.getFunctionAliases("select new MyDto(sum(case when myEntity.prop3=0 then 1 else 0 end) as myAlias")) // - .contains("myAlias"); + .contains("myAlias"); } @Test // DATAJPA-1506 @@ -784,18 +811,19 @@ void applySortingAccountsForNativeWindowFunction() { // order by in over clause + at the end assertThat( QueryUtils.applySorting("select dense_rank() over (order by lastname) from user u order by u.lastname", sort)) - .isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc"); + .isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc"); // partition by + order by in over clause - assertThat(QueryUtils.applySorting( - "select dense_rank() over (partition by active, age order by lastname) from user u", sort)).isEqualTo( + assertThat(QueryUtils + .applySorting("select dense_rank() over (partition by active, age order by lastname) from user u", sort)) + .isEqualTo( "select dense_rank() over (partition by active, age order by lastname) from user u order by u.age desc"); // partition by + order by in over clause + order by at the end assertThat(QueryUtils.applySorting( "select dense_rank() over (partition by active, age order by lastname) from user u order by active", sort)) - .isEqualTo( - "select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc"); + .isEqualTo( + "select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc"); // partition by + order by in over clause + frame clause assertThat(QueryUtils.applySorting( @@ -812,8 +840,7 @@ void applySortingAccountsForNativeWindowFunction() { // order by in subselect (select expression) assertThat( QueryUtils.applySorting("select lastname, (select i.id from item i order by i.id limit 1) from user u", sort)) - .isEqualTo( - "select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc"); + .isEqualTo("select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc"); // order by in subselect (select expression) + at the end assertThat(QueryUtils.applySorting( @@ -949,7 +976,7 @@ select q.specialist_id, listagg(q.points, '%s') as points @Test // GH-3324 void createCountQueryForSimpleQuery() { - assertCountQuery("select * from User","select count(*) from User"); - assertCountQuery("select * from User u","select count(u) from User u"); + assertCountQuery("select * from User", "select count(*) from User"); + assertCountQuery("select * from User u", "select count(u) from User u"); } } From 72f662793ee6495c56e46a5f9e47e5c7baacb772 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 10 Jun 2025 12:27:16 +0200 Subject: [PATCH 16/93] Polishing. Remove type parameter to enable usage with Hibernate 6.2. See: #3425 Original pull request: #3885 --- .../repository/HibernateCurrentTenantIdentifierResolver.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java index d3b87cb004..54f062d5ef 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java @@ -23,8 +23,10 @@ * {@code CurrentTenantIdentifierResolver} instance for testing. * * @author Ariel Morelli Andres + * @author Mark Paluch */ -public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver { +@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<>(); From a54ad17fec721316ddec6902412db786341c6ead Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 11 Jun 2025 14:17:43 +0200 Subject: [PATCH 17/93] Upgrade to PGJDBC Driver 42.7.7. Closes #3914 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f5845e7679..22a4bd2eef 100755 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ 3.1.0 5.2 9.2.0 - 42.7.5 + 42.7.7 3.5.1-SNAPSHOT 0.10.3 From b7b9db5a74b59579fac7a524cd1125dc59d84cf0 Mon Sep 17 00:00:00 2001 From: hoyeon Jang Date: Sat, 7 Jun 2025 10:08:35 +0900 Subject: [PATCH 18/93] Fix typos in query-methods.adoc. Signed-off-by: hoyeon Jang Closes #3912 --- src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 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. From 261d693d8b0d6691c01ca485181ee6a5f37afa9b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 12 Jun 2025 08:31:35 +0200 Subject: [PATCH 19/93] Polishing. Simplify regex. See #3911 --- .../data/jpa/repository/query/QueryUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 5de51a2f97..f693637747 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 @@ -193,15 +193,15 @@ public abstract class QueryUtils { // any function call including parameters within the brackets builder.append("\\w+\\s*\\([\\w\\.,\\s'=:;\\\\?]+\\)"); // the potential alias - builder.append("\\s+(?:as|AS)+\\s+([\\w\\.]+)"); + builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); - FUNCTION_PATTERN = compile(builder.toString()); + FUNCTION_PATTERN = compile(builder.toString(), CASE_INSENSITIVE); builder = new StringBuilder(); builder.append("[^\\s\\(\\)]+"); // No white char no bracket builder.append("\\s+(?:as)+\\s+([\\w\\.]+)"); // the potential alias - FIELD_ALIAS_PATTERN = compile(builder.toString()); + FIELD_ALIAS_PATTERN = compile(builder.toString(), CASE_INSENSITIVE); } /** From 01a2c72e2fb3b127a55b34b162811f4c6cf98631 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 13 Jun 2025 13:39:13 +0200 Subject: [PATCH 20/93] Prepare 3.5.1 (2025.0.1). See #3891 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 22a4bd2eef..ccbb4391f4 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.1-SNAPSHOT + 3.5.1 @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.1-SNAPSHOT + 3.5.1 0.10.3 org.hibernate @@ -173,20 +173,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From bb662ae6a435559bf78b135b3f77b87181b3841f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 13 Jun 2025 13:39:34 +0200 Subject: [PATCH 21/93] Release version 3.5.1 (2025.0.1). See #3891 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index ccbb4391f4..eb6a810bd7 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.1-SNAPSHOT + 3.5.1 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index aabe6d3a97..33a659724c 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.1-SNAPSHOT + 3.5.1 org.springframework.data spring-data-jpa-parent - 3.5.1-SNAPSHOT + 3.5.1 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 1ea8eaa775..776c95b38f 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.1-SNAPSHOT + 3.5.1 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 4f08ee7886..e564b5368b 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.1-SNAPSHOT + 3.5.1 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.1-SNAPSHOT + 3.5.1 ../pom.xml From f3c93ef65a5ccade2a1bfc2a8be572e2b80e776d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 13 Jun 2025 13:42:18 +0200 Subject: [PATCH 22/93] Prepare next development iteration. See #3891 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index eb6a810bd7..270acce4ff 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.1 + 3.5.2-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 33a659724c..cac0a23a90 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.1 + 3.5.2-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.1 + 3.5.2-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 776c95b38f..9e6206afa1 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.1 + 3.5.2-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index e564b5368b..91b5eb6258 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.1 + 3.5.2-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.1 + 3.5.2-SNAPSHOT ../pom.xml From 961d825fd85c935238efff0eaf7d5f81adb210bd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 13 Jun 2025 13:42:19 +0200 Subject: [PATCH 23/93] After release cleanups. See #3891 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 270acce4ff..0d91c947e1 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.1 + 3.5.2-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.1 + 3.5.2-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 + From faa526f82318680c4519c3767d215168e70585e8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 17 Jun 2025 09:37:50 +0200 Subject: [PATCH 24/93] Polishing. Refine readme. See #3892 --- README.adoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index 3c5597973a..3bd2b4da00 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Data JPA image:https://jenkins.spring.io/buildStatus/icon?job=spring-data-jpa%2Fmain&subject=Build[link=https://jenkins.spring.io/view/SpringData/job/spring-data-jpa/] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data JPA Parent"] += Spring Data JPA image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=Spring Data JPA Parent"] Spring Data JPA, part of the larger https://projects.spring.io/spring-data[Spring Data] family, makes it easy to implement JPA-based repositories. This module deals with enhanced support for JPA-based data access layers. @@ -157,7 +157,9 @@ You also need JDK 17 or above. If you want to build with the regular `mvn` command, you will need https://maven.apache.org/run-maven/index.html[Maven v3.8.0 or above]. -_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular please sign the https://cla.pivotal.io/sign/spring[Contributor’s Agreement] before your first non-trivial change._ +_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests._ +All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. === Building reference documentation @@ -168,7 +170,7 @@ Building the documentation builds also the project without running tests. $ ./mvnw clean install -Pantora ---- -The generated documentation is available from `target/antora/site/index.html`. +The generated documentation is available from `target/antora/index.html`. == Guides From 2489e0118ffe658a05b441eeabdd5d1a812c4925 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 25 Jun 2025 14:29:06 +0200 Subject: [PATCH 25/93] Fix `PersistenceProvider` lookup using proxied `EntityManagerFactory`. We now distinguish between Spring-proxied and other JDK proxied EntityManagerFactory objects for proper unwrapping. Spring consistently uses a null value as class to get hold of the target object. Both, Hibernate and EclipseLink fail with a NullPointerException when calling unwrap(null) and therefore, we call all other JDK proxies with unwrap(EntityManagerFactory.class) to adhere to the JPA specification and avoid failures according to the implementations. Any other proxying mechanism that behaves differently will require additional refinement once such a case comes up. Closes #3923 --- .../jpa/provider/PersistenceProvider.java | 6 ++- .../PersistenceProviderUnitTests.java | 44 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 62f0c45f94..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 @@ -31,7 +31,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Set; -import java.util.function.LongSupplier; import java.util.stream.Stream; import org.eclipse.persistence.config.QueryHints; @@ -270,7 +269,10 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) { if (Proxy.isProxyClass(unwrapped.getClass())) { - unwrapped = unwrapped.unwrap(null); + + Class unwrapTo = Proxy.getInvocationHandler(unwrapped).getClass().getName() + .contains("org.springframework.orm.jpa.") ? null : EntityManagerFactory.class; + unwrapped = unwrapped.unwrap(unwrapTo); } else if (AopUtils.isAopProxy(unwrapped)) { unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java index 66d55e2397..bb3c0aa161 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java @@ -22,7 +22,9 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceException; +import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Map; @@ -35,6 +37,7 @@ import org.springframework.asm.ClassWriter; import org.springframework.asm.Opcodes; import org.springframework.instrument.classloading.ShadowingClassLoader; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; @@ -110,6 +113,25 @@ void detectsProviderFromProxiedEntityManager() throws Exception { assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK); } + @Test // GH-3923 + void detectsEntityManagerFromProxiedEntityManagerFactory() throws Exception { + + EntityManagerFactory emf = mockProviderSpecificEntityManagerFactoryInterface( + "foo.bar.unknown.jpa.JpaEntityManager"); + when(emf.unwrap(null)).thenThrow(new NullPointerException()); + when(emf.unwrap(EntityManagerFactory.class)).thenReturn(emf); + + MyEntityManagerFactoryBean factoryBean = new MyEntityManagerFactoryBean(EntityManagerFactory.class, emf); + EntityManagerFactory springProxy = factoryBean.createEntityManagerFactoryProxy(emf); + + Object externalProxy = Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { EntityManagerFactory.class }, (proxy, method, args) -> method.invoke(emf, args)); + + assertThat(PersistenceProvider.fromEntityManagerFactory(springProxy)).isEqualTo(GENERIC_JPA); + assertThat(PersistenceProvider.fromEntityManagerFactory((EntityManagerFactory) externalProxy)) + .isEqualTo(GENERIC_JPA); + } + private EntityManager mockProviderSpecificEntityManagerInterface(String interfaceName) throws ClassNotFoundException { Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, @@ -128,7 +150,7 @@ private EntityManagerFactory mockProviderSpecificEntityManagerFactoryInterface(S throws ClassNotFoundException { Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, - EntityManager.class); + EntityManagerFactory.class); return (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerInterface); } @@ -181,4 +203,24 @@ private static String[] toResourcePaths(Class... interfacesToImplement) { .toArray(String[]::new); } } + + static class MyEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean { + + public MyEntityManagerFactoryBean(Class entityManagerFactoryInterface, + EntityManagerFactory entityManagerFactory) { + setEntityManagerFactoryInterface(entityManagerFactoryInterface); + ReflectionTestUtils.setField(this, "nativeEntityManagerFactory", entityManagerFactory); + + } + + @Override + protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException { + return null; + } + + @Override + protected EntityManagerFactory createEntityManagerFactoryProxy(EntityManagerFactory emf) { + return super.createEntityManagerFactoryProxy(emf); + } + } } From ab96acca26a4ecb5fabde10ada7387b6e0b2a2d5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 26 Jun 2025 08:49:39 +0200 Subject: [PATCH 26/93] Simplify build. Remove additional Eclipselink build steps as we don't use these. See #3892 --- Jenkinsfile | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 673b2370bf..c9dadd6a44 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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" - } - } - } - } - } } } From caa4704cd0547f7a1fb39ba34fbacc562df337da Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 7 Jul 2025 11:08:29 +0200 Subject: [PATCH 27/93] Exclude DTO types without custom construction from DTO constructor rewriting. We now verify that we can actually express a valid constructor expression before rewriting queries to use constructor expressions. See #3929 --- .../query/AbstractStringBasedJpaQuery.java | 5 ++- .../data/jpa/domain/sample/Country.java | 41 +++++++++++++++++++ .../data/jpa/domain/sample/Customer.java | 4 +- .../RepositoryWithCompositeKeyTests.java | 21 +++++++++- .../query/SimpleJpaQueryUnitTests.java | 15 +++++++ .../EmployeeRepositoryWithEmbeddedId.java | 4 ++ 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index f447716c10..f71e72d1e2 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 @@ -145,7 +145,7 @@ public Query doCreateQuery(JpaParametersParameterAccessor accessor) { ReturnedType getReturnedType(ResultProcessor processor) { ReturnedType returnedType = processor.getReturnedType(); - Class returnedJavaType = processor.getReturnedType().getReturnedType(); + Class returnedJavaType = returnedType.getReturnedType(); if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNativeQuery()) { return returnedType; @@ -157,7 +157,8 @@ ReturnedType getReturnedType(ResultProcessor processor) { return returnedType; } - if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)) { + if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType) + || !returnedType.needsCustomConstruction()) { if (known == null) { knownProjections.put(returnedJavaType, false); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java new file mode 100644 index 0000000000..e02b800bd3 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/Country.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.domain.sample; + +/** + * @author Mark Paluch + */ +public class Country { + + private final String code; + + // workaround to avoid DTO projections as needsCustomConstruction is false. + private Country(Country other) { + this.code = other.code; + } + + private Country(String code) { + this.code = code; + } + + public static Country of(String code) { + return new Country(code); + } + + public String getCode() { + return code; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/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/repository/RepositoryWithCompositeKeyTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java index 20613cc1d6..99665ecbfd 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/RepositoryWithCompositeKeyTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import jakarta.persistence.EntityManager; @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -115,6 +116,24 @@ void shouldSupportSavingEntitiesWithCompositeKeyClassesWithEmbeddedIdsAndDerived assertThat(persistedEmp.getDepartment().getName()).isEqualTo(dep.getName()); } + @Test // GH-3929 + void shouldReturnIdentifiers() { + + EmbeddedIdExampleDepartment dep = new EmbeddedIdExampleDepartment(); + dep.setName("TestDepartment"); + dep.setDepartmentId(-1L); + + EmbeddedIdExampleEmployee emp = new EmbeddedIdExampleEmployee(); + emp.setDepartment(dep); + emp.setEmployeePk(new EmbeddedIdExampleEmployeePK(1L, 2L)); + + emp = employeeRepositoryWithEmbeddedId.save(emp); + + List identifiers = employeeRepositoryWithEmbeddedId.findIdentifiers(); + + assertThat(identifiers).hasSize(1).contains(emp.getEmployeePk()); + } + @Test // DATAJPA-472, DATAJPA-912 void shouldSupportFindAllWithPageableAndEntityWithIdClass() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index e2934c84df..45c2470407 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 @@ -45,6 +45,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.Country; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.NativeQuery; @@ -325,6 +326,17 @@ void jdbcStyleParametersOnlyAllowedInNativeQueries() throws Exception { assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(illegalMethod)); } + @Test // GH-3929 + void doesNotRewriteQueryForDtoWithMultipleConstructors() throws Exception { + + AbstractStringBasedJpaQuery jpaQuery = (AbstractStringBasedJpaQuery) createJpaQuery( + SampleRepository.class.getMethod("justCountries")); + + String queryString = createQuery(jpaQuery); + + assertThat(queryString).startsWith("select u.country from User u"); + } + @Test // DATAJPA-1163 void resolvesExpressionInCountQuery() throws Exception { @@ -408,6 +420,9 @@ interface SampleRepository extends Repository { @Query("select r.name from User u LEFT JOIN FETCH u.roles r") Collection projectWithJoinPaths(); + @Query("select u.country from User u") + Collection justCountries(); + @Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u") List findAllWithExpressionInCountQuery(Pageable pageable); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/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); } From f92b40cbc72297c208109250f1daefe47f7c1d60 Mon Sep 17 00:00:00 2001 From: Giheon Do Date: Wed, 9 Jul 2025 20:38:16 +0900 Subject: [PATCH 28/93] Replace regex with startsWith / endsWith check for LIKE pattern detection. Signed-off-by: Giheon Do Closes #3932 --- .../data/jpa/repository/query/ParameterBinding.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index e5cffccaf6..e841994c5e 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 @@ -231,6 +231,8 @@ static class LikeParameterBinding extends ParameterBinding { private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, Type.ENDING_WITH, Type.LIKE); + private static final String PERCENT = "%"; + private final Type type; /** @@ -326,15 +328,15 @@ static Type getLikeTypeFrom(String expression) { Assert.hasText(expression, "Expression must not be null or empty"); - if (expression.matches("%.*%")) { + if (expression.startsWith(PERCENT) && expression.endsWith(PERCENT)) { return Type.CONTAINING; } - if (expression.startsWith("%")) { + if (expression.startsWith(PERCENT)) { return Type.ENDING_WITH; } - if (expression.endsWith("%")) { + if (expression.endsWith(PERCENT)) { return Type.STARTING_WITH; } From f635de742c70a44045dae21072eb180f26fae674 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Jul 2025 12:12:12 +0200 Subject: [PATCH 29/93] Polishing. Refine conditional flow. See #3932 --- .../data/jpa/repository/query/ParameterBinding.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index e841994c5e..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 @@ -231,8 +231,6 @@ static class LikeParameterBinding extends ParameterBinding { private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, Type.ENDING_WITH, Type.LIKE); - private static final String PERCENT = "%"; - private final Type type; /** @@ -328,15 +326,11 @@ static Type getLikeTypeFrom(String expression) { Assert.hasText(expression, "Expression must not be null or empty"); - if (expression.startsWith(PERCENT) && expression.endsWith(PERCENT)) { - return Type.CONTAINING; - } - - if (expression.startsWith(PERCENT)) { - return Type.ENDING_WITH; + if (expression.startsWith("%")) { + return expression.endsWith("%") ? Type.CONTAINING : Type.ENDING_WITH; } - if (expression.endsWith(PERCENT)) { + if (expression.endsWith("%")) { return Type.STARTING_WITH; } From a4fe9c38815c3586e4384e69de18a09f0dfa2036 Mon Sep 17 00:00:00 2001 From: Giheon Do Date: Sun, 15 Jun 2025 16:41:19 +0900 Subject: [PATCH 30/93] Cache query strings in `SimpleJpaRepository`. Cache the deleteAll and count query strings as final fields in SimpleJpaRepository. This avoids repeated String.format operations and reduces unnecessary object creation on every invocation of deleteAllInBatch() and count(). No functional changes. Signed-off-by: Giheon Do Closes #3920 --- .../jpa/repository/support/SimpleJpaRepository.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 12d92cef7d..833ba27f0b 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 @@ -103,6 +103,7 @@ * @author Diego Krupitza * @author Seol-JY * @author Joshua Chen + * @author Dockerel */ @Repository @Transactional(readOnly = true) @@ -120,6 +121,9 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation entityInformation, EntityM this.entityManager = entityManager; this.provider = PersistenceProvider.fromEntityManager(entityManager); this.projectionFactory = new SpelAwareProxyProjectionFactory(); + + this.deleteAllQueryString = getDeleteAllQueryString(); + this.countQueryString = getCountQueryString(); } /** @@ -309,7 +316,7 @@ public void deleteAll() { @Transactional public void deleteAllInBatch() { - Query query = entityManager.createQuery(getDeleteAllQueryString()); + Query query = entityManager.createQuery(deleteAllQueryString); applyQueryHints(query); @@ -631,7 +638,7 @@ public R findBy(Example example, Function query = entityManager.createQuery(getCountQueryString(), Long.class); + TypedQuery query = entityManager.createQuery(countQueryString, Long.class); applyQueryHintsForCount(query); From 3784582f3271fa4e702a15120254a9d8fded00c6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Jul 2025 12:17:00 +0200 Subject: [PATCH 31/93] Polishing. Inline methods, lazify query creation. See #3920 --- .../support/SimpleJpaRepository.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 833ba27f0b..73a6e52139 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 @@ -73,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; @@ -103,7 +104,7 @@ * @author Diego Krupitza * @author Seol-JY * @author Joshua Chen - * @author Dockerel + * @author Giheon Do */ @Repository @Transactional(readOnly = true) @@ -121,8 +122,8 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation deleteAllQueryString; + private final Lazy countQueryString; private @Nullable CrudMethodMetadata metadata; private ProjectionFactory projectionFactory; @@ -144,8 +145,11 @@ public SimpleJpaRepository(JpaEntityInformation entityInformation, EntityM this.provider = PersistenceProvider.fromEntityManager(entityManager); this.projectionFactory = new SpelAwareProxyProjectionFactory(); - this.deleteAllQueryString = getDeleteAllQueryString(); - this.countQueryString = getCountQueryString(); + this.deleteAllQueryString = Lazy + .of(() -> getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName())); + this.countQueryString = Lazy + .of(() -> getQueryString(String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s"), + entityInformation.getEntityName())); } /** @@ -188,16 +192,6 @@ protected Class getDomainClass() { return entityInformation.getJavaType(); } - private String getDeleteAllQueryString() { - return getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()); - } - - private String getCountQueryString() { - - String countQuery = String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s"); - return getQueryString(countQuery, entityInformation.getEntityName()); - } - @Override @Transactional public void deleteById(ID id) { @@ -316,7 +310,7 @@ public void deleteAll() { @Transactional public void deleteAllInBatch() { - Query query = entityManager.createQuery(deleteAllQueryString); + Query query = entityManager.createQuery(deleteAllQueryString.get()); applyQueryHints(query); @@ -638,7 +632,7 @@ public R findBy(Example example, Function query = entityManager.createQuery(countQueryString, Long.class); + TypedQuery query = entityManager.createQuery(countQueryString.get(), Long.class); applyQueryHintsForCount(query); From 672ab08d9b0486165af2626c1371b4ae6b382258 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 08:36:33 +0200 Subject: [PATCH 32/93] Upgrade to Eclipselink 4.0.7. Closes #3936 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d91c947e1..22198abde0 100755 --- a/pom.xml +++ b/pom.xml @@ -28,8 +28,8 @@ 4.13.0 - 4.0.6 - 4.0.7-SNAPSHOT + 4.0.7 + 4.0.8-SNAPSHOT 6.6.17.Final 6.2.38.Final 6.6.18-SNAPSHOT From 0e39197e57c606c6a4bdf8eddcf5f163541aa1fc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 08:37:48 +0200 Subject: [PATCH 33/93] Upgrade to Hibernate 6.6.21.Final. Closes #3937 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 22198abde0..bbe3ab3c34 100755 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,9 @@ 4.13.0 4.0.7 4.0.8-SNAPSHOT - 6.6.17.Final + 6.6.21.Final 6.2.38.Final - 6.6.18-SNAPSHOT + 6.6.22-SNAPSHOT 7.0.0.Beta5 7.0.0-SNAPSHOT 2.7.4 From 5ad33927cd3791c76c08f81668817055606487d0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 11:16:08 +0200 Subject: [PATCH 34/93] Backport `Specification.unrestricted()` to `3.5.x`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introducing a replacement method for the deprecated `where(…)` method. Closes #3942 --- .../data/jpa/domain/Specification.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index c8f67bc0bf..9ef08b9869 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 @@ -64,6 +64,17 @@ static Specification not(@Nullable Specification spec) { }; } + /** + * Simple static factory method to create a specification matching all objects. + * + * @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}. * @@ -72,11 +83,12 @@ static Specification not(@Nullable Specification spec) { * @param spec can be {@literal null}. * @return guaranteed to be not {@literal null}. * @since 2.0 - * @deprecated since 3.5, to be removed with 4.0 as we no longer want to support {@literal null} specifications. + * @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; } /** From d0fe59056793db7e42826e1aafb0ad4ed40aa88b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Jul 2025 12:06:22 +0200 Subject: [PATCH 35/93] Polishing. Hibernate 6.6.22-SNAPSHOT not available yet. See #3937 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbe3ab3c34..eac7e43b03 100755 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 4.0.8-SNAPSHOT 6.6.21.Final 6.2.38.Final - 6.6.22-SNAPSHOT + 6.6.21.Final 7.0.0.Beta5 7.0.0-SNAPSHOT 2.7.4 From 5c01c8fe9e20cce652b7c399fa4d8fecf7682b6b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 17 Jul 2025 14:00:49 +0200 Subject: [PATCH 36/93] Upgrade to Maven Wrapper 3.9.11. See #3946 --- .mvn/wrapper/maven-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8eb4cb3b3d..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 From 5753877048637be1b93123ec489584e910352c85 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 10:27:37 +0200 Subject: [PATCH 37/93] Prepare 3.5.2 (2025.0.2). See #3918 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index eac7e43b03..8e9634e020 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.2-SNAPSHOT + 3.5.2 @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.2-SNAPSHOT + 3.5.2 0.10.3 org.hibernate @@ -173,20 +173,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 41b58292819dfbafd9f76dd8bd186d049ff9dddc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 10:27:58 +0200 Subject: [PATCH 38/93] Release version 3.5.2 (2025.0.2). See #3918 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 8e9634e020..5085982c37 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.2-SNAPSHOT + 3.5.2 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index cac0a23a90..8f996a2340 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.2-SNAPSHOT + 3.5.2 org.springframework.data spring-data-jpa-parent - 3.5.2-SNAPSHOT + 3.5.2 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 9e6206afa1..28b531ac12 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.2-SNAPSHOT + 3.5.2 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 91b5eb6258..9436963127 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.2-SNAPSHOT + 3.5.2 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.2-SNAPSHOT + 3.5.2 ../pom.xml From 4acf655984cfe32cfcb6254dad3356301e39b8ae Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 10:30:32 +0200 Subject: [PATCH 39/93] Prepare next development iteration. See #3918 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 5085982c37..63b84744ff 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.2 + 3.5.3-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 8f996a2340..3f51a41991 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.2 + 3.5.3-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.2 + 3.5.3-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 28b531ac12..7df02b594d 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.2 + 3.5.3-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 9436963127..f198481967 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.2 + 3.5.3-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.2 + 3.5.3-SNAPSHOT ../pom.xml From 8bf33bae5ad89adcc370ae92656e7df734f12c00 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jul 2025 10:30:33 +0200 Subject: [PATCH 40/93] After release cleanups. See #3918 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 63b84744ff..cc328c62ac 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.2 + 3.5.3-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.2 + 3.5.3-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 + From a5adaf3e6bf83dbb0997137ea857fdbdd85e152b Mon Sep 17 00:00:00 2001 From: shchae04 <94516539+shchae04@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:38:09 +0900 Subject: [PATCH 41/93] Fix typo in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request fixes a small typo in the README file: - `datatabase` → `database` It's a minor change, but helps improve the clarity and quality of the documentation. Signed-off-by: shchae04 <94516539+shchae04@users.noreply.github.com> Original pull request #3953 --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 3bd2b4da00..82e05e71d2 100644 --- a/README.adoc +++ b/README.adoc @@ -143,7 +143,7 @@ https://github.com/spring-projects/spring-data-jpa/issues[issue tracker] to see * If the issue doesn’t exist already, https://github.com/spring-projects/spring-data-jpa/issues[create a new issue]. * Please provide as much information as possible with the issue report, we like to know the version of Spring Data that you are using and JVM version, complete stack traces and any relevant configuration information. * If you need to paste code, or include a stack trace format it as code using triple backtick. -* If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code. Use an in-memory datatabase if possible or set the database up using https://github.com/testcontainers[Testcontainers]. +* If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code. Use an in-memory database if possible or set the database up using https://github.com/testcontainers[Testcontainers]. == Building from Source From b04e8f86dd275f606c449d89f71a64737ef0fd92 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 24 Jul 2025 07:56:45 +0200 Subject: [PATCH 42/93] Polishing. Formatting. Reduced scope of field to variable. Original pull request #3954 --- .../query/JSqlParserQueryEnhancer.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java index 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) { From 593ea71a645238a3d7e281bd423207a639bd8272 Mon Sep 17 00:00:00 2001 From: Now Date: Thu, 24 Jul 2025 03:26:06 +0900 Subject: [PATCH 43/93] Fix typo in Jpa21Utils javadoc. Signed-off-by: Now Original pull request #3955 --- .../springframework/data/jpa/repository/query/Jpa21Utils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java index 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}. From c761f08959d1a6a20bbd371cf26abb076230211f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 6 Aug 2025 13:53:16 +0200 Subject: [PATCH 44/93] Upgrade to Hibernate 6.6.24.Final. Closes #3963 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cc328c62ac..b79e59ffb0 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.0 4.0.7 4.0.8-SNAPSHOT - 6.6.21.Final + 6.6.24.Final 6.2.38.Final 6.6.21.Final 7.0.0.Beta5 From 77ceaf3efff7afeeaa0f276bc774ba3a8e7358f0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Aug 2025 11:59:58 +0200 Subject: [PATCH 45/93] Upgrade to Hibernate 6.6.25.Final. Closes #3973 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b79e59ffb0..d23ec4033b 100755 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,9 @@ 4.13.0 4.0.7 4.0.8-SNAPSHOT - 6.6.24.Final + 6.6.25.Final 6.2.38.Final - 6.6.21.Final + 6.6.26.Final 7.0.0.Beta5 7.0.0-SNAPSHOT 2.7.4 From 63b8cd53293868bbe45fedd1c2bc484dee0081d0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 14 Aug 2025 16:40:47 +0200 Subject: [PATCH 46/93] Polishing. Refine antora documentation keys. See #3952 --- .../modules/ROOT/pages/jpa/entity-persistence.adoc | 3 ++- src/main/antora/resources/antora-resources/antora.yml | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc b/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc index 990c959441..ac8fcd4a75 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/entity-persistence.adoc @@ -19,7 +19,8 @@ Spring Data JPA offers the following strategies to detect whether an entity is n If the identifier property is `null`, then the entity is assumed to be new. Otherwise, it is assumed to be not new. In contrast to other Spring Data modules, JPA considers `0` (zero) as the first inserted version of an entity and therefore, a primitive version property cannot be used to determine whether an entity is new or not. -2. Implementing `Persistable`: If an entity implements `Persistable`, Spring Data JPA delegates the new detection to the `isNew(…)` method of the entity. See the link:$$https://docs.spring.io/spring-data/data-commons/docs/current/api/index.html?org/springframework/data/domain/Persistable.html$$[JavaDoc] for details. +2. Implementing `Persistable`: If an entity implements `Persistable`, Spring Data JPA delegates the new detection to the `isNew(…)` method of the entity. +See the link:{spring-data-commons-javadoc-base}/org/springframework/data/domain/Persistable.html[JavaDoc] for details. 3. Implementing `EntityInformation`: You can customize the `EntityInformation` abstraction used in the `SimpleJpaRepository` implementation by creating a subclass of `JpaRepositoryFactory` and overriding the `getEntityInformation(…)` method accordingly. You then have to register the custom implementation of `JpaRepositoryFactory` as a Spring bean. Note that this should be rarely necessary. See the javadoc:org.springframework.data.jpa.repository.support.JpaRepositoryFactory[JavaDoc] for details. Option 1 is not an option for entities that use manually assigned identifiers and no version attribute as with those the identifier will always be non-`null`. diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index eedc4999e3..a037a43a83 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -3,18 +3,19 @@ prerelease: ${antora-component.prerelease} asciidoc: attributes: + attribute-missing: 'warn' + chomp: 'all' version: ${project.version} copyright-year: ${current.year} springversionshort: ${spring.short} springversion: ${spring} - attribute-missing: 'warn' commons: ${springdata.commons.docs} include-xml-namespaces: false - spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference - spring-data-commons-javadoc-base: https://docs.spring.io/spring-data/commons/docs/${springdata.commons}/api/ + spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference/{commons} + spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java' springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api spring-framework-docs: '{springdocsurl}' + springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api spring-framework-javadoc: '{springjavadocurl}' springhateoasversion: ${spring-hateoas} hibernatejavadocurl: https://docs.jboss.org/hibernate/orm/6.6/javadocs/ From 7e3c8dad3c6a9901c9b1db6bf61e2c484ddbbcd2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:01:13 +0200 Subject: [PATCH 47/93] Prepare 3.5.3 (2025.0.3). See #3949 --- pom.xml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index d23ec4033b..e655082d6a 100755 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,4 @@ - + 4.0.0 @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.3-SNAPSHOT + 3.5.3 @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.3-SNAPSHOT + 3.5.3 0.10.3 org.hibernate @@ -173,20 +173,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From eae94a4a331ab6ca68438246ce9ee30d026087d4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:01:35 +0200 Subject: [PATCH 48/93] Release version 3.5.3 (2025.0.3). See #3949 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index e655082d6a..a8e870dafc 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.3-SNAPSHOT + 3.5.3 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 3f51a41991..3a7552ccee 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.3-SNAPSHOT + 3.5.3 org.springframework.data spring-data-jpa-parent - 3.5.3-SNAPSHOT + 3.5.3 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 7df02b594d..80096b40f7 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.3-SNAPSHOT + 3.5.3 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index f198481967..517771c2f0 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.3-SNAPSHOT + 3.5.3 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.3-SNAPSHOT + 3.5.3 ../pom.xml From 18d0665135de3f9fb17d55bef1ae320ba70b2352 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:04:15 +0200 Subject: [PATCH 49/93] Prepare next development iteration. See #3949 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index a8e870dafc..c8f99fb338 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.3 + 3.5.4-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 3a7552ccee..e727189c3b 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.3 + 3.5.4-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.3 + 3.5.4-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 80096b40f7..f3a364ecd0 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.3 + 3.5.4-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 517771c2f0..83396331b6 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.3 + 3.5.4-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.3 + 3.5.4-SNAPSHOT ../pom.xml From 176ecff850206741c01eeca222e42e11ce65e20c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 10:04:16 +0200 Subject: [PATCH 50/93] After release cleanups. See #3949 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index c8f99fb338..82312b5d84 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.3 + 3.5.4-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.3 + 3.5.4-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 + From aad1f7bf98cf64f09995f4d8e79fa83dd5b8d106 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 15 Aug 2025 14:13:08 +0200 Subject: [PATCH 51/93] Refine version properties for documentation build. See spring-projects/spring-data-build#2638 --- .../resources/antora-resources/antora.yml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/antora/resources/antora-resources/antora.yml b/src/main/antora/resources/antora-resources/antora.yml index a037a43a83..b0a1b58fdb 100644 --- a/src/main/antora/resources/antora-resources/antora.yml +++ b/src/main/antora/resources/antora-resources/antora.yml @@ -5,19 +5,19 @@ asciidoc: attributes: attribute-missing: 'warn' chomp: 'all' - version: ${project.version} - copyright-year: ${current.year} - springversionshort: ${spring.short} - springversion: ${spring} - commons: ${springdata.commons.docs} + version: '${project.version}' + copyright-year: '${current.year}' + springversionshort: '${spring.short}' + springversion: '${spring}' + commons: '${springdata.commons.docs}' include-xml-namespaces: false - spring-data-commons-docs-url: https://docs.spring.io/spring-data/commons/reference/{commons} + spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}' spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java' - springdocsurl: https://docs.spring.io/spring-framework/reference/{springversionshort} + springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}' spring-framework-docs: '{springdocsurl}' - springjavadocurl: https://docs.spring.io/spring-framework/docs/${spring}/javadoc-api + springjavadocurl: '${documentation.spring-javadoc-url}' spring-framework-javadoc: '{springjavadocurl}' - springhateoasversion: ${spring-hateoas} + springhateoasversion: '${spring-hateoas}' hibernatejavadocurl: https://docs.jboss.org/hibernate/orm/6.6/javadocs/ - releasetrainversion: ${releasetrain} + releasetrainversion: '${releasetrain}' store: Jpa From 2bf3bb43e84784910c6cc4b532e8017b026f25d1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Aug 2025 09:35:40 +0200 Subject: [PATCH 52/93] Fix entity name expansion templated native queries. ExpressionBasedStringQuery now correctly expands the entityName placeholder again. Closes #3979 --- .../query/AbstractStringBasedJpaQuery.java | 7 ++---- .../query/ExpressionBasedStringQuery.java | 13 ++++++++++- .../ExpressionBasedStringQueryUnitTests.java | 22 +++++-------------- .../jpa/repository/sample/UserRepository.java | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index f71e72d1e2..6412d195d2 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 @@ -83,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()); 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/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/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index c7ec3362f5..85d9603d70 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 @@ -613,7 +613,7 @@ Page findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, Map findMapWithNullValues(); // DATAJPA-1307 - @Query(value = "select * from SD_User u where u.emailAddress = ?", nativeQuery = true) + @Query(value = "select * from SD_#{#entityName} u where u.emailAddress = ?", nativeQuery = true) User findByEmailNativeAddressJdbcStyleParameter(String emailAddress); // DATAJPA-1334 From 32afca168d46feed06a3fc730b5278d2daca2ee3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Aug 2025 10:21:24 +0200 Subject: [PATCH 53/93] Reinstate parameter per entity for batch deletes using EclipseLink. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EclipseLink doesn't support WHERE e IN (:entities) and requires e = ?1 OR e = ?2 OR … style. Closes #3983 --- .../data/jpa/repository/query/QueryUtils.java | 57 ++++++++++++++++++- .../jpa/repository/UserRepositoryTests.java | 2 +- ...EclipseLinkQueryUtilsIntegrationTests.java | 23 ++++++++ .../query/QueryUtilsIntegrationTests.java | 25 ++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index f693637747..05447237ab 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; @@ -528,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"); @@ -539,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/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..69433efb09 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 @@ -3583,7 +3583,7 @@ private interface UserProjectionInterfaceBased { String getLastname(); } - record UserDto(Integer id, String firstname, String lastname, String emailAddress) { + public record UserDto(Integer id, String firstname, String lastname, String emailAddress) { } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkQueryUtilsIntegrationTests.java index 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/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; } From 2105292cdc724b856f86fe1f8d48adcfb602e02b Mon Sep 17 00:00:00 2001 From: Minho Park Date: Tue, 26 Aug 2025 19:30:07 +0900 Subject: [PATCH 54/93] =?UTF-8?q?Qualify=20identifier=20used=20in=20`Simpl?= =?UTF-8?q?eJpaRepository.deleteAllByIdInBatch(=E2=80=A6)`=20JPQL.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Minho Park Closes #3990 Original pull request: #3993 --- .../springframework/data/jpa/repository/query/QueryUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 05447237ab..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 @@ -92,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 From 7af9d64e7427b73a1d6d2a915f033ba783348bb6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Aug 2025 13:48:05 +0200 Subject: [PATCH 55/93] Polishing. Add integration tests. See #3990 Original pull request: #3993 --- .../EclipseLinkJpaRepositoryTests.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java index 4892d568c1..95a9308e02 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java @@ -15,7 +15,18 @@ */ package org.springframework.data.jpa.repository.support; +import static org.assertj.core.api.Assertions.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.data.jpa.domain.sample.User; import org.springframework.test.context.ContextConfiguration; /** @@ -23,10 +34,49 @@ * * @author Oliver Gierke * @author Greg Turnquist + * @author Mark Paluch */ @ContextConfiguration("classpath:eclipselink.xml") class EclipseLinkJpaRepositoryTests extends JpaRepositoryTests { + @PersistenceContext EntityManager em; + + SimpleJpaRepository repository; + User firstUser, secondUser; + + @BeforeEach + @Override + void setUp() { + + super.setUp(); + + repository = new SimpleJpaRepository<>(User.class, em); + + firstUser = new User("Oliver", "Gierke", "gierke@synyx.de"); + firstUser.setAge(28); + secondUser = new User("Joachim", "Arrasz", "arrasz@synyx.de"); + secondUser.setAge(35); + + repository.deleteAll(); + repository.saveAllAndFlush(List.of(firstUser, secondUser)); + } + + @Test // GH-3990 + void deleteAllBySimpleIdInBatch() { + + repository.deleteAllByIdInBatch(List.of(firstUser.getId(), secondUser.getId())); + + assertThat(repository.count()).isZero(); + } + + @Test // GH-3990 + void deleteAllInBatch() { + + repository.deleteAllInBatch(List.of(firstUser, secondUser)); + + assertThat(repository.count()).isZero(); + } + @Override @Disabled("https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477") void deleteAllByIdInBatch() { @@ -38,4 +88,5 @@ void deleteAllByIdInBatch() { void deleteAllByIdInBatchShouldConvertAnIterableToACollection() { // disabled } + } From 2c10b5ecb49a776d4f47509905218fb0d20239fd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 Sep 2025 11:35:19 +0200 Subject: [PATCH 56/93] Skip fenced comments in HQL, EQL and JPQL parsers. Align with Hibernate and allow comments also in EQL and JPQL. Closes #3997 --- .../data/jpa/repository/query/Eql.g4 | 1 + .../data/jpa/repository/query/Hql.g4 | 1 + .../data/jpa/repository/query/Jpql.g4 | 1 + .../repository/query/JpaQueryEnhancer.java | 2 +- .../query/JpaQueryEnhancerUnitTests.java | 57 +++++++++++++++++++ 5 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancerUnitTests.java diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index 0c5468091a..b7ebccc60b 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 @@ -859,6 +859,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..ca8ef1d8f8 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 @@ -846,6 +846,7 @@ reserved_word WS : [ \t\r\n] -> channel(HIDDEN) ; +COMMENT : '/*' (~'*' | '*' ~'/' )* '*/' -> skip; // Build up case-insentive tokens diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index 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/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); + } +} From 989822139ce5935033124b6ccab26145b4dfb072 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 4 Sep 2025 12:01:13 +0200 Subject: [PATCH 57/93] Return deleted entity from derived deleteBy method. We now return the deleted entity and check, guard the delete query against batch deletes if the delete yields more than done result. Closes #3995 --- .../repository/query/JpaQueryExecution.java | 23 +++++++++++-- .../jpa/repository/UserRepositoryTests.java | 32 +++++++++++++++++++ .../jpa/repository/sample/UserRepository.java | 4 +++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 35a680c8fe..23eb878a28 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; /** @@ -290,16 +292,33 @@ public DeleteExecution(EntityManager em) { } @Override - protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) { + protected @Nullable Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) { Query query = jpaQuery.createQuery(accessor); List resultList = query.getResultList(); + boolean simpleBatch = Number.class.isAssignableFrom(jpaQuery.getQueryMethod().getReturnType()) + || org.springframework.data.util.ReflectionUtils.isVoid(jpaQuery.getQueryMethod().getReturnType()); + boolean collectionQuery = jpaQuery.getQueryMethod().isCollectionQuery(); + + if (!simpleBatch && !collectionQuery) { + + if (resultList.size() > 1) { + throw new IncorrectResultSizeDataAccessException( + "Delete query returned more than one element: expected 1, actual " + resultList.size(), 1, + resultList.size()); + } + } + for (Object o : resultList) { em.remove(o); } - return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size(); + if (simpleBatch) { + return resultList.size(); + } + + return collectionQuery ? resultList : CollectionUtils.firstElement(resultList); } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 69433efb09..ac0d25c0f2 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 @@ -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() { 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 85d9603d70..433db0186a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -299,6 +299,10 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 List deleteByLastname(String lastname); + User deleteOneByLastname(String lastname); + + Optional deleteOneOptionalByLastname(String lastname); + /** * @see OPENJPA-2484 */ From 07f87c7b6f287a5e04122c290fe4f55b4292ad87 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 5 Sep 2025 15:00:21 +0200 Subject: [PATCH 58/93] Upgrade to Hibernate 6.6.28.Final. Closes #4005 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 82312b5d84..6d68a5ba7c 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.0 4.0.7 4.0.8-SNAPSHOT - 6.6.25.Final + 6.6.28.Final 6.2.38.Final 6.6.26.Final 7.0.0.Beta5 From 631aefa7af56eef8e2673af514882b78945539d6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 11:41:08 +0200 Subject: [PATCH 59/93] Prepare 3.5.4 (2025.0.4). See #3976 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 6d68a5ba7c..85d3c0e43f 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.4-SNAPSHOT + 3.5.4 @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.4-SNAPSHOT + 3.5.4 0.10.3 org.hibernate @@ -173,20 +173,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 8bfb05a88ac26fb52d52bfa742a9605b6c122de7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 11:41:38 +0200 Subject: [PATCH 60/93] Release version 3.5.4 (2025.0.4). See #3976 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 85d3c0e43f..81bd1a04c8 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.4-SNAPSHOT + 3.5.4 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index e727189c3b..877ea30176 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.4-SNAPSHOT + 3.5.4 org.springframework.data spring-data-jpa-parent - 3.5.4-SNAPSHOT + 3.5.4 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index f3a364ecd0..8717f41ea1 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.4-SNAPSHOT + 3.5.4 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 83396331b6..e9d772837d 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.4-SNAPSHOT + 3.5.4 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.4-SNAPSHOT + 3.5.4 ../pom.xml From 39b40acffa2c8e16e75a74d76411b19b1a6cfa80 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 11:45:32 +0200 Subject: [PATCH 61/93] Prepare next development iteration. See #3976 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 81bd1a04c8..ff80b3a942 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.4 + 3.5.5-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 877ea30176..c9b354d2d1 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.4 + 3.5.5-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.4 + 3.5.5-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 8717f41ea1..7dec44d4f3 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.4 + 3.5.5-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index e9d772837d..ab88d1f903 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.4 + 3.5.5-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.4 + 3.5.5-SNAPSHOT ../pom.xml From 44716180797f300f5892130fb378d9fabb4d1971 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 12 Sep 2025 11:45:33 +0200 Subject: [PATCH 62/93] After release cleanups. See #3976 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index ff80b3a942..8f7b29bc86 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.4 + 3.5.5-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.4 + 3.5.5-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 + From 041898eb35785fb27b1f5b29a6d8e3591b7e97de Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Sep 2025 15:26:18 +0200 Subject: [PATCH 63/93] Fix nested EQL and JPQL aggregation function argument grammar. We now accept a wider range of function arguments instead of limiting to property paths. Closes #4013 --- .../data/jpa/repository/query/Eql.g4 | 9 +++----- .../data/jpa/repository/query/Jpql.g4 | 9 +++----- .../repository/query/EqlQueryRenderer.java | 21 +++--------------- .../repository/query/JpqlQueryRenderer.java | 22 ++++--------------- .../query/EqlQueryRendererTests.java | 8 +++++++ .../query/HqlQueryRendererTests.java | 6 +++++ .../query/JpqlQueryRendererTests.java | 8 +++++++ 7 files changed, 35 insertions(+), 48 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index b7ebccc60b..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 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 ca8ef1d8f8..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 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 1da19ca211..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 @@ -766,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) { @@ -775,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())); @@ -2085,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 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 fae722d524..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 @@ -702,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) { @@ -711,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())); @@ -1982,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 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/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 6abc8b5048..f9d56fdf70 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() { 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() { From 3f8a8cd413b0d1e4c66e01a05014e32e9de8466a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Sep 2025 15:50:33 +0200 Subject: [PATCH 64/93] Fix HQL rendering of CTE with CYCLE clause. Ensure SET identifier is an expression. Closes #4012 --- .../repository/query/HqlQueryRenderer.java | 8 +++--- .../query/HqlQueryRendererTests.java | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) 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 a6936ddbb9..366b6f3e1f 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 @@ -195,22 +195,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; 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 f9d56fdf70..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 @@ -1730,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() { From d90d0541574419d310b47716482438d9f199c0d3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 18 Sep 2025 08:22:53 +0200 Subject: [PATCH 65/93] Fix method return for delete execution returning primitive numbers. We now properly check for assignability of numeric values considering primitive types. Closes #4015 --- .../data/jpa/repository/query/JpaQueryExecution.java | 5 +++-- .../data/jpa/repository/UserRepositoryTests.java | 11 ++++++++++- .../data/jpa/repository/sample/UserRepository.java | 4 ++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index 23eb878a28..febe60805b 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 @@ -296,9 +296,10 @@ public DeleteExecution(EntityManager em) { Query query = jpaQuery.createQuery(accessor); List resultList = query.getResultList(); + Class returnType = jpaQuery.getQueryMethod().getReturnType(); - boolean simpleBatch = Number.class.isAssignableFrom(jpaQuery.getQueryMethod().getReturnType()) - || org.springframework.data.util.ReflectionUtils.isVoid(jpaQuery.getQueryMethod().getReturnType()); + boolean simpleBatch = ClassUtils.isAssignable(Number.class, returnType) + || org.springframework.data.util.ReflectionUtils.isVoid(returnType); boolean collectionQuery = jpaQuery.getQueryMethod().isCollectionQuery(); if (!simpleBatch && !collectionQuery) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index ac0d25c0f2..3928b006fa 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 @@ -1598,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 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 433db0186a..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,6 +296,10 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S // DATAJPA-460 Long removeByLastname(String lastname); + long removeOneByLastname(String lastname); + + int removeOneMoreByLastname(String lastname); + // DATAJPA-460 List deleteByLastname(String lastname); From c835124c8b1ccc0af27790fcb0685aa17e016fae Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 18 Sep 2025 08:55:14 +0200 Subject: [PATCH 66/93] Polishing. Align assignability check for modifying execution. See #4015 --- .../data/jpa/repository/query/JpaQueryExecution.java | 8 ++++---- .../jpa/repository/query/JpaQueryExecutionUnitTests.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java index febe60805b..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 @@ -248,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(); 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)); } From 5f048e93af4a8bac7d5c11be9ac13df508350a4f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 1 Sep 2025 13:16:04 +0200 Subject: [PATCH 67/93] Fix unpaged revision query. We now return all results for an unpaged query. Closes #3999 Original pull request #4000 --- .../support/EnversRevisionRepositoryImpl.java | 7 ++- .../support/RepositoryIntegrationTests.java | 55 ++++++++++++++++--- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index 4515a74ed9..efdaf77c98 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 @@ -182,9 +182,12 @@ public Page> findRevisions(ID id, Pageable pageable) { orderMapped.forEach(baseQuery::addOrder); + if (pageable.isPaged()) { + baseQuery.setFirstResult((int) pageable.getOffset()) // + .setMaxResults(pageable.getPageSize()); + } + List resultList = baseQuery // - .setFirstResult((int) pageable.getOffset()) // - .setMaxResults(pageable.getPageSize()) // .getResultList(); Long count = (Long) createBaseQuery(id) // diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java index 43f159b633..bf04d06e28 100755 --- a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java @@ -15,12 +15,23 @@ */ package org.springframework.data.envers.repository.support; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.history.RevisionMetadata.RevisionType.*; + +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.envers.Config; import org.springframework.data.envers.sample.Country; @@ -33,21 +44,13 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.time.Instant; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.history.RevisionMetadata.RevisionType.*; - /** * Integration tests for repositories. * * @author Oliver Gierke * @author Jens Schauder * @author Niklas Loechte + * @author Mark Paluch */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = Config.class) @@ -107,6 +110,40 @@ void testLifeCycle() { }); } + @Test // GH-3999 + void shouldReturnUnpagedResults() { + + License license = new License(); + license.name = "Schnitzel"; + + licenseRepository.save(license); + + Country de = new Country(); + de.code = "de"; + de.name = "Deutschland"; + + countryRepository.save(de); + + Country se = new Country(); + se.code = "se"; + se.name = "Schweden"; + + countryRepository.save(se); + + license.laender = new HashSet<>(); + license.laender.addAll(Arrays.asList(de, se)); + + licenseRepository.save(license); + + de.name = "Daenemark"; + + countryRepository.save(de); + + Page> revisions = licenseRepository.findRevisions(license.id, Pageable.unpaged()); + + assertThat(revisions).hasSize(2); + } + @Test // #1 void returnsEmptyLastRevisionForUnrevisionedEntity() { assertThat(countryRepository.findLastChangeRevision(100L)).isEmpty(); From 20975c16f4d095f94fecd66f239d8ff86d7d0d5d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 1 Sep 2025 13:16:52 +0200 Subject: [PATCH 68/93] Polishing. Add missing Override annotations. See #3999 Original pull request #4000 --- .../repository/support/EnversRevisionRepositoryImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index efdaf77c98..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) { From 656b971f7e73a4c601530e378fdffde9fa91d3a1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 19 Sep 2025 09:23:25 +0200 Subject: [PATCH 69/93] =?UTF-8?q?Document=20placeholder=20and=20Ant-style?= =?UTF-8?q?=20pattern=20support=20for=20`@Enable=E2=80=A6Repositories`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes spring-projects/spring-data-commons#3366 --- .../repository/config/EnableJpaRepositories.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java index 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 {}; From 114c1f4935fa884ad1f95fded860ffcc18d83466 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Sep 2025 10:51:44 +0200 Subject: [PATCH 70/93] Update GitHub Actions. See #4010 --- .github/workflows/codeql.yml | 21 +++++++++++++++++++++ .github/workflows/project.yml | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..411d4a9338 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,21 @@ +# GitHub Actions for CodeQL Scanning + +name: "CodeQL Advanced" + +on: + push: + pull_request: + workflow_dispatch: + schedule: + # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule + - cron: '0 5 * * *' + +permissions: read-all + +jobs: + codeql-analysis-call: + permissions: + actions: read + contents: read + security-events: write + uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@1 diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index a5f764579a..4c8108d353 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -10,6 +10,11 @@ on: pull_request_target: types: [opened, edited, reopened] +permissions: + contents: read + issues: write + pull-requests: write + jobs: Inbox: runs-on: ubuntu-latest From 3f43ceee1586d8af4cec24678d869c0c460dbdae Mon Sep 17 00:00:00 2001 From: Peter Aisher Date: Thu, 25 Sep 2025 17:21:16 +0200 Subject: [PATCH 71/93] Constistent `unrestricted()` behaviour for all `*Specification` types. Closes #4023 Original pull request: #4024 Signed-off-by: Peter Aisher --- .../data/jpa/domain/Specification.java | 19 ++++++++++++++++++- .../jpa/domain/SpecificationUnitTests.java | 11 +++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index 9ef08b9869..cfa990a741 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 { @@ -65,7 +73,16 @@ static Specification not(@Nullable Specification spec) { } /** - * Simple static factory method to create a specification matching all objects. + * Simple static factory method to create a specification which does not participate in matching. The specification + * returned is {@code null}-like, and is elided in all operations. + * + *

+	 * {@code
+	 * unrestricted().and(other) // consider only `other`
+	 * unrestricted().or(other) // consider only `other`
+	 * not(unrestricted()) // equivalent to `unrestricted()`
+	 * }
+	 * 
* * @param the type of the {@link Root} the resulting {@literal Specification} operates on. * @return guaranteed to be not {@literal null}. 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 4768c15947..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,6 +45,7 @@ * @author Jens Schauder * @author Mark Paluch * @author Daniel Shuy + * @author Peter Aisher */ @SuppressWarnings({ "unchecked", "deprecation", "removal" }) @ExtendWith(MockitoExtension.class) @@ -209,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(Specification.unrestricted()); - Specification notSpec = Specification.not((r, q, cb) -> null); - - assertThat(notSpec.toPredicate(root, query, builder)).isNotNull(); - verify(builder).disjunction(); + assertThat(notSpec.toPredicate(root, query, builder)).isNull(); + verifyNoInteractions(builder); } static class SerializableSpecification implements Serializable, Specification { From 42506df113845f58e983234ea60c553e286340ad Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Sep 2025 10:12:43 +0200 Subject: [PATCH 72/93] Polishing. Refine Javadoc. See #4023 Original pull request: #4024 --- .../org/springframework/data/jpa/domain/Specification.java | 6 ++---- .../data/jpa/domain/SpecificationComposition.java | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/domain/Specification.java index cfa990a741..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 @@ -68,7 +68,7 @@ static Specification not(@Nullable Specification spec) { : (root, query, builder) -> { Predicate predicate = spec.toPredicate(root, query, builder); - return predicate != null ? builder.not(predicate) : builder.disjunction(); + return predicate != null ? builder.not(predicate) : null; }; } @@ -76,12 +76,10 @@ static Specification not(@Nullable Specification spec) { * Simple static factory method to create a specification which does not participate in matching. The specification * returned is {@code null}-like, and is elided in all operations. * - *
-	 * {@code
+	 * 
 	 * unrestricted().and(other) // consider only `other`
 	 * unrestricted().or(other) // consider only `other`
 	 * not(unrestricted()) // equivalent to `unrestricted()`
-	 * }
 	 * 
* * @param the type of the {@link Root} the resulting {@literal Specification} operates on. 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 { From 6a372edc73fe2089ef59e0cc0df804cd9d82c0ef Mon Sep 17 00:00:00 2001 From: KNU-K Date: Fri, 26 Sep 2025 07:11:35 +0900 Subject: [PATCH 73/93] =?UTF-8?q?Replace=20recursion=20in=20`QueryRenderer?= =?UTF-8?q?.isSubquery(=E2=80=A6)`=20with=20loop.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: KNU-K Closes #4025 --- .../repository/query/HqlQueryRenderer.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) 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 366b6f3e1f..7a65ae86e7 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 @@ -29,6 +29,7 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. * + * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl * @since 3.1 @@ -43,19 +44,20 @@ class HqlQueryRenderer extends HqlBaseVisitor { */ static boolean isSubquery(ParserRuleContext ctx) { - if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) { - return true; - } else if (ctx instanceof HqlParser.SelectStatementContext) { - return false; - } else if (ctx instanceof HqlParser.InsertStatementContext) { - return false; - } else if (ctx instanceof HqlParser.DeleteStatementContext) { - return false; - } else if (ctx instanceof HqlParser.UpdateStatementContext) { - return false; - } else { - return ctx.getParent() != null && isSubquery(ctx.getParent()); + while (ctx != null) { + if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) { + return true; + } + if (ctx instanceof HqlParser.SelectStatementContext || + ctx instanceof HqlParser.InsertStatementContext || + ctx instanceof HqlParser.DeleteStatementContext || + ctx instanceof HqlParser.UpdateStatementContext + ) { + return false; + } + ctx = ctx.getParent(); } + return false; } @Override From 05dfa2b805d033a6432794621654b2b4ec64791f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Sep 2025 08:42:39 +0200 Subject: [PATCH 74/93] Polishing. Reformat code and reorder author tags. See #4025 --- .../data/jpa/repository/query/HqlQueryRenderer.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 7a65ae86e7..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 @@ -29,9 +29,10 @@ /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. * - * @author TaeHyun Kang(polyglot-k) * @author Greg Turnquist * @author Christoph Strobl + * @author Mark Paluch + * @author TaeHyun Kang * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode", "UnreachableCode" }) @@ -40,23 +41,26 @@ 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) { while (ctx != null) { + if (ctx instanceof HqlParser.SubqueryContext || ctx instanceof HqlParser.CteContext) { return true; } + if (ctx instanceof HqlParser.SelectStatementContext || ctx instanceof HqlParser.InsertStatementContext || ctx instanceof HqlParser.DeleteStatementContext || - ctx instanceof HqlParser.UpdateStatementContext - ) { + ctx instanceof HqlParser.UpdateStatementContext) { return false; } + ctx = ctx.getParent(); } + return false; } From d854c63f120d04757eb57bb450262bfdb08b80d3 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 21 Dec 2022 22:21:14 +0100 Subject: [PATCH 75/93] Improve query method validation exceptions for declared queries. When validating manually declared queries on repositories, the exception that captures the query to validate now actually also reports it in the exception message. Closes: #2736. Original pull request: #2738 --- .../jpa/repository/query/SimpleJpaQuery.java | 28 +++++++++---------- .../query/SimpleJpaQueryUnitTests.java | 6 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index c9a80e4a38..f84cd46c98 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(), "Validation failed for query %s for method %s", method); if (method.isPageQuery()) { - validateQuery(getCountQuery().getQueryString(), - String.format("Count query validation failed for method %s", method)); + validateQuery(getCountQuery(), "Count query %s validation failed for method %s", method); } } @@ -78,23 +77,24 @@ 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; + var queryString = query.getQueryString(); try { validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager(); - validatingEm.createQuery(query); + validatingEm.createQuery(queryString); } catch (RuntimeException e) { // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider - // https://java.net/projects/jpa-spec/lists/jsr338-experts/archive/2012-07/message/17 - throw new IllegalArgumentException(String.format(errorMessage, arguments), e); + // https://download.oracle.com/javaee-archive/jpa-spec.java.net/users/2012/07/0404.html + throw new IllegalArgumentException(errorMessage.formatted(query, method), e); } finally { 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 45c2470407..66b8edf893 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 @@ -40,7 +40,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -194,14 +193,15 @@ void doesNotValidateCountQueryIfNotPagingMethod() throws Exception { } @Test // DATAJPA-352 - @SuppressWarnings("unchecked") void validatesAndRejectsCountQueryIfPagingMethod() throws Exception { Method method = SampleRepository.class.getMethod("pageByAnnotatedQuery", Pageable.class); when(em.createQuery(Mockito.contains("count"))).thenThrow(IllegalArgumentException.class); - assertThatIllegalArgumentException().isThrownBy(() -> createJpaQuery(method)).withMessageContaining("Count") + assertThatIllegalArgumentException() // + .isThrownBy(() -> createJpaQuery(method)) // + .withMessageContaining("Count") // .withMessageContaining(method.getName()); } From 7ea0132424e2d28817a890c8562baaaedb3c25b1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Sep 2025 09:07:54 +0200 Subject: [PATCH 76/93] Polishing. Refine error message format. See #2736 Original pull request: #2738 --- .../jpa/repository/query/SimpleJpaQuery.java | 20 +++++-------------- .../query/SimpleJpaQueryUnitTests.java | 12 +++++------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/SimpleJpaQuery.java index f84cd46c98..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 @@ -64,10 +64,10 @@ public SimpleJpaQuery(JpaQueryMethod method, EntityManager em, String sourceQuer super(method, em, sourceQuery, countQueryString, queryRewriter, valueExpressionDelegate); - validateQuery(getQuery(), "Validation failed for query %s for method %s", method); + validateQuery(getQuery(), "Query '%s' validation failed for method %s", method); if (method.isPageQuery()) { - validateQuery(getCountQuery(), "Count query %s validation failed for method %s", method); + validateQuery(getCountQuery(), "Count query '%s' validation failed for method %s", method); } } @@ -83,24 +83,14 @@ private void validateQuery(DeclaredQuery query, String errorMessage, JpaQueryMet return; } - EntityManager validatingEm = null; - var queryString = query.getQueryString(); - - try { - validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager(); + String queryString = query.getQueryString(); + try (EntityManager validatingEm = getEntityManager().getEntityManagerFactory().createEntityManager()) { validatingEm.createQuery(queryString); - } catch (RuntimeException e) { // Needed as there's ambiguities in how an invalid query string shall be expressed by the persistence provider // https://download.oracle.com/javaee-archive/jpa-spec.java.net/users/2012/07/0404.html - throw new IllegalArgumentException(errorMessage.formatted(query, method), e); - - } finally { - - if (validatingEm != null) { - validatingEm.close(); - } + throw new IllegalArgumentException(errorMessage.formatted(queryString, method), e); } } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 66b8edf893..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 @@ -192,7 +192,7 @@ void doesNotValidateCountQueryIfNotPagingMethod() throws Exception { createJpaQuery(method); } - @Test // DATAJPA-352 + @Test // DATAJPA-352, GH-2736 void validatesAndRejectsCountQueryIfPagingMethod() throws Exception { Method method = SampleRepository.class.getMethod("pageByAnnotatedQuery", Pageable.class); @@ -201,7 +201,7 @@ void validatesAndRejectsCountQueryIfPagingMethod() throws Exception { assertThatIllegalArgumentException() // .isThrownBy(() -> createJpaQuery(method)) // - .withMessageContaining("Count") // + .withMessageContaining("User u") // .withMessageContaining(method.getName()); } @@ -356,21 +356,21 @@ 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, + 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())); + countQueryString.orElse(queryMethod.getCountQuery())); } private String createQuery(AbstractStringBasedJpaQuery jpaQuery) { From d6011882be3da8dffc9e39290465dd9b10848795 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Oct 2025 09:09:55 +0200 Subject: [PATCH 77/93] Upgrade to Hibernate 6.6.33.Final. Closes: #4042 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8f7b29bc86..718d7fc314 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.0 4.0.7 4.0.8-SNAPSHOT - 6.6.28.Final + 6.6.33.Final 6.2.38.Final 6.6.26.Final 7.0.0.Beta5 From 09fd1a4643ce8c89bd06853b4028d8ffd37557df Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Oct 2025 09:10:36 +0200 Subject: [PATCH 78/93] Upgrade to Eclipselink 4.0.8. Closes: #4043 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 718d7fc314..b7f26844ce 100755 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ 4.13.0 - 4.0.7 + 4.0.8 4.0.8-SNAPSHOT 6.6.33.Final 6.2.38.Final From 2e52a0745fc53856fce9d00464d76bb976ed1b10 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Oct 2025 11:33:04 +0200 Subject: [PATCH 79/93] Prepare 3.5.5 (2025.0.5). See #4010 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index b7f26844ce..f5446946f0 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.5-SNAPSHOT + 3.5.5 @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.5-SNAPSHOT + 3.5.5 0.10.3 org.hibernate @@ -173,20 +173,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From 522fd16ee59ce45cc59a860ff8310e5be964c8fb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Oct 2025 11:34:12 +0200 Subject: [PATCH 80/93] Release version 3.5.5 (2025.0.5). See #4010 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index f5446946f0..72cd9cf647 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.5-SNAPSHOT + 3.5.5 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index c9b354d2d1..3e3bd1bef8 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.5-SNAPSHOT + 3.5.5 org.springframework.data spring-data-jpa-parent - 3.5.5-SNAPSHOT + 3.5.5 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 7dec44d4f3..0a5e5c933c 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.5-SNAPSHOT + 3.5.5 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index ab88d1f903..ca34876da6 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.5-SNAPSHOT + 3.5.5 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.5-SNAPSHOT + 3.5.5 ../pom.xml From d8b2d3467c41c0d3e9e80d215a285836e93aa58e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Oct 2025 11:39:04 +0200 Subject: [PATCH 81/93] Prepare next development iteration. See #4010 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 72cd9cf647..adfa9e83bc 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.5 + 3.5.6-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 3e3bd1bef8..9cd6ae3b6c 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.5 + 3.5.6-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.5 + 3.5.6-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 0a5e5c933c..2852eeea3c 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.5 + 3.5.6-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index ca34876da6..b5168e613c 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.5 + 3.5.6-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.5 + 3.5.6-SNAPSHOT ../pom.xml From ad6abbd4848677beb5d29261b64f54b5b074633b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Oct 2025 11:39:06 +0200 Subject: [PATCH 82/93] After release cleanups. See #4010 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index adfa9e83bc..da591f219a 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.5 + 3.5.6-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.5 + 3.5.6-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 + From 58f6daebe183f9adf38029b7f85456ae1c8b3862 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 4 Oct 2025 21:42:24 +0700 Subject: [PATCH 83/93] Fix typos. Signed-off-by: Tran Ngoc Nhan Closes #4033 --- .../envers/repository/config/EnableEnversRepositories.java | 2 +- .../repository/support/JpaPersistableEntityInformation.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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-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(); } + } From b7f9542df9a6c4128299725bbabd2f048caf569c Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 25 Oct 2025 13:40:06 +0700 Subject: [PATCH 84/93] Fix typos in methods and reference docs. Closes: #4058 Signed-off-by: Tran Ngoc Nhan --- .../jpa/mapping/JpaMetamodelMappingContextUnitTests.java | 2 +- .../EntityGraphRepositoryMethodsIntegrationTests.java | 6 +++--- .../springframework/data/jpa/repository/GreetingsFrom.java | 2 +- .../data/jpa/repository/UserRepositoryTests.java | 2 +- .../jpa/repository/query/DefaultQueryUtilsUnitTests.java | 2 +- .../data/jpa/repository/query/Jpa21UtilsTests.java | 2 +- .../data/jpa/repository/sample/UserRepositoryCustom.java | 2 +- .../data/jpa/repository/sample/UserRepositoryImpl.java | 2 +- .../support/DefaultJpaContextIntegrationTests.java | 2 +- ...sitoryFactoryBeanEntityPathResolverIntegrationTests.java | 2 +- .../ROOT/pages/jpa/misc-merging-persistence-units.adoc | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) 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/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/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 3928b006fa..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 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/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/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/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/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] ---- From b2250393a267cfd67335ac0d7f9e052fef7eb102 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 10 Nov 2025 11:23:36 +0100 Subject: [PATCH 85/93] Upgrade to Hibernate 6.6.34.Final. Closes #4073 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index da591f219a..da89a0d4aa 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.0 4.0.8 4.0.8-SNAPSHOT - 6.6.33.Final + 6.6.34.Final 6.2.38.Final 6.6.26.Final 7.0.0.Beta5 From 7b9e9385521ddb3ff27b3e96335e0ca9b0976197 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 14 Nov 2025 08:47:49 +0100 Subject: [PATCH 86/93] Upgrade to Hibernate 6.6.35.Final. Closes: #4078 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index da89a0d4aa..aaf80d950a 100755 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 4.13.0 4.0.8 4.0.8-SNAPSHOT - 6.6.34.Final + 6.6.35.Final 6.2.38.Final 6.6.26.Final 7.0.0.Beta5 From f4d075021bac6bb6c591bf5ba85e2555dc1778fd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 29 Oct 2025 12:03:06 +0100 Subject: [PATCH 87/93] Update security.adoc. Closes #4065 Original pull request: #4066 --- SECURITY.adoc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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. From 32d3978d546bd6b6d45c85cd8ac6df73fa76b71c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 14 Nov 2025 11:29:46 +0100 Subject: [PATCH 88/93] Prepare 3.5.6 (2025.0.6). See #4050 --- pom.xml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index aaf80d950a..6aafc4c3e6 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.6-SNAPSHOT + 3.5.6 @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.6-SNAPSHOT + 3.5.6 0.10.3 org.hibernate @@ -173,20 +173,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + From ebcbbd69fbac5b1993f8e86beefe78784eb7c8b0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 14 Nov 2025 11:30:07 +0100 Subject: [PATCH 89/93] Release version 3.5.6 (2025.0.6). See #4050 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 6aafc4c3e6..be96c321cd 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.6-SNAPSHOT + 3.5.6 pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 9cd6ae3b6c..f0e333ad24 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.6-SNAPSHOT + 3.5.6 org.springframework.data spring-data-jpa-parent - 3.5.6-SNAPSHOT + 3.5.6 ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 2852eeea3c..5b2c61d583 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.6-SNAPSHOT + 3.5.6 ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index b5168e613c..dbcf79efe7 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.6-SNAPSHOT + 3.5.6 Spring Data JPA Spring Data module for JPA repositories. @@ -16,7 +16,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.6-SNAPSHOT + 3.5.6 ../pom.xml From 135d6198ac6efe5792e2daaa9282ddd9b06c87ae Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 14 Nov 2025 11:33:14 +0100 Subject: [PATCH 90/93] Prepare next development iteration. See #4050 --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index be96c321cd..1e7d34ac71 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.5.6 + 3.5.7-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index f0e333ad24..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.6 + 3.5.7-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.5.6 + 3.5.7-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 5b2c61d583..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.6 + 3.5.7-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index dbcf79efe7..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.6 + 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.6 + 3.5.7-SNAPSHOT ../pom.xml From ef820cb90b4608c970ae5608961eec684a78277d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 14 Nov 2025 11:33:15 +0100 Subject: [PATCH 91/93] After release cleanups. See #4050 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 1e7d34ac71..351f148c23 100755 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.data.build spring-data-parent - 3.5.6 + 3.5.7-SNAPSHOT @@ -41,7 +41,7 @@ 5.2 9.2.0 42.7.7 - 3.5.6 + 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 + From 122e21eb0c218c304e00ef5a4c8765ce72ac5488 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Nov 2025 09:36:38 +0100 Subject: [PATCH 92/93] Defer `ReturnedType.inputProperties` access. Closes #4088 --- .../repository/query/AbstractJpaQuery.java | 21 +++++++++++++++++-- .../query/AbstractStringBasedJpaQuery.java | 3 ++- .../DtoProjectionTransformerDelegate.java | 11 +++++----- .../query/EqlSortedQueryTransformer.java | 2 +- .../query/HqlSortedQueryTransformer.java | 2 +- .../query/JpqlSortedQueryTransformer.java | 2 +- 6 files changed, 30 insertions(+), 11 deletions(-) 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 45d5ed13b7..bb29738ab5 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 @@ -161,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) { @@ -313,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; @@ -346,7 +363,7 @@ 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(); + && type.needsCustomConstruction(); if (this.dtoProjection) { this.preferredConstructor = PreferredConstructorDiscoverer.discover(type.getReturnedType()); 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 6412d195d2..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 @@ -144,7 +144,8 @@ ReturnedType getReturnedType(ResultProcessor processor) { ReturnedType returnedType = processor.getReturnedType(); Class returnedJavaType = returnedType.getReturnedType(); - if (!returnedType.isProjecting() || returnedJavaType.isInterface() || query.isNativeQuery()) { + if (query.hasConstructorExpression() || !returnedType.isProjecting() || returnedJavaType.isInterface() + || query.isNativeQuery()) { 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 28de9ba657..70cc36813f 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 @@ -23,6 +23,7 @@ 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. @@ -40,21 +41,21 @@ class DtoProjectionTransformerDelegate { private final ReturnedType returnedType; - private final boolean applyRewriting; + private final Lazy applyRewriting; private final List selectItems = new ArrayList<>(); public DtoProjectionTransformerDelegate(ReturnedType returnedType) { this.returnedType = returnedType; - this.applyRewriting = returnedType.isProjecting() && !returnedType.getReturnedType().isInterface() - && returnedType.needsCustomConstruction(); + this.applyRewriting = Lazy.of(() -> returnedType.isProjecting() && !returnedType.getReturnedType().isInterface() + && returnedType.needsCustomConstruction()); } public boolean applyRewriting() { - return applyRewriting; + return applyRewriting.get(); } public boolean canRewrite() { - return applyRewriting() && !selectItems.isEmpty(); + return !selectItems.isEmpty() && applyRewriting(); } public void appendSelectItem(QueryTokenStream selectItem) { 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 e54979e008..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 @@ -121,7 +121,7 @@ public QueryTokenStream visitSelect_expression(EqlParser.Select_expressionContex QueryTokenStream selectItem = super.visitSelect_expression(ctx); - if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + if (ctx.constructor_expression() == null && dtoDelegate != null && dtoDelegate.applyRewriting()) { dtoDelegate.appendSelectItem(selectItem); } 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 43083634ff..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 @@ -103,7 +103,7 @@ public QueryTokenStream visitSelectExpression(HqlParser.SelectExpressionContext QueryTokenStream selectItem = super.visitSelectExpression(ctx); - if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.instantiation() == null && !isSubquery(ctx)) { + if (ctx.instantiation() == null && !isSubquery(ctx) && dtoDelegate != null && dtoDelegate.applyRewriting()) { dtoDelegate.appendSelectItem(QueryRenderer.expression(selectItem)); } 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 cca1ae1985..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 @@ -113,7 +113,7 @@ public QueryTokenStream visitSelect_expression(JpqlParser.Select_expressionConte QueryTokenStream selectItem = super.visitSelect_expression(ctx); - if (dtoDelegate != null && dtoDelegate.applyRewriting() && ctx.constructor_expression() == null) { + if (ctx.constructor_expression() == null && dtoDelegate != null && dtoDelegate.applyRewriting()) { dtoDelegate.appendSelectItem(selectItem); } From 3c84f2ac423265830649c9ce8ff0a3b9436bb6fb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Nov 2025 10:06:22 +0100 Subject: [PATCH 93/93] Polishing. Use ReturnedType.isDtoProjection/isInterfaceProjection methods. See #4088 --- .../jpa/repository/query/AbstractJpaQuery.java | 4 ++-- .../query/DtoProjectionTransformerDelegate.java | 2 +- .../data/jpa/repository/query/NativeJpaQuery.java | 2 +- .../support/FetchableFluentQueryByPredicate.java | 4 ++-- .../repository/support/SimpleJpaRepository.java | 2 +- .../repository/query/TupleConverterUnitTests.java | 14 +++++++++----- 6 files changed, 16 insertions(+), 12 deletions(-) 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 bb29738ab5..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 @@ -291,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; @@ -362,7 +362,7 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) { this.type = type; this.tupleWrapper = nativeQuery ? FallbackTupleWrapper::new : UnaryOperator.identity(); - this.dtoProjection = type.isProjecting() && !type.getReturnedType().isInterface() + this.dtoProjection = type.isDtoProjection() && type.needsCustomConstruction(); if (this.dtoProjection) { 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 70cc36813f..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 @@ -46,7 +46,7 @@ class DtoProjectionTransformerDelegate { public DtoProjectionTransformerDelegate(ReturnedType returnedType) { this.returnedType = returnedType; - this.applyRewriting = Lazy.of(() -> returnedType.isProjecting() && !returnedType.getReturnedType().isInterface() + this.applyRewriting = Lazy.of(() -> returnedType.isDtoProjection() && returnedType.needsCustomConstruction()); } 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/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/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 73a6e52139..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 @@ -794,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(); 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);