Skip to content

Commit e783dcf

Browse files
michael-simonsmeistermeier
authored andcommitted
DATAGRAPH-1452 - Support known entities as parameter for repository methods.
Introduce a `Neo4jNestedMapEntityWriter`, responsible for turning mapped entities into nested maps of Neo4j values. The `Neo4jNestedMapEntityWriter` is called from the repository infrasctructure, not from the templates itself to be consistent in the way that the templates don’t do automatic conversions of parameters.
1 parent 924b4a0 commit e783dcf

File tree

12 files changed

+1201
-19
lines changed

12 files changed

+1201
-19
lines changed

src/main/asciidoc/faq/faq.adoc

+170
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,176 @@ The Spring Boot Maven and Gradle plugins do this automatically for you.
171171
If this is not feasible for any reason, you can either add
172172
`@Param` and specify the name explicitly or use the parameters index.
173173

174+
Mapped entities (everything with a `@Node`) passed as parameter to a function that is annotated with
175+
a custom query will be turned into a nested map.
176+
The following example represents the structure as Neo4j parameters.
177+
178+
Given are a `Movie`, `Person` and `Actor` classes annotated as shown in <<movie-model, the movie model>>:
179+
180+
[[movie-model]]
181+
[source,java]
182+
."Standard" movies model
183+
----
184+
@Node
185+
public final class Movie {
186+
187+
@Id
188+
private final String title;
189+
190+
@Property("tagline")
191+
private final String description;
192+
193+
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
194+
private final List<Actor> actors;
195+
196+
@Relationship(value = "DIRECTED", direction = Direction.INCOMING)
197+
private final List<Person> directors;
198+
}
199+
200+
@Node
201+
public final class Person {
202+
203+
@Id @GeneratedValue
204+
private final Long id;
205+
206+
private final String name;
207+
208+
private Integer born;
209+
210+
@Relationship("REVIEWED")
211+
private List<Movie> reviewed = new ArrayList<>();
212+
}
213+
214+
@RelationshipProperties
215+
public final class Actor {
216+
217+
@TargetNode
218+
private final Person person;
219+
220+
private final List<String> roles;
221+
}
222+
223+
interface MovieRepository extends Neo4jRepository<Movie, String> {
224+
225+
@Query("MATCH (m:Movie {title: $movie.__id__})\n"
226+
+ "MATCH (m) <- [r:DIRECTED|REVIEWED|ACTED_IN] - (p:Person)\n"
227+
+ "return m, collect(r), collect(p)")
228+
Movie findByMovie(@Param("movie") Movie movie);
229+
}
230+
----
231+
232+
Passing an instance of `Movie` to the repository method above, will generate the following Neo4j map parameter:
233+
234+
[source,json]
235+
----
236+
{
237+
"movie": {
238+
"__labels__": [
239+
"Movie"
240+
],
241+
"__id__": "The Da Vinci Code",
242+
"__properties__": {
243+
"ACTED_IN": [
244+
{
245+
"__properties__": {
246+
"roles": [
247+
"Sophie Neveu"
248+
]
249+
},
250+
"__target__": {
251+
"__labels__": [
252+
"Person"
253+
],
254+
"__id__": 402,
255+
"__properties__": {
256+
"name": "Audrey Tautou",
257+
"born": 1976
258+
}
259+
}
260+
},
261+
{
262+
"__properties__": {
263+
"roles": [
264+
"Sir Leight Teabing"
265+
]
266+
},
267+
"__target__": {
268+
"__labels__": [
269+
"Person"
270+
],
271+
"__id__": 401,
272+
"__properties__": {
273+
"name": "Ian McKellen",
274+
"born": 1939
275+
}
276+
}
277+
},
278+
{
279+
"__properties__": {
280+
"roles": [
281+
"Dr. Robert Langdon"
282+
]
283+
},
284+
"__target__": {
285+
"__labels__": [
286+
"Person"
287+
],
288+
"__id__": 360,
289+
"__properties__": {
290+
"name": "Tom Hanks",
291+
"born": 1956
292+
}
293+
}
294+
},
295+
{
296+
"__properties__": {
297+
"roles": [
298+
"Silas"
299+
]
300+
},
301+
"__target__": {
302+
"__labels__": [
303+
"Person"
304+
],
305+
"__id__": 403,
306+
"__properties__": {
307+
"name": "Paul Bettany",
308+
"born": 1971
309+
}
310+
}
311+
}
312+
],
313+
"DIRECTED": [
314+
{
315+
"__labels__": [
316+
"Person"
317+
],
318+
"__id__": 404,
319+
"__properties__": {
320+
"name": "Ron Howard",
321+
"born": 1954
322+
}
323+
}
324+
],
325+
"tagline": "Break The Codes",
326+
"released": 2006
327+
}
328+
}
329+
}
330+
----
331+
332+
A node is represented by a map. The map will always contain `__id__` which is the mapped id property.
333+
Under `__labels__` all labels, static and dynamic, will be available.
334+
All properties - and type of relationships - appear in those maps as they would appear in the graph when the entity would
335+
have been written by SDN.
336+
Values will have the correct Cypher type and won't need further conversion.
337+
338+
All relationships are lists of maps. Dynamic relationships will be resolved accordingly.
339+
If an entity has a relationship with the same type to different types of others nodes, they will all appear in the same list.
340+
If you need such a mapping and also have the need to work with those custom parameters, you have to unroll it accordingly.
341+
One way to do this are correlated subqueries (Neo4j 4.1+ required).
342+
343+
174344
[[faq.custom-queries]]
175345
== How do I use custom queries with repository methods returning `Page<T>` or `Slice<T>`?
176346

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

