diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java index ef6030e38..5571df177 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java @@ -120,7 +120,7 @@ final class FetchableFluentQueryByExample extends FluentQuerySupport im public FetchableFluentQuery project(Collection properties) { return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java index 19159f6d7..5843822ae 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java @@ -125,7 +125,7 @@ final class FetchableFluentQueryByPredicate extends FluentQuerySupport public FetchableFluentQuery project(Collection properties) { return new FetchableFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, this.resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java index 40ee97fb0..44693d694 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.repository.query; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -97,12 +98,12 @@ abstract class FluentQuerySupport { Collections.reverse(rawResult); } - IntFunction ding = null; + IntFunction positionFunction = null; if (scrollPosition instanceof OffsetScrollPosition) { - ding = OffsetScrollPosition.positionFunction(skip); + positionFunction = OffsetScrollPosition.positionFunction(skip); } else { - ding = v -> { + positionFunction = v -> { var accessor = entity.getPropertyAccessor(rawResult.get(v)); var keys = new LinkedHashMap(); sort.forEach(o -> { @@ -114,7 +115,22 @@ abstract class FluentQuerySupport { return ScrollPosition.forward(keys); }; } - return Window.from(getSubList(rawResult, limit, scrollDirection), ding, hasMoreElements(rawResult, limit)); + return Window.from(getSubList(rawResult, limit, scrollDirection), positionFunction, hasMoreElements(rawResult, limit)); + } + + final Collection extractAllPaths(Collection projectingProperties) { + if (projectingProperties.isEmpty()) { + return new HashSet<>(); + } + + Set allPaths = new HashSet<>(); + for (String property : projectingProperties) { + if (property.contains(".")) { + allPaths.addAll(Arrays.stream(property.split("\\.")).toList()); + } + allPaths.add(property); + } + return allPaths; } private static boolean hasMoreElements(List result, @Nullable Integer limit) { diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java index 658606da1..0f3c4f019 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java @@ -121,7 +121,7 @@ final class ReactiveFluentQueryByExample extends FluentQuerySupport imp public ReactiveFluentQuery project(Collection properties) { return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); } @Override diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java index 8e08f74bb..9bd1f7a14 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java @@ -124,7 +124,7 @@ import com.querydsl.core.types.Predicate; public ReactiveFluentQuery project(Collection properties) { return new ReactiveFluentQueryByPredicate<>(this.predicate, this.mappingContext, this.metaData, resultType, this.findOperation, - this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(properties)); + this.countOperation, this.existsOperation, this.sort, this.limit, mergeProperties(extractAllPaths(properties))); } @Override diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 97ca60fa0..0cff07d3e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -2777,6 +2777,21 @@ class RepositoryIT { true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, createdAt.toInstant()); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + + transaction.run(""" + CREATE (lhr:Airport {code: 'LHR', name: 'London Heathrow'}) + CREATE (lax:Airport {code: 'LAX', name: 'Los Angeles'}) + CREATE (cdg:Airport {code: 'CDG', name: 'Paris Charles de Gaulle'}) + CREATE (f1:Flight {name: 'FL 001'}) + CREATE (f2:Flight {name: 'FL 002'}) + CREATE (f3:Flight {name: 'FL 003'}) + CREATE (f1) -[:DEPARTS] ->(lhr) + CREATE (f1) -[:ARRIVES] ->(lax) + CREATE (f2) -[:DEPARTS] ->(lhr) + CREATE (f2) -[:ARRIVES] ->(cdg) + CREATE (f3) -[:DEPARTS] ->(lax) + CREATE (f3) -[:ARRIVES] ->(lhr) + """); } @Test @@ -2847,6 +2862,24 @@ class RepositoryIT { }); } + @Test + void findAllByExampleFluentProjectingRelationships(@Autowired FlightRepository repository) { + Example example = Example.of(new Flight("FL 001", null, null), + ExampleMatcher.matchingAll().withIgnoreNullValues()); + List flights = repository.findBy(example, + q -> q.project("name", "departure.name").all()); + + assertThat(flights) + .hasSize(1) + .first().satisfies(p -> { + assertThat(p.getName()).isEqualTo("FL 001"); + assertThat(p.getArrival()).isNull(); + assertThat(p.getDeparture()).isNotNull(); + assertThat(p.getDeparture().getName()).isEqualTo("London Heathrow"); + assertThat(p.getDeparture().getCode()).isNull(); + }); + } + @Test // GH-2343 void findAllByExampleFluentAs(@Autowired PersonRepository repository) { 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 da263ad5c..141729164 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 @@ -71,6 +71,7 @@ import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider; import org.springframework.data.neo4j.core.UserSelection; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.reactive.repositories.ReactiveFlightRepository; import org.springframework.data.neo4j.integration.reactive.repositories.ReactivePersonRepository; import org.springframework.data.neo4j.integration.reactive.repositories.ReactiveThingRepository; import org.springframework.data.neo4j.integration.shared.common.AltHobby; @@ -86,6 +87,7 @@ import org.springframework.data.neo4j.integration.shared.common.DeepRelationship import org.springframework.data.neo4j.integration.shared.common.DtoPersonProjection; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels; import org.springframework.data.neo4j.integration.shared.common.EntityWithConvertedId; +import org.springframework.data.neo4j.integration.shared.common.Flight; import org.springframework.data.neo4j.integration.shared.common.Hobby; import org.springframework.data.neo4j.integration.shared.common.ImmutablePerson; import org.springframework.data.neo4j.integration.shared.common.LikesHobbyRelationship; @@ -190,6 +192,21 @@ class ReactiveRepositoryIT { person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); + + transaction.run(""" + CREATE (lhr:Airport {code: 'LHR', name: 'London Heathrow'}) + CREATE (lax:Airport {code: 'LAX', name: 'Los Angeles'}) + CREATE (cdg:Airport {code: 'CDG', name: 'Paris Charles de Gaulle'}) + CREATE (f1:Flight {name: 'FL 001'}) + CREATE (f2:Flight {name: 'FL 002'}) + CREATE (f3:Flight {name: 'FL 003'}) + CREATE (f1) -[:DEPARTS] ->(lhr) + CREATE (f1) -[:ARRIVES] ->(lax) + CREATE (f2) -[:DEPARTS] ->(lhr) + CREATE (f2) -[:ARRIVES] ->(cdg) + CREATE (f3) -[:DEPARTS] ->(lax) + CREATE (f3) -[:ARRIVES] ->(lhr) + """); } @Test @@ -337,6 +354,25 @@ class ReactiveRepositoryIT { }).verifyComplete(); } + @Test + void findAllByExampleFluentProjectingRelationships(@Autowired ReactiveFlightRepository repository) { + + Example example = Example.of(new Flight("FL 001", null, null), + ExampleMatcher.matchingAll().withIgnoreNullValues()); + + repository.findBy(example, q -> q.project("name", "departure.name").all()) + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getName()).isEqualTo("FL 001"); + assertThat(p.getArrival()).isNull(); + assertThat(p.getDeparture()).isNotNull(); + assertThat(p.getDeparture().getName()).isEqualTo("London Heathrow"); + assertThat(p.getDeparture().getCode()).isNull(); + + return true; + }).verifyComplete(); + } + @Test // GH-2343 void findAllByExampleFluentAs(@Autowired ReactivePersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java new file mode 100644 index 000000000..322780da8 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactiveFlightRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2011-2023 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.neo4j.integration.reactive.repositories; + +import org.springframework.data.neo4j.integration.shared.common.Flight; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +/** + * @author Gerrit Meier + */ +public interface ReactiveFlightRepository extends ReactiveNeo4jRepository { +}