Skip to content

Commit f2a0383

Browse files
GH-1911 - Provide a way for defining dynamic queries in a type safe way.
This commit adds a couple of additional Spring Data Neo4j Extensions (mixins): * `QuerydslPredicateExecutor` * `CypherdslConditionExecutor` * `CypherdslStatementExecutor` * `ReactiveCypherdslStatementExecutor` SDN provides now two different ways of adding dynamic conditions to generated queries: By supporting the existing Spring Data Commons `QuerydslPredicateExecutor` via the new support of QueryDSL expressions inside Cypher-DSL 2021.1.1 as well as the Neo4j native pendant to it, the `CypherdslConditionExecutor`. Conceptionally, both mixins provide the same feature. Both are implemented via an additional repository fragment, that is linked to the repository via the `Neo4jRepositoryFactory`. The Cypher-DSL statement executors come as a pair: For the imperative and reactive world. The interface provides overloads for `findOne` and `finaAll`, which all take at least a Cypher-DSL statement, so that these extensions can be used to specifiy a complete query in a type safe way. Also provided are overloads taking in a type parameter that allows to specify a project, thus creating symmetry with the standard repository and their string based finder methods with a type parameter. Implementationwise both statement executors are provided via new variants of a `AbstractNeo4jQuery` respectively `AbstractReactiveNeo4jQuery`, as we didn’t find a solution to access the projection type from a fragment. This closes #1911.
1 parent fd20a81 commit f2a0383

38 files changed

+1778
-587
lines changed

pom.xml

+6-1
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,12 @@
359359
<artifactId>neo4j</artifactId>
360360
<scope>test</scope>
361361
</dependency>
362-
362+
<dependency>
363+
<groupId>com.querydsl</groupId>
364+
<artifactId>querydsl-core</artifactId>
365+
<version>${querydsl}</version>
366+
<scope>provided</scope>
367+
</dependency>
363368
</dependencies>
364369

365370
<repositories>

src/main/asciidoc/faq/faq.adoc

+22-6
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
204204
Also, the `Pageable` should be unsorted and you should provide a stable order.
205205
We won't use the sorting information from the pageable.
206206
<.> This method returns a page. A page knows about the exact number of total pages.
207-
Therefore you must specify an additional count query.
207+
Therefore, you must specify an additional count query.
208208
All other restrictions from the second method apply.
209209

210210
[[faq.custom-queries-and-custom-mappings]]
@@ -219,15 +219,31 @@ object to write the data back which will eventually erase or overwrite data you
219219
So, please use repositories and declarative methods with `@Query` in all cases where the result is shaped like your domain
220220
model or you are sure you don't use a partially mapped model for write commands.
221221

