From 4ae1c5d94b9b18759a3b0397a60612b1daca8420 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 6 Nov 2025 16:21:12 +0100 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b23a16229f..fd7f2cd40c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5089-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..4e39598cf5 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5089-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 37730f7d40..b39b6d46d4 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5089-SNAPSHOT ../pom.xml From 076cf06f0fb6eb6570d65ecdcc15f15fa25d04c4 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 7 Nov 2025 08:48:26 +0100 Subject: [PATCH 2/2] Support Streamable return type in AOT repositories. --- .../repository/aot/AggregationBlocks.java | 10 ++++- .../mongodb/repository/aot/QueryBlocks.java | 10 ++++- .../test/java/example/aot/UserRepository.java | 8 ++++ ...tractPersonRepositoryIntegrationTests.java | 38 +++++++++++++++++++ .../mongodb/repository/PersonRepository.java | 8 ++++ .../aot/QueryMethodContributionUnitTests.java | 19 ++++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index 38ac76c0b4..56ca6db4e7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -41,6 +41,7 @@ import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.util.ReflectionUtils; +import org.springframework.data.util.Streamable; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.util.ClassUtils; @@ -145,8 +146,15 @@ CodeBlock build() { builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); } else { - builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, + + CodeBlock resultBlock = CodeBlock.of("$L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, aggregationVariableName, outputType); + + if (queryMethod.getReturnType().getType().equals(Streamable.class)) { + resultBlock = CodeBlock.of("$T.of($L)", Streamable.class, resultBlock); + } + + builder.addStatement("return $L", resultBlock); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index a1b86ab010..2ed605c02f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -34,6 +34,7 @@ import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.util.Lazy; +import org.springframework.data.util.Streamable; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -145,8 +146,15 @@ CodeBlock build() { context.localVariable("finder"), query.name(), terminatingMethod, returnType); } else { - builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(), + + CodeBlock resultBlock = CodeBlock.of("$L.matching($L).$L", context.localVariable("finder"), query.name(), terminatingMethod); + + if (queryMethod.getReturnType().getType().equals(Streamable.class)) { + resultBlock = CodeBlock.of("$T.of($L)", Streamable.class, resultBlock); + } + + builder.addStatement("return $L", resultBlock); } } diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 2f8b8a4cbc..b7ffe22193 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -48,6 +48,7 @@ import org.springframework.data.mongodb.repository.VectorSearch; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; +import org.springframework.data.util.Streamable; /** * @author Christoph Strobl @@ -58,6 +59,8 @@ public interface UserRepository extends CrudRepository { List findUserNoArgumentsBy(); + Streamable streamUserNoArgumentsBy(); + User findOneByUsername(String username); Optional findOptionalOneByUsername(String username); @@ -267,6 +270,11 @@ public interface UserRepository extends CrudRepository { "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) Stream streamGroupByLastnameAndAsAggregationResults(String property); + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + Streamable streamAsStreamableGroupByLastnameAndAsAggregationResults(String property); + @Aggregation(pipeline = { // "{ '$match' : { 'posts' : { '$ne' : null } } }", // "{ '$project': { 'nrPosts' : {'$size': '$posts' } } }", // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index c361d5513d..c7eca37c30 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -79,6 +79,7 @@ import org.springframework.data.mongodb.test.util.DirtiesStateExtension.ProvidesState; import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.querydsl.QSort; +import org.springframework.data.util.Streamable; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -312,6 +313,33 @@ void findsPersonByAddressCorrectly() { assertThat(result).hasSize(1).contains(dave); } + @Test // GH-5089 + void streamPersonByAddressCorrectly() { + + Address address = new Address("Foo Street 1", "C0123", "Bar"); + dave.setAddress(address); + repository.save(dave); + + Streamable result = repository.streamByAddress(address); + assertThat(result).hasSize(1).contains(dave); + } + + @Test // GH-5089 + void streamPersonByAddressCorrectlyWhenPaged() { + + Address address = new Address("Foo Street 1", "C0123", "Bar"); + dave.setAddress(address); + oliver.setAddress(address); + repository.saveAll(List.of(dave, oliver)); + + Streamable result = repository.streamByAddress(address, + PageRequest.of(0, 1, Sort.by(Direction.DESC, "firstname"))); + assertThat(result).containsExactly(oliver); + + result = repository.streamByAddress(address, PageRequest.of(1, 1, Sort.by(Direction.DESC, "firstname"))); + assertThat(result).containsExactly(dave); + } + @Test void findsPeopleByZipCode() { @@ -1516,6 +1544,16 @@ void annotatedAggregationWithPageable() { new PersonAggregate("Matthews", Arrays.asList("Dave", "Oliver August"))); } + @Test // GH-5089 + void annotatedAggregationReturningStreamable() { + + assertThat(repository.streamGroupByLastnameAnd("firstname", PageRequest.of(1, 2, Sort.by("lastname")))) // + .isInstanceOf(Streamable.class) // + .containsExactly( // + new PersonAggregate("Lessard", Collections.singletonList("Stefan")), // + new PersonAggregate("Matthews", Arrays.asList("Dave", "Oliver August"))); + } + @Test // DATAMONGO-2153 void annotatedAggregationWithSingleSimpleResult() { assertThat(repository.sumAge()).isEqualTo(245); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index a9f694e200..7718c12411 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -45,6 +45,7 @@ import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; +import org.springframework.data.util.Streamable; /** * Sample repository managing {@link Person} entities. @@ -211,6 +212,10 @@ Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname */ List findByAddress(Address address); + Streamable streamByAddress(Address address); + + Streamable streamByAddress(Address address, Pageable pageable); + List findByAddressZipCode(String zipCode); List findByLastnameLikeAndAgeBetween(String lastname, int from, int to); @@ -442,6 +447,9 @@ Page findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") List groupByLastnameAnd(String property, Pageable page); + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + Streamable streamGroupByLastnameAnd(String property, Pageable page); + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") int sumAge(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java index 9598e50c52..f5834a67e3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java @@ -387,6 +387,25 @@ void rendersVectorSearchOrderByWithScoreLast() throws NoSuchMethodException { "Document(\"$sort\", mappedSort.append(\"__score__\", -1))"); } + @Test // GH-5089 + void rendersStreamableReturnType() throws NoSuchMethodException { + + MethodSpec methodSpec = codeOf(UserRepository.class, "streamUserNoArgumentsBy"); + + assertThat(methodSpec.toString()) // + .containsSubsequence("return", "Streamable.of(", "all())"); + } + + @Test // GH-5089 + void rendersStreamableReturnTypeForAggregation() throws NoSuchMethodException { + + MethodSpec methodSpec = codeOf(UserRepository.class, "streamAsStreamableGroupByLastnameAndAsAggregationResults", + String.class); + + assertThat(methodSpec.toString()) // + .containsSubsequence("return", "Streamable.of(", "getMappedResults())"); + } + private static MethodSpec codeOf(Class repository, String methodName, Class... args) throws NoSuchMethodException {