feature: Allow Cypher LIST<ANY> to be used directly as repository methods returning collections.

Signed-off-by: Michael Simons <michael@simons.ac>
This commit is contained in:
Michael Simons
2025-01-30 16:22:45 +01:00
parent c2cbc473bf
commit 56d049d894
8 changed files with 58 additions and 6 deletions

View File

@@ -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<T> values = result.stream().map(partialMappingFunction(TypeSystem.getDefault())).filter(Objects::nonNull).collect(Collectors.toList());
Collection<T> 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<T>) 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) {

View File

@@ -408,7 +408,13 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, Applicati
Flux<T> executeWith(Tuple2<String, Map<String, Object>> 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<T>) 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));
}

View File

@@ -1268,7 +1268,7 @@ public final class Neo4jTemplate implements
if (preparedQuery.resultsHaveBeenAggregated()) {
return all.stream().flatMap(nested -> ((Collection<T>) 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<T> 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,

View File

@@ -54,7 +54,11 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
throw new IllegalArgumentException("Records with more than one value cannot be converted without a mapper");
}
Value source = record.get(0);
return convertValue(record.get(0));
}
@Nullable
T convertValue(@Nullable Value source) {
if (targetClass == Void.class || targetClass == void.class) {
return null;
}

View File

@@ -265,6 +265,18 @@ class RepositoryIT {
RETURN *""");
}
@Test
void noDomainType(@Autowired PersonRepository repository) {
var strings = repository.noDomainType();
assertThat(strings).containsExactly("a", "b", "c");
}
@Test
void noDomainTypeWithListInQueryShouldWork(@Autowired PersonRepository repository) {
var strings = repository.noDomainTypeWithListInQuery();
assertThat(strings).containsExactly("a", "b", "c");
}
@Test
void findAll(@Autowired PersonRepository repository) {

View File

@@ -131,6 +131,12 @@ public interface PersonRepository extends Neo4jRepository<PersonWithAllConstruct
countQuery = "MATCH (n:PersonWithAllConstructor) WHERE n.name = $aName OR n.name = $anotherName RETURN count(n)")
Page<PersonWithAllConstructor> findPageByCustomQueryWithCount(@Param("aName") String aName, @Param("anotherName") String anotherName, Pageable pageable);
@Query("UNWIND ['a', 'b', 'c'] AS x RETURN x")
List<String> noDomainType();
@Query("RETURN ['a', 'b', 'c']")
List<String> noDomainTypeWithListInQuery();
Long countAllByNameOrName(String aName, String anotherName);
Optional<PersonWithAllConstructor> findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName);

View File

@@ -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) {

View File

@@ -48,6 +48,12 @@ public interface ReactivePersonRepository extends ReactiveNeo4jRepository<Person
@Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n")
Mono<PersonWithAllConstructor> getOnePersonViaQuery();
@Query("UNWIND ['a', 'b', 'c'] AS x RETURN x")
Flux<String> noDomainTypeAsFlux();
@Query("RETURN ['a', 'b', 'c']")
Flux<String> noDomainTypeWithListInQuery();
Mono<PersonWithAllConstructor> findOneByNameAndFirstName(String name, String firstName);
Mono<PersonWithAllConstructor> findOneByNameAndFirstNameAllIgnoreCase(String name, String firstName);