|
| 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. |
0 commit comments