222-
What are the alternatives? First, please read up on two things: <<repositories.custom-implementations,custom repository fragments>>
222+
What are the alternatives?
223+
224+
* <<projections.sdn.general-remarks,Projections>> might be already enough to shape your *view* on the graph: They can be used to define
225+
the depth of fetching properties and related entities in an explicit way: By modelling them.
226+
* If your goal is to make only the conditions of your queries *dynamic*, then have a look at the <<core.extensions.querydsl, `QuerydslPredicateExecutor`>>
227+
but especially our own variant of it, the `CypherdslConditionExecutor`. Both <<sdn-mixins,mixins>> allow to add conditions to
228+
the full queries we create for you. Thus, you will have the domain fully populated together with custom conditions.
229+
Of course, your conditions must work with what we generate. Find the names of the root node, the related nodes and more
230+
<<custom-queries,here>>.
231+
* Use the http://neo4j-contrib.github.io/cypher-dsl/current/[Cypher-DSL] via the `CypherdslStatementExecutor` or the `ReactiveCypherdslStatementExecutor`.
232+
The Cypher-DSL is predestined to create dynamic queries. In the end, it's what SDN uses under the hood anyway. The corresponding
233+
mixins work both with the domain type of a repository itself as well as with projections (something that the mixins for adding
234+
conditions don't).
235+
236+
If you think that you can solve your problem with a partially dynamic query or a full dynamic query together with a projection,
237+
please jump back now to the chapter <<sdn-mixins, about Spring Data Neo4j Mixins>>.
238+
239+
Otherwise, please read up on two things: <<repositories.custom-implementations,custom repository fragments>>
223240
the <<sdn-building-blocks,levels of abstractions>> we offer in SDN 6.
224241

225-
Why speaking about custom repository fragments?
242+
Why speaking about custom repository fragments now?
226243

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
244+
* You might have more complex situation in which more than one dynamic query is required, but the queries still belong
229245
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
246+
* Your custom queries return a graph shaped result that fits not quite to your domain model
231247
and therefore the custom query should be accomponied by a custom mapping as well
232248
* You have the need for interacting with the driver, i.e. for bulk loads that should not go through object mapping.
233249

src/main/asciidoc/getting-started/getting-started.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Our domain layer should accomplish two things:
133133
=== Example Node-Entity
134134

135135
SDN fully supports unmodifiable entities, for both Java and `data` classes in Kotlin.
136-
Therefore we will focus on immutable entities here, <<movie-entity>> shows a such an entity.
136+
Therefore, we will focus on immutable entities here, <<movie-entity>> shows a such an entity.
137137

138138
NOTE: SDN supports all data types the Neo4j Java Driver supports, see https://neo4j.com/docs/driver-manual/current/cypher-workflow/#driver-type-mapping[Map Neo4j types to native language types] inside the chapter "The Cypher type system".
139139
Future versions will support additional converters.
@@ -229,7 +229,7 @@ Most of the time it's harder to take something away, than to add stuff afterward
229229
Furthermore, using store specifics leaks your store into your domain.
230230
From a performance point of view, there is no penalty.
231231

232-
A repository fitting to any of the movie entities above looks like this:
232+
A reactive repository fitting to any of the movie entities above looks like this:
233233

234234
[source,java]
235235
[[movie-repository]]

src/main/asciidoc/index.adoc

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ NOTE: Copies of this document may be made for your own use and for distribution
3333

3434
include::introduction-and-preface/index.adoc[]
3535
include::{spring-data-commons-docs}/dependencies.adoc[leveloffset=+1]
36-
include::{spring-data-commons-docs}/repositories.adoc[leveloffset=+1]
3736

3837
[[reference]]
3938
= Reference Documentation
@@ -49,6 +48,12 @@ include::getting-started/index.adoc[]
4948

5049
include::object-mapping/index.adoc[]
5150

51+
include::{spring-data-commons-docs}/repositories.adoc[leveloffset=+1]
52+
53+
include::{spring-data-commons-docs}/query-by-example.adoc[leveloffset=+2]
54+
55+
include::object-mapping/sdn-extensions.adoc[leveloffset=+1]
56+
5257
include::{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+1]
5358

5459
include::object-mapping/projections.adoc[leveloffset=+1,lines=4..]

src/main/asciidoc/object-mapping/projections.adoc

+13-2
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,23 @@ interface TestRepository extends CrudRepository<TestEntity, Long> { // <.>
126126
<.> Methods returning one or more `TestEntity` will just return instances of it, as it matches the domain type
127127
<.> Methods returning one or more instances of class that extend the domain type will just return instances
128128
of the extending class. The domain type of the method in question will the extended class, which
129-
still satifies the domain type of the repository itself
129+
still satisfies the domain type of the repository itself
130130
<.> This method returns an interface projection, the return type of the method is therefore different
131131
from the repository's domain type. The interface can only access properties defined in the domain type
132132
<.> This method returns a DTO projection. Executing it will cause SDN to issue a warning, as the DTO defines
133133
`numberOfRelations` as additional attribute, which is not in the contract of the domain type.
134134
The annotated attribute `aProperty` in `TestEntity` will be correctly translated to `a_property` in the query.
135-
As above, the return type is different from the repositories domain type.
135+
As above, the return type is different from the repositories' domain type.
136136
<.> This method also returns a DTO projection. However, no warning will be issued, as the query contains a fitting
137137
value for the additional attributes defined in the projection.
138+
139+
TIP: While the repository in <<projections.simple-entity-repository,the listing above>> uses a concrete return type to
140+
define the projection, another variant is the use of <<projection.dynamic,dynamic projections>> as explained in the
141+
parts of the documentation Spring Data Neo4j shares with other Spring Data Projects. A dynamic projection can be
142+
applied to both closed and open interface projections as well as to class based DTO projections:
143+
+
144+
+
145+
The key to a dynamic projection is to specifiy the desired projection type as the last parameter to a query method
146+
in a repository like this: `<T> Collection<T> findByName(String name, Class<T> type)`. This is a declaration that
147+
could be added to the `TestRepository` above and allow for different projections retrieved by the same method, without
148+
to repeat a possible `@Query` annotation on several methods.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
[[sdn-mixins]]
2+
== Spring Data Neo4j Extensions
3+
4+
=== Available extensions for Spring Data Neo4j repositories
5+
6+
Spring Data Neo4j offers a couple of extensions or "mixins" that can be added to repositories. What is a mixin? According to
7+
https://en.wikipedia.org/wiki/Mixin[Wikipedia] mixins are a language concept that allows a programmer to inject some code
8+
into a class. Mixin programming is a style of software development, in which units of functionality are created in a class
9+
and then mixed in with other classes.
10+
11+
Java does not support that concept on the language level, but we do emulate it via a couple of interfaces and a runtime
12+
that adds appropriate implementations and interceptors for.
13+
14+
Mixins added by default are `QueryByExampleExecutor` and `ReactiveQueryByExampleExecutor` respectively. Those interfaces are
15+
explained in detail in <<query-by-example>>.
16+
17+
Additional mixins provided are:
18+
19+
* `QuerydslPredicateExecutor`
20+
* `CypherdslConditionExecutor`
21+
* `CypherdslStatementExecutor`
22+
* `ReactiveCypherdslStatementExecutor`
23+
24+
[[sdn-mixins.dynamic-conditions]]
25+
==== Add dynamic conditions to generated queries
26+
27+
Both the `QuerydslPredicateExecutor` and `CypherdslConditionExecutor` provide the same concept: SDN generates a query, you
28+
provide "predicates" (Query DSL) or "conditions" (Cypher DSL) that will be added. We recommend the Cypher DSL, as this is
29+
what SDN 6 uses natively. You might even want to consider using the
30+
http://neo4j-contrib.github.io/cypher-dsl/2021.1.1/#thespringdataneo4j6annotationprocessor[annotation processor] that generates
31+
a static meta model for you.
32+
33+
How does that work? Declare your repository as described above and add *one* of the following interfaces:
34+
35+
[source,java,indent=0,tabsize=4]
36+
----
37+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java[tags=sdn-mixins.dynamic-conditions.add-mixin]
38+
----
39+
<.> Standard repository declaration
40+
<.> The Query DSL mixin
41+
42+
*OR*
43+
44+
[source,java,indent=0,tabsize=4]
45+
----
46+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java[tags=sdn-mixins.dynamic-conditions.add-mixin]
47+
----
48+
<.> Standard repository declaration
49+
<.> The Cypher DSL mixin
50+
51+
Exemplary usage is shown with the Cypher DSL condition executor:
52+
53+
[source,java,indent=0,tabsize=4]
54+
----
55+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java[tags=sdn-mixins.dynamic-conditions.usage]
56+
----
57+
<.> Define a named `Node` object, targeting the root of the query
58+
<.> Derive some properties from it
59+
<.> Create an `or` condition. An anonymous parameter is used for the first name, a named parameter for
60+
the last name. This is how you define parameters in those fragments and one of the advantages over the Query-DSL
61+
mixin which can't do that.
62+
Literals can be expressed with `Cypher.literalOf`.
63+
<.> Define a `SortItem` from one of the properties
64+
65+
The code looks pretty similar for the Query-DSL mixin. Reasons for the Query-DSL mixin can be familiarity of the API and
66+
that it works with other stores, too. Reasons against it are the fact that you need an additional library on the class path,
67+
it's missing support for traversing relationships and the above mentioned fact that it doesn't support parameters in its
68+
predicates (it technically does, but there are no API methods to actually pass them to the query being executed).
69+
70+
[[sdn-mixins.using-cypher-dsl-statements]]
71+
==== Using (dynamic) Cypher-DSL statements for entities and projections
72+
73+
Adding the corresponding mixin is not different than using the <<sdn-mixins.dynamic-conditions, condition excecutor>>:
74+
75+
[source,java,indent=0,tabsize=4]
76+
----
77+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslStatementExecutorIT.java[tags=sdn-mixins.using-cypher-dsl-statements.add-mixin]
78+
----
79+
80+
Please use the `ReactiveCypherdslStatementExecutor` when extending the `ReactiveNeo4jRepository`.
81+
82+
The `CypherdslStatementExecutor` comes with several overloads for `findOne` and `findAll`. They all take a Cypher-DSL
83+
statement respectively an ongoing definition of that as a first parameter and in case of the projecting methods, a type.
84+
85+
If a query requires parameters, they must be defined via the Cypher-DSL itself and also populated by it, as the following listing shows:
86+
87+
[source,java,indent=0,tabsize=4]
88+
----
89+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslStatementExecutorIT.java[tags=sdn-mixins.using-cypher-dsl-statements.using]
90+
----
91+
<.> The dynamic query is build in a type safe way in a helper method
92+
<.> We already saw this in <<sdn-mixins.dynamic-conditions,here>>, where we also defined some variables holding the model
93+
<.> We define an anonymous parameter, filled by the actual value of `name`, which was passed to the method
94+
<.> The statement returned from the helper method is used to find an entity
95+
<.> Or a projection.
96+
97+
The `findAll` methods works similar.
98+
The imperative Cypher-DSL statement executor also provides an overload returning paged results.

src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java

+11-8
Original file line numberDiff line numberDiff line change
@@ -676,29 +676,29 @@ public T getRequiredSingleResult() {
676676
private Optional<Neo4jClient.RecordFetchSpec<T>> createFetchSpec() {
677677
QueryFragmentsAndParameters queryFragmentsAndParameters = preparedQuery.getQueryFragmentsAndParameters();
678678
String cypherQuery = queryFragmentsAndParameters.getCypherQuery();
679-
Map<String, Object> finalParameters = preparedQuery.getQueryFragmentsAndParameters().getParameters();
679+
Map<String, Object> finalParameters = queryFragmentsAndParameters.getParameters();
680680

681681
QueryFragmentsAndParameters.QueryFragments queryFragments = queryFragmentsAndParameters.getQueryFragments();
682682
Neo4jPersistentEntity<?> entityMetaData = (Neo4jPersistentEntity<?>) queryFragmentsAndParameters.getNodeDescription();
683683

684684
boolean containsPossibleCircles = entityMetaData != null && entityMetaData.containsPossibleCircles(queryFragments::includeField);
685685
if (cypherQuery == null || containsPossibleCircles) {
686686

687-
Map<String, Object> parameters = queryFragmentsAndParameters.getParameters();
688-
689687
if (containsPossibleCircles && !queryFragments.isScalarValueReturn()) {
690688
GenericQueryAndParameters genericQueryAndParameters =
691-
createQueryAndParameters(entityMetaData, queryFragments, parameters);
689+
createQueryAndParameters(entityMetaData, queryFragments, queryFragmentsAndParameters.getParameters());
692690

693691
if (genericQueryAndParameters.isEmpty()) {
694692
return Optional.empty();
695693
}
696694
cypherQuery = renderer.render(queryFragments.generateGenericStatement());
697695
finalParameters = genericQueryAndParameters.getParameters();
698696
} else {
699-
cypherQuery = renderer.render(queryFragments.toStatement());
697+
Statement statement = queryFragments.toStatement();
698+
cypherQuery = renderer.render(statement);
699+
finalParameters = new HashMap<>(finalParameters);
700+
finalParameters.putAll(statement.getParameters());
700701
}
701-
702702
}
703703

704704
Neo4jClient.MappingSpec<T> newMappingSpec = neo4jClient.query(cypherQuery)
@@ -715,9 +715,11 @@ private GenericQueryAndParameters createQueryAndParameters(Neo4jPersistentEntity
715715
.prepareMatchOf(entityMetaData, queryFragments.getMatchOn(), queryFragments.getCondition())
716716
.returning(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).build();
717717

718+
Map<String, Object> usedParameters = new HashMap<>(parameters);
719+
usedParameters.putAll(rootNodesStatement.getParameters());
718720
final Collection<Long> rootNodeIds = new HashSet<>((Collection<Long>) neo4jClient
719721
.query(renderer.render(rootNodesStatement))
720-
.bindAll(parameters)
722+
.bindAll(usedParameters)
721723
.fetch()
722724
.one()
723725
.map(values -> values.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE))
@@ -737,6 +739,8 @@ private GenericQueryAndParameters createQueryAndParameters(Neo4jPersistentEntity
737739
.prepareMatchOf(entityMetaData, relationshipDescription, queryFragments.getMatchOn(), queryFragments.getCondition())
738740
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
739741

742+
usedParameters = new HashMap<>(parameters);
743+
usedParameters.putAll(statement.getParameters());
740744
neo4jClient.query(renderer.render(statement))
741745
.bindAll(parameters)
742746
.fetch()
@@ -791,5 +795,4 @@ private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<Long> relations
791795
};
792796
}
793797
}
794-
795798
}

0 commit comments

Comments
 (0)