Skip to content

Commit e815baa

Browse files
GH-2269 - Allow simple return pattern for single results.
This allows to ommit the artifial collection of relationships and related nodes for one-to-one relationships and closes #2269.
1 parent 4089155 commit e815baa

File tree

4 files changed

+197
-17
lines changed

4 files changed

+197
-17
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java

+51-17
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import java.util.function.Function;
3131
import java.util.function.Predicate;
3232
import java.util.function.Supplier;
33-
import java.util.stream.Collectors;
3433
import java.util.stream.StreamSupport;
3534

3635
import org.neo4j.driver.Value;
@@ -111,7 +110,12 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
111110
}
112111
}
113112

114-
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, Neo4jPersistentEntity<R> rootNodeDescription) {
113+
@Nullable
114+
private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Neo4jPersistentEntity<R> rootNodeDescription) {
115+
116+
if (rootNodeDescription == null) {
117+
return null;
118+
}
115119

116120
List<String> primaryLabels = new ArrayList<>();
117121
primaryLabels.add(rootNodeDescription.getPrimaryLabel());
@@ -354,7 +358,7 @@ private boolean containsOnePlainNode(MapAccessor queryResult) {
354358

355359
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, MapAccessor values, MapAccessor allValues,
356360
Collection<RelationshipDescription> relationships, Collection<String> surplusLabels,
357-
Object lastMappedEntity) {
361+
@Nullable Object lastMappedEntity) {
358362

359363
ParameterValueProvider<Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<Neo4jPersistentProperty>() {
360364
@Override
@@ -459,22 +463,10 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
459463

460464
if (Values.NULL.equals(list)) {
461465

462-
Collection<Relationship> allMatchingTypeRelationshipsInResult = new ArrayList<>();
463-
Collection<Node> allNodesWithMatchingLabelInResult = new ArrayList<>();
464-
465466
// Retrieve all relationships from the result's list(s)
466-
StreamSupport.stream(allValues.values().spliterator(), false)
467-
.filter(MappingSupport.isListContainingOnly(listType, this.relationshipType))
468-
.flatMap(entry -> MappingSupport.extractRelationships(listType, entry).stream())
469-
.filter(r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic())
470-
.forEach(allMatchingTypeRelationshipsInResult::add);
471-
467+
Collection<Relationship> allMatchingTypeRelationshipsInResult = extractMatchingRelationships(allValues, relationshipDescription, typeOfRelationship);
472468
// Retrieve all nodes from the result's list(s)
473-
StreamSupport.stream(allValues.values().spliterator(), false)
474-
.filter(MappingSupport.isListContainingOnly(listType, this.nodeType))
475-
.flatMap(entry -> MappingSupport.extractNodes(listType, entry).stream())
476-
.filter(n -> n.hasLabel(targetLabel)).collect(Collectors.toList())
477-
.forEach(allNodesWithMatchingLabelInResult::add);
469+
Collection<Node> allNodesWithMatchingLabelInResult = extractMatchingNodes(allValues, targetLabel);
478470

479471
Function<Relationship, Long> targetIdSelector = relationshipDescription.isIncoming() ? Relationship::startNodeId : Relationship::endNodeId;
480472
Function<Relationship, Long> sourceIdSelector = relationshipDescription.isIncoming() ? Relationship::endNodeId : Relationship::startNodeId;
@@ -546,6 +538,48 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
546538
}
547539
}
548540

541+
private Collection<Node> extractMatchingNodes(MapAccessor allValues, String targetLabel) {
542+
543+
Collection<Node> allNodesWithMatchingLabelInResult = new ArrayList<>();
544+
Predicate<Node> onlyWithMatchingLabels = n -> n.hasLabel(targetLabel);
545+
StreamSupport.stream(allValues.values().spliterator(), false)
546+
.filter(MappingSupport.isListContainingOnly(listType, this.nodeType))
547+
.flatMap(entry -> MappingSupport.extractNodes(listType, entry).stream())
548+
.filter(onlyWithMatchingLabels)
549+
.forEach(allNodesWithMatchingLabelInResult::add);
550+
551+
if (allNodesWithMatchingLabelInResult.isEmpty()) {
552+
StreamSupport.stream(allValues.values().spliterator(), false)
553+
.filter(this.nodeType::isTypeOf)
554+
.map(Value::asNode)
555+
.filter(onlyWithMatchingLabels)
556+
.forEach(allNodesWithMatchingLabelInResult::add);
557+
}
558+
559+
return allNodesWithMatchingLabelInResult;
560+
}
561+
562+
private Collection<Relationship> extractMatchingRelationships(MapAccessor allValues, RelationshipDescription relationshipDescription, String typeOfRelationship) {
563+
564+
Collection<Relationship> allMatchingTypeRelationshipsInResult = new ArrayList<>();
565+
Predicate<Relationship> onlyWithMatchingType = r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic();
566+
StreamSupport.stream(allValues.values().spliterator(), false)
567+
.filter(MappingSupport.isListContainingOnly(listType, this.relationshipType))
568+
.flatMap(entry -> MappingSupport.extractRelationships(listType, entry).stream())
569+
.filter(onlyWithMatchingType)
570+
.forEach(allMatchingTypeRelationshipsInResult::add);
571+
572+
if (allMatchingTypeRelationshipsInResult.isEmpty()) {
573+
StreamSupport.stream(allValues.values().spliterator(), false)
574+
.filter(this.relationshipType::isTypeOf)
575+
.map(Value::asRelationship)
576+
.filter(onlyWithMatchingType)
577+
.forEach(allMatchingTypeRelationshipsInResult::add);
578+
}
579+
580+
return allMatchingTypeRelationshipsInResult;
581+
}
582+
549583
private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) {
550584
if (property.isInternalIdProperty()) {
551585
return propertyContainer instanceof Entity ? Values.value(((Entity) propertyContainer).id())

src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java

+74
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@
118118
import org.springframework.data.neo4j.integration.shared.common.KotlinPerson;
119119
import org.springframework.data.neo4j.integration.shared.common.LikesHobbyRelationship;
120120
import org.springframework.data.neo4j.integration.shared.common.MultipleLabels;
121+
import org.springframework.data.neo4j.integration.shared.common.OneToOneSource;
122+
import org.springframework.data.neo4j.integration.shared.common.OneToOneTarget;
121123
import org.springframework.data.neo4j.integration.shared.common.ParentNode;
122124
import org.springframework.data.neo4j.integration.shared.common.Person;
123125
import org.springframework.data.neo4j.integration.shared.common.PersonWithAllConstructor;
@@ -1229,6 +1231,63 @@ void cyclicDerivedFinderWithSort(@Autowired RelationshipRepository repository) {
12291231
assertThat(people).hasSize(1);
12301232
}
12311233

1234+
private void createOneToOneScenario() {
1235+
doWithSession(session -> {
1236+
try (Transaction tx = session.beginTransaction()) {
1237+
tx.run("CREATE (s:OneToOneSource {name: 's1'}) -[:OWNS]->(t:OneToOneTarget {name: 't1'})");
1238+
tx.run("CREATE (s:OneToOneSource {name: 's2'}) -[:OWNS]->(t:OneToOneTarget {name: 't2'})");
1239+
tx.commit();
1240+
}
1241+
return null;
1242+
}
1243+
);
1244+
}
1245+
1246+
private void assertOneToOneScenario(List<OneToOneSource> oneToOnes) {
1247+
assertThat(oneToOnes).hasSize(2);
1248+
assertThat(oneToOnes).extracting(OneToOneSource::getName).contains("s1", "s2");
1249+
assertThat(oneToOnes).extracting(s -> s.getTarget().getName()).contains("t1", "t2");
1250+
}
1251+
1252+
@Test // GH-2269
1253+
void shouldFindOneToOneWithDefault(@Autowired OneToOneRepository repository) {
1254+
createOneToOneScenario();
1255+
1256+
List<OneToOneSource> oneToOnes = repository.findAll();
1257+
assertOneToOneScenario(oneToOnes);
1258+
}
1259+
1260+
@Test // GH-2269
1261+
void shouldFindOneToOneWithCollect(@Autowired OneToOneRepository repository) {
1262+
createOneToOneScenario();
1263+
1264+
List<OneToOneSource> oneToOnes = repository.findAllWithCustomQuery();
1265+
assertOneToOneScenario(oneToOnes);
1266+
}
1267+
1268+
@Test // GH-2269
1269+
void shouldFindOneToOneWithoutCollect(@Autowired OneToOneRepository repository) {
1270+
createOneToOneScenario();
1271+
1272+
List<OneToOneSource> oneToOnes = repository.findAllWithCustomQueryNoCollect();
1273+
assertOneToOneScenario(oneToOnes);
1274+
}
1275+
1276+
@Test // GH-2269
1277+
void shouldFindOne(@Autowired OneToOneRepository repository) {
1278+
createOneToOneScenario();
1279+
1280+
Optional<OneToOneSource> optionalSource = repository.findOneByName("s1");
1281+
assertThat(optionalSource).hasValueSatisfying(s -> assertThat(s).extracting(OneToOneSource::getTarget).extracting(OneToOneTarget::getName).isEqualTo("t1"));
1282+
}
1283+
1284+
@Test // GH-2269
1285+
void shouldFindOneToOneWithWildcardReturn(@Autowired OneToOneRepository repository) {
1286+
createOneToOneScenario();
1287+
1288+
List<OneToOneSource> oneToOnes = repository.findAllWithCustomQueryReturnStar();
1289+
assertOneToOneScenario(oneToOnes);
1290+
}
12321291
}
12331292

12341293
@Nested
@@ -3990,6 +4049,21 @@ interface PetRepository extends Neo4jRepository<Pet, Long> {
39904049
boolean existsByName(String name);
39914050
}
39924051

4052+
interface OneToOneRepository extends Neo4jRepository<OneToOneSource, String> {
4053+
4054+
@Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return p1, collect(r), collect(p2)")
4055+
List<OneToOneSource> findAllWithCustomQuery();
4056+
4057+
@Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return p1, r, p2")
4058+
List<OneToOneSource> findAllWithCustomQueryNoCollect();
4059+
4060+
@Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) WHERE p1.name = $0 return p1, r, p2")
4061+
Optional<OneToOneSource> findOneByName(String name);
4062+
4063+
@Query("MATCH (p1:#{#staticLabels})-[r:OWNS]-(p2) return *")
4064+
List<OneToOneSource> findAllWithCustomQueryReturnStar();
4065+
}
4066+
39934067
interface RelationshipRepository extends Neo4jRepository<PersonWithRelationship, Long> {
39944068

39954069
@Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) "
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2011-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.shared.common;
17+
18+
import lombok.Data;
19+
20+
import org.springframework.data.neo4j.core.schema.Id;
21+
import org.springframework.data.neo4j.core.schema.Node;
22+
import org.springframework.data.neo4j.core.schema.Relationship;
23+
24+
/**
25+
* For usage in testing various one-to-one mappings
26+
*
27+
* @author Michael J. Simons
28+
*/
29+
@Node
30+
@Data
31+
public class OneToOneSource {
32+
33+
@Id
34+
private String name;
35+
36+
@Relationship("OWNS")
37+
private OneToOneTarget target;
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2011-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.integration.shared.common;
17+
18+
import lombok.Data;
19+
20+
import org.springframework.data.neo4j.core.schema.Id;
21+
import org.springframework.data.neo4j.core.schema.Node;
22+
23+
/**
24+
* For usage in testing various one-to-one mappings
25+
*
26+
* @author Michael J. Simons
27+
*/
28+
@Node
29+
@Data
30+
public class OneToOneTarget {
31+
32+
@Id
33+
private String name;
34+
}

0 commit comments

Comments
 (0)