Skip to content

Commit c1422f4

Browse files
committed
GH-2032 - Document manual queries.
1 parent 4183ed0 commit c1422f4

File tree

3 files changed

+304
-258
lines changed

3 files changed

+304
-258
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
[[custom-queries]]
2+
= Custom queries
3+
4+
Spring Data Neo4j, like all the other Spring Data modules, allows you to specify custom queries in you repositories.
5+
Those come in handy if you cannot express the finder logic via derived query functions.
6+
7+
Because Spring Data Neo4j works heavily record-oriented under the hood, it is important to keep this in mind and not build up a result set with multiple records for the same "root node".
8+
9+
[[custom-queries.for-relationships]]
10+
== Queries with relationships
11+
12+
[[custom-queries.for-relationships.cartesian-product]]
13+
=== Beware of the cartesian product
14+
15+
Assuming you have a query like `MATCH (m:Movie{title: 'The Matrix'})<-[r:ACTED_IN]-(p:Person) return m,r,p` that results into something like this:
16+
17+
.Multiple records (shortened)
18+
----
19+
+------------------------------------------------------------------------------------------+
20+
| m | r | p |
21+
+------------------------------------------------------------------------------------------+
22+
| (:Movie) | [:ACTED_IN {roles: ["Emil"]}] | (:Person {name: "Emil Eifrem"}) |
23+
| (:Movie) | [:ACTED_IN {roles: ["Agent Smith"]}] | (:Person {name: "Hugo Weaving}) |
24+
| (:Movie) | [:ACTED_IN {roles: ["Morpheus"]}] | (:Person {name: "Laurence Fishburne"}) |
25+
| (:Movie) | [:ACTED_IN {roles: ["Trinity"]}] | (:Person {name: "Carrie-Anne Moss"}) |
26+
| (:Movie) | [:ACTED_IN {roles: ["Neo"]}] | (:Person {name: "Keanu Reeves"}) |
27+
+------------------------------------------------------------------------------------------+
28+
----
29+
30+
The result from the mapping would be most likely unusable.
31+
If this would get mapped into a list, it will contain duplicates for the `Movie` but this movie will only have one relationship.
32+
33+
[[custom-queries.for-relationships.one.record]]
34+
=== Getting one record per root node
35+
36+
To get the right object(s) back, it is required to _collect_ the relationships and related nodes in the query: `MATCH (m:Movie{title: 'The Matrix'})<-[r:ACTED_IN]-(p:Person) return m,collect(r),collect(p)`
37+
38+
.Single record (shortended)
39+
----
40+
+------------------------------------------------------------------------+
41+
| m | collect(r) | collect(p) |
42+
+------------------------------------------------------------------------+
43+
| (:Movie) | [[:ACTED_IN], [:ACTED_IN], ...]| [(:Person), (:Person),...] |
44+
+------------------------------------------------------------------------+
45+
----
46+
47+
With this result as a single record it is possible for Spring Data Neo4j to add all related nodes correctly to the root node.
48+
49+
[[custom-queries.parameters]]
50+
== Parameters in custom queries
51+
52+
You do this exactly the same way as in a standard Cypher query issued in the Neo4j Browser or the Cypher-Shell, with the `$` syntax (from Neo4j 4.0 on upwards, the old `{foo}` syntax for Cypher parameters has been removed from the database);
53+
54+
[source,java,indent=0]
55+
.ARepository.java
56+
----
57+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java[tags=standard-parameter]
58+
----
59+
<.> Here we are referring to the parameter by its name.
60+
You can also use `$0` etc. instead.
61+
62+
NOTE: You need to compile your Java 8+ project with `-parameters` to make named parameters work without further annotations.
63+
The Spring Boot Maven and Gradle plugins do this automatically for you.
64+
If this is not feasible for any reason, you can either add
65+
`@Param` and specify the name explicitly or use the parameters index.
66+
67+
Mapped entities (everything with a `@Node`) passed as parameter to a function that is annotated with
68+
a custom query will be turned into a nested map.
69+
The following example represents the structure as Neo4j parameters.
70+
71+
Given are a `Movie`, `Person` and `Actor` classes annotated as shown in <<movie-model, the movie model>>:
72+
73+
[[movie-model]]
74+
[source,java]
75+
."Standard" movies model
76+
----
77+
@Node
78+
public final class Movie {
79+
80+
@Id
81+
private final String title;
82+
83+
@Property("tagline")
84+
private final String description;
85+
86+
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
87+
private final List<Actor> actors;
88+
89+
@Relationship(value = "DIRECTED", direction = Direction.INCOMING)
90+
private final List<Person> directors;
91+
}
92+
93+
@Node
94+
public final class Person {
95+
96+
@Id @GeneratedValue
97+
private final Long id;
98+
99+
private final String name;
100+
101+
private Integer born;
102+
103+
@Relationship("REVIEWED")
104+
private List<Movie> reviewed = new ArrayList<>();
105+
}
106+
107+
@RelationshipProperties
108+
public final class Actor {
109+
110+
@TargetNode
111+
private final Person person;
112+
113+
private final List<String> roles;
114+
}
115+
116+
interface MovieRepository extends Neo4jRepository<Movie, String> {
117+
118+
@Query("MATCH (m:Movie {title: $movie.__id__})\n"
119+
+ "MATCH (m) <- [r:DIRECTED|REVIEWED|ACTED_IN] - (p:Person)\n"
120+
+ "return m, collect(r), collect(p)")
121+
Movie findByMovie(@Param("movie") Movie movie);
122+
}
123+
----
124+
125+
Passing an instance of `Movie` to the repository method above, will generate the following Neo4j map parameter:
126+
127+
[source,json]
128+
----
129+
{
130+
"movie": {
131+
"__labels__": [
132+
"Movie"
133+
],
134+
"__id__": "The Da Vinci Code",
135+
"__properties__": {
136+
"ACTED_IN": [
137+
{
138+
"__properties__": {
139+
"roles": [
140+
"Sophie Neveu"
141+
]
142+
},
143+
"__target__": {
144+
"__labels__": [
145+
"Person"
146+
],
147+
"__id__": 402,
148+
"__properties__": {
149+
"name": "Audrey Tautou",
150+
"born": 1976
151+
}
152+
}
153+
},
154+
{
155+
"__properties__": {
156+
"roles": [
157+
"Sir Leight Teabing"
158+
]
159+
},
160+
"__target__": {
161+
"__labels__": [
162+
"Person"
163+
],
164+
"__id__": 401,
165+
"__properties__": {
166+
"name": "Ian McKellen",
167+
"born": 1939
168+
}
169+
}
170+
},
171+
{
172+
"__properties__": {
173+
"roles": [
174+
"Dr. Robert Langdon"
175+
]
176+
},
177+
"__target__": {
178+
"__labels__": [
179+
"Person"
180+
],
181+
"__id__": 360,
182+
"__properties__": {
183+
"name": "Tom Hanks",
184+
"born": 1956
185+
}
186+
}
187+
},
188+
{
189+
"__properties__": {
190+
"roles": [
191+
"Silas"
192+
]
193+
},
194+
"__target__": {
195+
"__labels__": [
196+
"Person"
197+
],
198+
"__id__": 403,
199+
"__properties__": {
200+
"name": "Paul Bettany",
201+
"born": 1971
202+
}
203+
}
204+
}
205+
],
206+
"DIRECTED": [
207+
{
208+
"__labels__": [
209+
"Person"
210+
],
211+
"__id__": 404,
212+
"__properties__": {
213+
"name": "Ron Howard",
214+
"born": 1954
215+
}
216+
}
217+
],
218+
"tagline": "Break The Codes",
219+
"released": 2006
220+
}
221+
}
222+
}
223+
----
224+
225+
A node is represented by a map. The map will always contain `__id__` which is the mapped id property.
226+
Under `__labels__` all labels, static and dynamic, will be available.
227+
All properties - and type of relationships - appear in those maps as they would appear in the graph when the entity would
228+
have been written by SDN.
229+
Values will have the correct Cypher type and won't need further conversion.
230+
231+
All relationships are lists of maps. Dynamic relationships will be resolved accordingly.
232+
If an entity has a relationship with the same type to different types of others nodes, they will all appear in the same list.
233+
If you need such a mapping and also have the need to work with those custom parameters, you have to unroll it accordingly.
234+
One way to do this are correlated subqueries (Neo4j 4.1+ required).
235+
236+
[[custom-queries.spel]]
237+
== Spring Expression Language in custom queries
238+
239+
{spring-framework-ref}/core.html#expressions[Spring Expression Language (SpEL)] can be used in custom queries inside `:#{}`.
240+
This is the standard Spring Data way of defining a block of text inside a query that undergoes SpEL evaluation.
241+
242+
The following example basically defines the same query as above, but uses a `WHERE` clause to avoid even more curly braces:
243+
244+
[source,java,indent=0]
245+
[[custom-queries-with-spel]]
246+
.ARepository.java
247+
----
248+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java[tags=spel]
249+
----
250+
251+
The SpEL blocked starts with `:#{` and than refers to the given `String` parameters by name (`#pt1`).
252+
Don't confuse this with the above Cypher syntax!
253+
The SpEL expression concatenates both parameters into one single value that is eventually passed on to the <<neo4j-client>>.
254+
The SpEL block ends with `}`.
255+
256+
SpEL also solves two additional problems. We provide two extensions that allow to pass in a `Sort` object into custom queries.
257+
Remember <<custom-queries-with-page-and-slice-examples>> from <<faq.custom-queries-with-page-and-slice,custom queries>>?
258+
With the `orderBy` extension you can pass in a `Pageable` with a dynamic sort to a custom query:
259+
260+
[[custom-queries.spel.source]]
261+
[source,java]
262+
.orderBy-Extension
263+
----
264+
import org.springframework.data.domain.Pageable;
265+
import org.springframework.data.domain.Sort;
266+
import org.springframework.data.neo4j.repository.Neo4jRepository;
267+
import org.springframework.data.neo4j.repository.query.Query;
268+
269+
public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
270+
271+
@Query(""
272+
+ "MATCH (n:Person) WHERE n.name = $name RETURN n "
273+
+ ":#{orderBy(#pageable)} SKIP $skip LIMIT $limit" // <.>
274+
)
275+
Slice<Person> findSliceByName(String name, Pageable pageable);
276+
277+
@Query(""
278+
+ "MATCH (n:Person) WHERE n.name = $name RETURN n :#{orderBy(#sort)}" // <.>
279+
)
280+
List<Person> findAllByName(String name, Sort sort);
281+
}
282+
----
283+
<.> A `Pageable` has always the name `pageable` inside the SpEL context.
284+
<.> A `Sort` has always the name `sort` inside the SpEL context.
285+
286+
The `literal` extension can be used to make things like labels or relationship-types "dynamic" in custom queries.
287+
Neither labels nor relationship types can be parameterized in Cypher, so they must be given literal.
288+
289+
[source,java]
290+
.literal-Extension
291+
----
292+
interface BaseClassRepository extends Neo4jRepository<BaseClass, Long> {
293+
294+
@Query("MATCH (n:`:#{literal(#label)}`) RETURN n") // <.>
295+
List<Inheritance.BaseClass> findByLabel(String label);
296+
}
297+
----
298+
<.> The `literal` extension will be replaced with the literal value of the evaluate parameter.
299+
Here it has been used to match dynamically on a Label.
300+
If you pass in `SomeLabel` as a parameter to the method, `MATCH (n:``SomeLabel``) RETURN n`
301+
will be generated. Ticks have been added to correctly escape values. SDN won't do this
302+
for you as this is probably not what you want in all cases.

src/main/asciidoc/appendix/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
include::conversions.adoc[]
99
include::neo4j-client.adoc[]
1010
include::query-creation.adoc[]
11+
include::custom-queries.adoc[]
1112
include::migrating.adoc[]
1213
include::build.adoc[]
1314
:leveloffset: -1

0 commit comments

Comments
 (0)