+11
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,22 @@ public final class Constants {
3434
public static final SymbolicName NAME_OF_ROOT_NODE = Cypher.name("n");
3535

3636
public static final String NAME_OF_INTERNAL_ID = "__internalNeo4jId__";
37+
/**
38+
* Indicates the list of dynamic labels.
39+
*/
3740
public static final String NAME_OF_LABELS = "__nodeLabels__";
41+
/**
42+
* Indicates the list of all labels.
43+
*/
44+
public static final String NAME_OF_ALL_LABELS = "__labels__";
3845
public static final String NAME_OF_IDS = "__ids__";
3946
public static final String NAME_OF_ID = "__id__";
4047
public static final String NAME_OF_VERSION_PARAM = "__version__";
4148
public static final String NAME_OF_PROPERTIES_PARAM = "__properties__";
49+
/**
50+
* Indicates the parameter that contains the static labels which are required to correctly compute the difference
51+
* in the list of dynamic labels when saving a node.
52+
*/
4253
public static final String NAME_OF_STATIC_LABELS_PARAM = "__staticLabels__";
4354
public static final String NAME_OF_ENTITY_LIST_PARAM = "__entities__";
4455
public static final String NAME_OF_PATHS = "__paths__";

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

+10-8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apiguardian.api.API;
2626
import org.neo4j.driver.Value;
2727
import org.neo4j.driver.types.Type;
28+
import org.springframework.lang.Nullable;
2829

2930
/**
3031
* @author Michael J. Simons
@@ -43,15 +44,16 @@ public final class MappingSupport {
4344
* @return A unified collection (Either a collection of Map.Entry for dynamic and relationships with properties or a
4445
* list of related values)
4546
*/
46-
public static Collection<?> unifyRelationshipValue(Neo4jPersistentProperty property, Object rawValue) {
47+
public static @Nullable Collection<?> unifyRelationshipValue(Neo4jPersistentProperty property, Object rawValue) {
4748
Collection<?> unifiedValue;
4849
if (property.isDynamicAssociation()) {
4950
if (property.isDynamicOneToManyAssociation()) {
50-
unifiedValue = ((Map<String, Collection<?>>) rawValue).entrySet().stream()
51-
.flatMap(e -> e.getValue().stream().map(v -> new SimpleEntry(e.getKey(), v))).collect(
52-
Collectors.toList());
51+
unifiedValue = ((Map<?, Collection<?>>) rawValue)
52+
.entrySet().stream()
53+
.flatMap(e -> e.getValue().stream().map(v -> new SimpleEntry(e.getKey(), v)))
54+
.collect(Collectors.toList());
5355
} else {
54-
unifiedValue = ((Map<String, Object>) rawValue).entrySet();
56+
unifiedValue = ((Map<?, Object>) rawValue).entrySet();
5557
}
5658
} else if (property.isRelationshipWithProperties()) {
5759
unifiedValue = (Collection<Object>) rawValue;
@@ -92,7 +94,7 @@ private MappingSupport() {}
9294
* Class that defines a tuple of relationship with properties and the connected target entity.
9395
*/
9496
@API(status = API.Status.INTERNAL)
95-
final static class RelationshipPropertiesWithEntityHolder {
97+
public final static class RelationshipPropertiesWithEntityHolder {
9698
private final Object relationshipProperties;
9799
private final Object relatedEntity;
98100

@@ -101,11 +103,11 @@ final static class RelationshipPropertiesWithEntityHolder {
101103
this.relatedEntity = relatedEntity;
102104
}
103105

104-
Object getRelationshipProperties() {
106+
public Object getRelationshipProperties() {
105107
return relationshipProperties;
106108
}
107109

108-
Object getRelatedEntity() {
110+
public Object getRelatedEntity() {
109111
return relatedEntity;
110112
}
111113
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ boolean hasCustomWriteTarget(Class<?> targetType) {
148148
return conversionService.hasCustomWriteTarget(targetType);
149149
}
150150

151-
152151
/*
153152
* (non-Javadoc)
154153
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation)
@@ -339,7 +338,7 @@ public CreateRelationshipStatementHolder createStatement(Neo4jPersistentEntity<?
339338
String dynamicRelationshipType = null;
340339
if (relationshipContext.getRelationship().isDynamic()) {
341340
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation().getRequiredComponentType();
342-
Object key = ((Map.Entry<String, ?>) relatedValue).getKey();
341+
Object key = ((Map.Entry) relatedValue).getKey();
343342
dynamicRelationshipType = conversionService.writeValue(key, keyType, relationshipContext.getInverse().getOptionalWritingConverter()).asString();
344343
}
345344
return createStatementForRelationShipWithProperties(

0 commit comments

Comments
 (0)