Skip to content

Commit 3bbff33

Browse files
GH-2323 - Support entities annotated with @RelationshipProperties as top level parameters to custom procedures.
This change makes the `NestedMapEntityWriter` extract the `@Target` from a `@RelationshipProperty`when an entity annotated with this is passed in top level. Closes #2323.
1 parent 14b8195 commit 3bbff33

File tree

7 files changed

+345
-77
lines changed

7 files changed

+345
-77
lines changed

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jNestedMapEntityWriter.java

+17-5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.data.mapping.AssociationHandler;
3737
import org.springframework.data.mapping.MappingException;
3838
import org.springframework.data.mapping.PersistentPropertyAccessor;
39+
import org.springframework.data.mapping.PropertyHandler;
3940
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
4041
import org.springframework.data.neo4j.core.mapping.Constants;
4142
import org.springframework.data.neo4j.core.mapping.MappingSupport.RelationshipPropertiesWithEntityHolder;
@@ -45,6 +46,7 @@
4546
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
4647
import org.springframework.data.neo4j.core.mapping.NestedRelationshipContext;
4748
import org.springframework.data.neo4j.core.mapping.RelationshipDescription;
49+
import org.springframework.data.neo4j.core.schema.TargetNode;
4850
import org.springframework.data.util.TypeInformation;
4951

