From 125f233eb631859b1d80bd551cfdd4b0bb492ade Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 30 Jan 2025 16:22:45 +0100 Subject: [PATCH] feature: Allow Cypher `LIST` to be used directly as repository methods returning collections. Signed-off-by: Michael Simons --- .../data/neo4j/core/DefaultNeo4jClient.java | 8 +++++++- .../data/neo4j/core/DefaultReactiveNeo4jClient.java | 8 +++++++- .../data/neo4j/core/Neo4jTemplate.java | 6 +++--- .../data/neo4j/core/SingleValueMappingFunction.java | 6 +++++- .../neo4j/integration/imperative/RepositoryIT.java | 12 ++++++++++++ .../imperative/repositories/PersonRepository.java | 6 ++++++ .../integration/reactive/ReactiveRepositoryIT.java | 12 ++++++++++++ .../repositories/ReactivePersonRepository.java | 6 ++++++ 8 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 525ae1446..f2884c944 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -24,6 +24,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.neo4j.driver.Bookmark; import org.neo4j.driver.Driver; @@ -472,7 +473,12 @@ final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware { try (QueryRunner statementRunner = getQueryRunner(this.databaseSelection, this.impersonatedUser)) { Result result = runnableStatement.runWith(statementRunner); - Collection values = result.stream().map(partialMappingFunction(TypeSystem.getDefault())).filter(Objects::nonNull).collect(Collectors.toList()); + Collection values = result.stream().flatMap(r -> { + if (mappingFunction instanceof SingleValueMappingFunction && r.size() == 1 && r.get(0).hasType(TypeSystem.getDefault().LIST())) { + return r.get(0).asList(v -> ((SingleValueMappingFunction) mappingFunction).convertValue(v)).stream(); + } + return Stream.of(partialMappingFunction(TypeSystem.getDefault()).apply(r)); + }).filter(Objects::nonNull).collect(Collectors.toList()); ResultSummaries.process(result.consume()); return values; } catch (RuntimeException e) { diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java index 5ec4dc978..dc188aad9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultReactiveNeo4jClient.java @@ -408,7 +408,13 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati Flux executeWith(Tuple2> t, ReactiveQueryRunner runner) { return Flux.usingWhen(Flux.from(runner.run(t.getT1(), t.getT2())), - result -> Flux.from(result.records()).mapNotNull(r -> mappingFunction.apply(TypeSystem.getDefault(), r)), + result -> Flux.from(result.records()).flatMap(r -> { + if (mappingFunction instanceof SingleValueMappingFunction && r.size() == 1 && r.get(0).hasType(TypeSystem.getDefault().LIST())) { + return Flux.fromStream(r.get(0).asList(v -> ((SingleValueMappingFunction) mappingFunction).convertValue(v)).stream()); + } + var item = mappingFunction.apply(TypeSystem.getDefault(), r); + return item == null ? Flux.empty() : Flux.just(item); + }), result -> Flux.from(result.consume()).doOnNext(ResultSummaries::process)); } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 3c189c364..f25950666 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -1268,7 +1268,7 @@ public final class Neo4jTemplate implements if (preparedQuery.resultsHaveBeenAggregated()) { return all.stream().flatMap(nested -> ((Collection) nested).stream()).distinct().collect(Collectors.toList()); } - return all.stream().collect(Collectors.toList()); + return new ArrayList<>(all); }); } @@ -1328,8 +1328,8 @@ public final class Neo4jTemplate implements Neo4jClient.MappingSpec newMappingSpec = neo4jClient.query(cypherQuery) .bindAll(finalParameters).fetchAs(preparedQuery.getResultType()); - return Optional.of(preparedQuery.getOptionalMappingFunction() - .map(newMappingSpec::mappedBy).orElse(newMappingSpec)); + return preparedQuery.getOptionalMappingFunction() + .map(newMappingSpec::mappedBy).or(() -> Optional.of(newMappingSpec)); } private NodesAndRelationshipsByIdStatementProvider createNodesAndRelationshipsByIdStatementProvider(Neo4jPersistentEntity entityMetaData, diff --git a/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java b/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java index b164b6c91..25e9e7747 100644 --- a/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java +++ b/src/main/java/org/springframework/data/neo4j/core/SingleValueMappingFunction.java @@ -54,7 +54,11 @@ final class SingleValueMappingFunction implements BiFunction findPageByCustomQueryWithCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable); + @Query("UNWIND ['a', 'b', 'c'] AS x RETURN x") + List noDomainType(); + + @Query("RETURN ['a', 'b', 'c']") + List noDomainTypeWithListInQuery(); + Long countAllByNameOrName(String aName, String anotherName); Optional findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName); diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index e2f6e5060..d311a8d6c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -250,6 +250,18 @@ class ReactiveRepositoryIT { .expectNextMatches(personList::contains).verifyComplete(); } + @Test + void noDomainType(@Autowired ReactivePersonRepository repository) { + var strings = repository.noDomainTypeAsFlux(); + StepVerifier.create(strings).expectNext("a", "b", "c").verifyComplete(); + } + + @Test + void noDomainTypeWithListInQueryShouldWork(@Autowired ReactivePersonRepository repository) { + var strings = repository.noDomainTypeWithListInQuery(); + StepVerifier.create(strings).expectNext("a", "b", "c").verifyComplete(); + } + @Test void findAllByIdsPublisher(@Autowired ReactivePersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java index 11b36ab76..79b1d9405 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java @@ -48,6 +48,12 @@ public interface ReactivePersonRepository extends ReactiveNeo4jRepository getOnePersonViaQuery(); + @Query("UNWIND ['a', 'b', 'c'] AS x RETURN x") + Flux noDomainTypeAsFlux(); + + @Query("RETURN ['a', 'b', 'c']") + Flux noDomainTypeWithListInQuery(); + Mono findOneByNameAndFirstName(String name, String firstName); Mono findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName);