Skip to content

Commit 0da03cc

Browse files
GH-2135 - Add documentation for custom Graph repository fragments.
This closes #2135.
1 parent 3ef2065 commit 0da03cc

File tree

12 files changed

+520
-75
lines changed

12 files changed

+520
-75
lines changed

src/main/asciidoc/appendix/custom-queries.adoc

+3-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ With this result as a single record it is possible for Spring Data Neo4j to add
4949
[[custom-queries.parameters]]
5050
== Parameters in custom queries
5151

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);
52+
You do this exactly the same way as in a standard Cypher query issued in the Neo4j Browser or the Cypher-Shell,
53+
with the `$` syntax (from Neo4j 4.0 on upwards, the old `{foo}` syntax for Cypher parameters has been removed from the database).
5354

5455
[source,java,indent=0]
5556
.ARepository.java
@@ -73,7 +74,7 @@ This is the standard Spring Data way of defining a block of text inside a query
7374
The following example basically defines the same query as above, but uses a `WHERE` clause to avoid even more curly braces:
7475

7576
[source,java,indent=0]
76-
[[custom-queries-with-spel]]
77+
[[custom-queries-with-spel-parameter-example]]
7778
.ARepository.java
7879
----
7980
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java[tags=spel]

src/main/asciidoc/faq/faq.adoc

+180-2
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ This is our canonical movie example with the imperative template:
140140
[[imperative-template-example]]
141141
.TemplateExampleTest.java
142142
----
143-
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative]
143+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative-pt1]
144+
@DataNeo4jTest
145+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative-pt2]
144146
----
145147

146148
And here is the reactive version, omitting the setup for brevity:
@@ -149,9 +151,13 @@ And here is the reactive version, omitting the setup for brevity:
149151
[[reactive-template-example]]
150152
.ReactiveTemplateExampleTest.java
151153
----
152-
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive]
154+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive-pt1]
155+
@DataNeo4jTest
156+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive-pt2]
153157
----
154158

159+
Please note that both examples use `@DataNeo4jTest` from Spring Boot.
160+
155161
[[faq.custom-queries-with-page-and-slice]]
156162
== How do I use custom queries with repository methods returning `Page<T>` or `Slice<T>`?
157163

@@ -201,6 +207,178 @@ public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
201207
Therefore you must specify an additional count query.
202208
All other restrictions from the second method apply.
203209