5052
/**
@@ -80,10 +82,10 @@ public void write(Object source, Map<String, Object> sink) {
8082
}
8183

8284
Set<Object> seenObjects = new HashSet<>();
83-
writeImpl(source, sink, seenObjects);
85+
writeImpl(source, sink, seenObjects, true);
8486
}
8587

86-
Map<String, Object> writeImpl(Object source, Map<String, Object> sink, Set<Object> seenObjects) {
88+
Map<String, Object> writeImpl(Object source, Map<String, Object> sink, Set<Object> seenObjects, boolean initialObject) {
8789

8890
Class<?> sourceType = source.getClass();
8991
if (!this.mappingContext.hasPersistentEntityFor(sourceType)) {
@@ -111,6 +113,16 @@ Map<String, Object> writeImpl(Object source, Map<String, Object> sink, Set<Objec
111113

112114
addLabels(sink, entity, propertyAccessor);
113115
addRelations(sink, entity, propertyAccessor, seenObjects);
116+
if (initialObject && entity.isRelationshipPropertiesEntity()) {
117+
Map<String, Object> propertyMap = (Map<String, Object>) sink.get(Constants.NAME_OF_PROPERTIES_PARAM);
118+
entity.doWithProperties((PropertyHandler<Neo4jPersistentProperty>) p -> {
119+
if (p.isAnnotationPresent(TargetNode.class)) {
120+
Map<String, Object> target = this.writeImpl(propertyAccessor.getProperty(p), new HashMap<>(), seenObjects, false);
121+
propertyMap.put("__target__", Values.value(target));
122+
return;
123+
}
124+
});
125+
}
114126

115127
// Remove redundant values
116128
// Internal ID
@@ -225,17 +237,17 @@ private Map<String, Object> extractPotentialRelationProperties(
225237
) {
226238

227239
if (!description.hasRelationshipProperties()) {
228-
return this.writeImpl(relatedObject, new HashMap<>(), seenObjects);
240+
return this.writeImpl(relatedObject, new HashMap<>(), seenObjects, false);
229241
}
230242

231243
RelationshipPropertiesWithEntityHolder tuple = (RelationshipPropertiesWithEntityHolder) relatedObject;
232244
Map<String, Object> relatedObjectProperties;
233245
relatedObjectProperties = this
234246
.writeImpl(tuple.getRelationshipProperties(), new HashMap<>(),
235-
seenObjects);
247+
seenObjects, false);
236248
relatedObjectProperties.put("__target__",
237249
this.writeImpl(tuple.getRelatedEntity(), new HashMap<>(),
238-
seenObjects));
250+
seenObjects, false));
239251
return relatedObjectProperties;
240252
}
241253
}

src/test/java/org/springframework/data/neo4j/integration/issues/gh2289/SkuRO.java~Stashed changes

-71
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.issues.gh2323;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
24+
import org.junit.jupiter.api.BeforeAll;
25+
import org.junit.jupiter.api.Test;
26+
import org.neo4j.driver.Driver;
27+
import org.neo4j.driver.Session;
28+
import org.neo4j.driver.Transaction;
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.ComponentScan;
32+
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
34+
import org.springframework.data.neo4j.repository.Neo4jRepository;
35+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
36+
import org.springframework.data.neo4j.repository.query.Query;
37+
import org.springframework.data.neo4j.test.BookmarkCapture;
38+
import org.springframework.data.neo4j.test.Neo4jExtension;
39+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
40+
import org.springframework.data.repository.query.Param;
41+
import org.springframework.stereotype.Repository;
42+
import org.springframework.stereotype.Service;
43+
import org.springframework.transaction.annotation.EnableTransactionManagement;
44+
45+
/**
46+
* @author Michael J. Simons
47+
*/
48+
@Neo4jIntegrationTest
49+
class GH2323IT {
50+
51+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
52+
53+
protected static String personId;
54+
55+
@BeforeAll
56+
protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) {
57+
try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig());
58+
Transaction transaction = session.beginTransaction();
59+
) {
60+
transaction.run("MATCH (n) detach delete n");
61+
personId = transaction.run("CREATE (n:Person {id: randomUUID(), name: 'Helge'}) return n.id").single()
62+
.get(0)
63+
.asString();
64+
transaction.run("unwind ['German', 'English'] as name create (n:Language {name: name}) return name")
65+
.consume();
66+
transaction.commit();
67+
bookmarkCapture.seedWith(session.lastBookmark());
68+
}
69+
}
70+
71+
@Test // GH-2323
72+
void listOfRelationshipPropertiesShouldBeUnwindable(@Autowired PersonService personService) {
73+
Person person = personService.updateRel(personId, Arrays.asList("German"));
74+
assertThat(person).isNotNull();
75+
assertThat(person.getKnownLanguages()).hasSize(1);
76+
assertThat(person.getKnownLanguages()).first().satisfies(knows -> {
77+
assertThat(knows.getDescription()).isEqualTo("Some description");
78+
assertThat(knows.getLanguage()).extracting(Language::getName).isEqualTo("German");
79+
});
80+
}
81+
82+
@Repository
83+
public interface PersonRepository extends Neo4jRepository<Person, String> {
84+
85+
@Query("UNWIND $relations As rel WITH rel " +
86+
"CREATE (f:Person {id: $from}) - [r:KNOWS {description: rel.__properties__.description}] -> (t:Language {name: rel.__properties__.__target__.__id__}) "
87+
+
88+
"RETURN f, collect(r), collect(t)")
89+
Person updateRel(@Param("from") String from, @Param("relations") List<Knows> relations);
90+
}
91+
92+
@Service
93+
static class PersonService {
94+
95+
private final PersonRepository personRepository;
96+
97+
PersonService(PersonRepository personRepository) {
98+
this.personRepository = personRepository;
99+
}
100+
101+
public Person updateRel(String from, List<String> languageNames) {
102+
103+
List<Knows> knownLanguages = languageNames.stream().map(Language::new)
104+
.map(language -> new Knows("Some description", language))
105+
.collect(Collectors.toList());
106+
return personRepository.updateRel(from, knownLanguages);
107+
}
108+
}
109+
110+
@Configuration
111+
@EnableTransactionManagement
112+
@EnableNeo4jRepositories(considerNestedRepositories = true)
113+
@ComponentScan
114+
static class Config extends AbstractNeo4jConfig {
115+
116+
@Bean
117+
public BookmarkCapture bookmarkCapture() {
118+
return new BookmarkCapture();
119+
}
120+
121+
@Bean
122+
public Driver driver() {
123+
124+
return neo4jConnectionSupport.getDriver();
125+
}
126+
}
127+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.issues.gh2323;
17+
18+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
19+
import org.springframework.data.neo4j.core.schema.Id;
20+
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
21+
import org.springframework.data.neo4j.core.schema.TargetNode;
22+
23+
/**
24+
* @author Michael J. Simons
25+
*/
26+
@RelationshipProperties
27+
public class Knows {
28+
29+
@Id @GeneratedValue
30+
private Long id;
31+
32+
private final String description;
33+
34+
@TargetNode
35+
private final Language language;
36+
37+
public Knows(String description, Language language) {
38+
this.description = description;
39+
this.language = language;
40+
}
41+
42+
public Long getId() {
43+
return id;
44+
}
45+
46+
public String getDescription() {
47+
return description;
48+
}
49+
50+
public Language getLanguage() {
51+
return language;
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.issues.gh2323;
17+
18+
import org.springframework.data.neo4j.core.schema.Id;
19+
import org.springframework.data.neo4j.core.schema.Node;
20+
21+
/**
22+
* @author Michael J. Simons
23+
*/
24+
@Node
25+
public class Language {
26+
27+
@Id
28+
private final String name;
29+
30+
public Language(String name) {
31+
this.name = name;
32+
}
33+
34+
public String getName() {
35+
return name;
36+
}
37+
}

0 commit comments

Comments
 (0)