210+
[[faq.custom-queries-and-custom-mappings]]
211+
== Is `@Query` the only way to use custom queries?
212+
213+
No, `@Query` is *not* the only way to run custom queries.
214+
The annotation is comfortable in situations in which your custom query fills your domain completely.
215+
Please remember that SDN 6 assumes your mapped domain model to be the truth.
216+
That means if you use a custom query via `@Query` that only fills a model partially, you are in danger of using the same
217+
object to write the data back which will eventually erase or overwrite data you didn't consider in your query.
218+
219+
So, please use repositories and declarative methods with `@Query` in all cases where the result is shaped like your domain
220+
model or you are sure you don't use a partially mapped model for write commands.
221+
222+
What are the alternatives? First, please read up on two things: <<repositories.custom-implementations,custom repository fragments>>
223+
the <<sdn-building-blocks,levels of abstractions>> we offer in SDN 6.
224+
225+
Why speaking about custom repository fragments?
226+
227+
* You might want to use the Cypher-DSL for building type-safe queries
228+
* You might have more complex situation in which a dynamic query is required, but the query still belongs
229+
conceptually in a repository and not in the service layer
230+
* Your custom query returns a graph shaped result that fits not quite to your domain model
231+
and therefore the custom query should be accomponied by a custom mapping as well
232+
* You have the need for interacting with the driver, i.e. for bulk loads that should not go through object mapping.
233+
234+
Assume the following repository _declaration_ that basically aggregates one base repository plus 3 fragments:
235+
236+
[source,java,indent=0,tabsize=4]
237+
[[aggregating-repository]]
238+
.A repository composed from several fragments
239+
----
240+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java[tags=aggregating-interface]
241+
----
242+
243+
The repository contains <<movie-entity, Movies>> as shown in <<example-node-spring-boot-project,the getting started section>>.
244+
245+
The additional interface from which the repository extends (`DomainResults`, `NonDomainResults` and `LowlevelInteractions`)
246+
are the fragments that addresses all the concerncs above.
247+
248+
=== Using complex, dynamic custom queries and but still returning domain types
249+
250+
The fragment `DomainResults` declares one additional method `findMoviesAlongShortestPath`:
251+
252+
[source,java,indent=0,tabsize=4]
253+
[[domain-results]]
254+
.DomainResults fragment
255+
----
256+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java[tags=domain-results]
257+
----
258+
259+
This method is annotated with `@Transactional(readOnly = true)` to indicate that readers can answer it.
260+
It cannot be derived by SDN but would need a custom query.
261+
This custom query is provided by the one implementation of that interface.
262+
The implementation has the same name with the suffix `Impl`:
263+
264+
[source,java,indent=0,tabsize=4]
265+
[[domain-results-impl]]
266+
.A fragment implementation using the Neo4jTemplate
267+
----
268+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java[tags=domain-results-impl]
269+
----
270+
<.> The `Neo4jTemplate` is injected by the runtime through the constructor of `DomainResultsImpl`. No need for `@Autowired`.
271+
<.> The Cypher-DSL is used to build a complex statement (pretty much the same as shown in <<faq.path-mapping,path mapping>>.)
272+
The statement can be passed directly to the template.
273+
274+
The template has overloads for String-based queries as well, so you could write down the query as String as well.
275+
The important takeaway here is:
276+
277+
* The template "knows" your domain objects and maps them accordingly
278+
* `@Query` is not the only option to define custom queries
279+
* The can be generated in various ways
280+
* The `@Transactional` annotation is respected
281+
282+
=== Using custom queries and custom mappings
283+
284+
Often times a custom query indicates custom results.
285+
Should all of those results be mapepd as `@Node`? Of course not! Many times those objects represents read commands
286+
and are not meant to be used as write commands.
287+
It is also not unlikely that SDN 6 cannot or want not map everything that is possible with Cypher.
288+
It does however offer several hooks to run your own mapping: On the `Neo4jClient`.
289+
The benefit of using the SDN 6 `Neo4jClient` over the driver:
290+
291+
* The `Neo4jClient` is integrated with Springs transaction management
292+
* It has a fluent API for binding parameters
293+
* It has a fluent API exposing both the records and the Neo4j typesystem so that you can access
294+
everything in your result to execute the mapping
295+
296+
Declaring the fragment is exactly the same as before:
297+
298+
[source,java,indent=0,tabsize=4]
299+
[[non-domain-results]]
300+
.A fragment declaring non-domain-type results
301+
----
302+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java[tags=non-domain-results]
303+
----
304+
<.> This is a made up non-domain result. A real world query result would probably look more complex.
305+
<.> The method this fragment adds. Again, the method is annotated with Spring's `@Transactional`
306+
307+
Without an implementation for that fragment, startup would fail, so here it is:
308+
309+
[source,java,indent=0,tabsize=4]
310+
[[non-domain-results-impl]]
311+
.A fragment implementation using the Neo4jClient
312+
----
313+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java[tags=non-domain-results-impl]
314+
----
315+
<.> Here we use the `Neo4jClient`, as provided by the infrastructure.
316+
<.> The client takes only in Strings, but the Cypher-DSL can still be used when rendering into a String
317+
<.> Bind one single value to a named parameter. There's also an overload to bind a whole map of parameters
318+
<.> This is the type of the result you want
319+
<.> And finally, the `mappedBy` method, exposing one `Record` for each entry in the result plus the drivers typesystem if needed.
320+
This is the API in which you hook in for your custom mappings
321+
322+
The whole query runs in the context of a Spring transaction, in this case, a read-only one.
323+
324+
==== Low level interactions
325+
326+
Sometimes you might want to do bulk loadings from a repository or delete whole subgraphs or interact in very specific ways
327+
with the Neo4j Java-Driver. This is possible as well. The following example shows how:
328+
329+
[source,java,indent=0,tabsize=4]
330+
[[low-level-interactions]]
331+
.Fragments using the plain driver
332+
----
333+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java[tags=lowlevel-interactions]
334+
----
335+
<.> Work with the driver directly. As with all the examples: There is no need for `@Autowired` magic. All the fragments
336+
are actually testable on their own.
337+
<.> The usecase is made up. Here we use a driver managed transaction deleting the whole graph and return the number of
338+
deleted nodes and relationships
339+
340+
This interaction does of course not run in a Spring transaction, as the driver does not know about Spring.
341+
342+
Putting it all together, this test succeeds:
343+
344+
[source,java,indent=0,tabsize=4]
345+
[[custom-queries-test]]
346+
.Testing the composed repository
347+
----
348+
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/CustomQueriesIT.java[tags=custom-queries-test]
349+
----
350+
351+
As a final word: All three interfaces and implementations are picked up by Spring Data Neo4j automatically.
352+
There is no need for further configuration.
353+
Also, the same overall repository could have been created with only one additional fragment (the interface defining all three methods)
354+
and one implementation. The implementation would than have had all three abstractions injected (template, client and driver).
355+
356+
All of this applies of course to reactive repositories as well.
357+
They would work with the `ReactiveNeo4jTemplate` and `ReactiveNeo4jClient` and the reactive session provided by the driver.
358+
359+
If you have recuring methods for all repositories, you could swap out the default repository implementation.
360+
361+
[[faq.custom-base-repositories]]
362+
== How do I use custom Spring Data Neo4j base repositories?
363+
364+
Basically the same ways as the shared Spring Data Commons documentation shows for Spring Data JPA in <<repositories.customize-base-repository>>.
365+
Only that in our case you would extend from
366+
367+
[source,java,indent=0,tabsize=4]
368+
[[custom-base-repository]]
369+
.Custom base repository
370+
----
371+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CustomBaseRepositoryIT.java[tags=custom-base-repository]
372+
----
373+
<.> This signature is required by the base class. Take the `Neo4jOperations` (the actual specification of the `Neo4jTemplate`)
374+
and the entity information and store them on an attribute if needed.
375+
376+
In this example we forbide the use of the `findAll` method.
377+
You could add methods taking in a fetch depth and run custom queries based on that depth.
378+
One way to do this is shown in <<domain-results>>.
379+
380+
To enable this base repository for all declared repesotiries enable Neo4j repositories with: `@EnableNeo4jRepositories(repositoryBaseClass = MyRepositoryImpl.class)`.
381+
204382
[[faq.spel.custom-query]]
205383
== How do I use Spring Expression Language in custom queries?
206384

src/main/asciidoc/introduction-and-preface/building-blocks.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The reactive infrastructure requires a Neo4j 4.0+ database.
6464
v
6565
+-------------+--------------+ +---------------------------------------+
6666
| | Provides | |
67-
| Neo4j Java Driver | <-----------------+ neo4j-java-driver-spring-boot-starter |
67+
| Neo4j Java Driver | <-----------------+ spring-boot-starter |
6868
| | | |
6969
+----------------------------+ +---------------------------------------+
7070
----

src/test/java/org/springframework/data/neo4j/documentation/Test.java

-25
This file was deleted.

src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* @author Michael J. Simons
3333
*/
3434
// tag::mapping.annotations[]
35+
// tag::faq.custom-query[]
3536
@Node("Movie") // <.>
3637
public class MovieEntity {
3738

@@ -52,6 +53,7 @@ public MovieEntity(String title, String description) { // <.>
5253
this.title = title;
5354
this.description = description;
5455
}
56+
// end::faq.custom-query[]
5557

5658
// Getters omitted for brevity
5759
// end::mapping.annotations[]
@@ -72,5 +74,7 @@ public List<PersonEntity> getDirectors() {
7274
return directors;
7375
}
7476
// tag::mapping.annotations[]
77+
// tag::faq.custom-query[]
7578
}
7679
// end::mapping.annotations[]
80+
// end::faq.custom-query[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.documentation.repositories.custom_queries;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.BufferedReader;
21+
import java.io.IOException;
22+
import java.io.InputStreamReader;
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.stream.Collectors;
27+
28+
import org.junit.jupiter.api.BeforeAll;
29+
import org.junit.jupiter.api.Test;
30+
import org.neo4j.driver.Driver;
31+
import org.neo4j.driver.Session;
32+
import org.springframework.beans.factory.annotation.Autowired;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
36+
import org.springframework.data.neo4j.documentation.domain.MovieEntity;
37+
import org.springframework.data.neo4j.documentation.domain.PersonEntity;
38+
import org.springframework.data.neo4j.repository.Neo4jRepository;
39+
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
40+
import org.springframework.data.neo4j.test.Neo4jExtension;
41+
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
42+
import org.springframework.transaction.annotation.EnableTransactionManagement;
43+
44+
/**
45+
* @author Michael J. Simons
46+
*/
47+
@Neo4jIntegrationTest
48+
class CustomQueriesIT {
49+
50+
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
51+
52+
// tag::custom-queries-test[]
53+
@Test
54+
void customRepositoryFragmentsShouldWork(
55+
@Autowired PersonRepository people,
56+
@Autowired MovieRepository movies
57+
) {
58+
59+
PersonEntity meg = people.findById("Meg Ryan").get();
60+
PersonEntity kevin = people.findById("Kevin Bacon").get();
61+
62+
List<MovieEntity> moviesBetweenMegAndKevin = movies.
63+
findMoviesAlongShortestPath(meg, kevin);
64+
assertThat(moviesBetweenMegAndKevin).isNotEmpty();
65+
66+
Collection<NonDomainResults.Result> relatedPeople = movies
67+
.findRelationsToMovie(moviesBetweenMegAndKevin.get(0));
68+
assertThat(relatedPeople).isNotEmpty();
69+
70+
assertThat(movies.deleteGraph()).isGreaterThan(0);
71+
assertThat(movies.findAll()).isEmpty();
72+
assertThat(people.findAll()).isEmpty();
73+
}
74+
// end::custom-queries-test[]
75+
76+
@BeforeAll
77+
static void setupData(@Autowired Driver driver) throws IOException {
78+
79+
try (BufferedReader moviesReader = new BufferedReader(
80+
new InputStreamReader(CustomQueriesIT.class.getResourceAsStream("/data/movies.cypher")));
81+
Session session = driver.session()) {
82+
session.run("MATCH (n) DETACH DELETE n").consume();
83+
String moviesCypher = moviesReader.lines().collect(Collectors.joining(" "));
84+
session.run(moviesCypher).consume();
85+
}
86+
}
87+
88+
interface PersonRepository extends Neo4jRepository<PersonEntity, String> {
89+
}
90+
91+
@Configuration
92+
@EnableTransactionManagement
93+
@EnableNeo4jRepositories(considerNestedRepositories = true)
94+
static class Config extends AbstractNeo4jConfig {
95+
96+
@Bean
97+
@Override
98+
public Driver driver() {
99+
return neo4jConnectionSupport.getDriver();
100+
}
101+
102+
@Override
103+
protected Collection<String> getMappingBasePackages() {
104+
return Collections.singletonList(MovieEntity.class.getPackage().getName());
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)