DATAGRAPH-1333 - Migrate documentation and apply SD formatting.

This commit is contained in:
Gerrit Meier
2020-07-09 14:41:55 +02:00
parent 5dba573728
commit 74faebf0da
310 changed files with 6381 additions and 8254 deletions

View File

@@ -4,15 +4,13 @@ image:https://spring.io/badges/spring-data-neo4j/ga.svg[Spring Data Neo4j,link=h
:sectanchors:
// tag::properties[]
:neo4jGroupId: org.neo4j.springframework.data
:springGroupId: org.springframework.data.neo4j
:neo4jGroupId: org.springframework.data.neo4j
:artifactId: spring-data-neo4j
:artifactIdStarter: spring-data-neo4j-rx-spring-boot-starter
:artifactIdStarter: spring-data-neo4j-spring-boot-starter
:neo4j-version: 4.0.4
:spring-boot-version: 2.3.0.RELEASE
:spring-data-neo4j-rx-version: 1.1.1
:spring-data-neo4j-version: 6.0.0
:spring-boot-version: 2.3.2.RELEASE
:spring-data-neo4j-version: 6.0.0-SNAPSHOT
// end::properties[]
[abstract]
@@ -49,7 +47,7 @@ If you are on https://spring.io/projects/spring-boot[Spring Boot], all you have
<dependency>
<groupId>{neo4jgroupId}</groupId>
<artifactId>{artifactIdStarter}</artifactId>
<version>{spring-data-neo4j-rx-version}</version>
<version>{spring-data-neo4j-version}</version>
</dependency>
----
@@ -57,9 +55,9 @@ and configure your database connection:
[source,properties]
----
org.neo4j.driver.uri=bolt://localhost:7687
org.neo4j.driver.authentication.username=neo4j
org.neo4j.driver.authentication.password=secret
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
----
Please have a look at our https://neo4j.github.io/sdn-rx[manual] for an overview about the architecture, how to define
@@ -103,9 +101,6 @@ The imperative version looks pretty much the same but uses `EnableNeo4jRepositor
IMPORTANT: We recommend Spring Boot, the automatic configuration and especially the dependency management
through the Starters in contrast to the manual work of managing dependencies and configuration.
+
Please consult our https://neo4j.github.io/sdn-rx[manual] for more information.
Here is a quick teaser of a reactive application using Spring Data Repositories in Java:
@@ -150,7 +145,7 @@ class MyService {
=== Building SDN
Please have a look at the documentation: https://neo4j.github.io/sdn-rx/current/#building-sdn-rx[Building SDN].
Please have a look at the documentation: https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#building-sdn-rx[Building SDN].
== Getting Help

View File

@@ -19,7 +19,7 @@ The strategy will either be `internal`, `assigned` or `generated`.
The internal strategy will be the default.
To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.neo4j.springframework.data.core.schema.Id`
To configure the Id strategy, a meta-annotated `@Id` annotation will be provided through `org.springframework.data.neo4j.core.schema.Id`
=== Consequences

View File

@@ -1,4 +1,4 @@
= General architectural discussions about Spring Data Neo4jRX
= General architectural discussions about Spring Data Neo4j
[abstract]
--
@@ -25,7 +25,7 @@ So far we have identified the following modules:
* Lifecycle: Lifecycle must not depend directly on mapping, but should only care whether an Object and its Relations are managed or not
* Querying: Generates cypher queries, depends on schema
Those will be reassembled as packages inside Spring Data Neo4j RX.
Those will be reassembled as packages inside Spring Data Neo4j.
There are no short-term planes to create additional artifacts from those.
[[schema]]
@@ -42,8 +42,8 @@ That way we we can avoid a compile time dependency to Spring Data and have an in
Spring Data JDBC doesn't restrict the supported or scanned classes from Spring Data sides.
Our schema should also support non-annotated classes and be smart about naming things, but we will require at least the `@Node` or `@Relationship` annotation to the outside world.
The schema will life independent from Spring classes in `org.neo4j.springframework.data.core.schema`.
Each property of a class that is not identified as a simple type by `org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well.
The schema will life independent from Spring classes in `org.springframework.data.neo4j.core.schema`.
Each property of a class that is not identified as a simple type by `org.springframework.data.neo4j.core.schema.Neo4jSimpleTypes` will be considered describing a relationship and thus required to be part of the schema as well.
==== Context
@@ -87,7 +87,7 @@ static class BikeNode {
=== "Rich" relationships
There should be no means of using a relationship as aggregate root in SDN/RX (like it is today the case with `@RelationshipEntity`).
There should be no means of using a relationship as aggregate root in SDN (like it is today the case with `@RelationshipEntity`).
Instead we suggest that properties of relationships are mapped to POJOs.
This has the following requirements:
On the node representing the start node, the relationship (either 1:1 or 1:n) has to be annotated with `@Relationship` specifying the type of the end node like this:
@@ -140,13 +140,13 @@ Integration tests will get executed withing the `verify` goal and their class na
== Configuration
Spring Data Neo4j RX takes a "ready to use" drivers instance and uses that.
Spring Data Neo4j takes a "ready to use" drivers instance and uses that.
We won't provide any additional configuration for aspects that are configurable through the driver.
We will however provide support to configure the drivers instance in Spring Boot.
The current SDN Spring Boot Starter only configures the Neo4j-OGM transport and not the "real" driver.
Our plans for a future starter a have been <<starter,described separately>>.
Closing the driver is not the the concern of Spring Data Neo4j RX.
Closing the driver is not the the concern of Spring Data Neo4j.
The lifecycle of that bean should be managed by the application.
Therefore, the starter need to take care of register the drivers instance with the application.
@@ -162,7 +162,7 @@ It is only meant to be a basic for discussions.
----
@startuml
note "Implementation of Spring Data Commons SPI" as SDC_note
package "org.neo4j.springframework.data" {
package "org.springframework.data.neo4j" {
package "core" {
interface Neo4jClient
interface ReactiveNeo4jClient
@@ -286,7 +286,7 @@ and executes it through the `neo4jOperations` (`Neo4jTemplate`) class.
=== Dirty tracking
We considered several approaches of dirty tracking in SDN/RX:
We considered several approaches of dirty tracking in SDN:
. No dirty tracking at all.
_Not an option when it comes to relationships._
@@ -299,12 +299,12 @@ We considered several approaches of dirty tracking in SDN/RX:
We have settled with option 1 (See ADR-004), analogue to Spring Data JDBC.
[[starter]]
== Spring Data Neo4jRX Spring Boot Starter
== Spring Data Neo4j Spring Boot Starter
The Spring Data Neo4j RX Spring Boot Starter provides automatic configuration to
The Spring Data Neo4j Spring Boot Starter provides automatic configuration to
* Create an instance of the https://github.com/neo4j/neo4j-java-driver[neo4j-java-driver]
* Configure Spring Data Neo4j RX itself inside a Spring Boot application and enabling Spring Data repositories
* Configure Spring Data Neo4j itself inside a Spring Boot application and enabling Spring Data repositories
=== Architectural guidelines and principles
@@ -313,17 +313,17 @@ we don't use https://projectlombok.org[Lombok] currently in the starter as none
==== Project hierarchy and dependency management
While the starter is a module of SDN/RX itself, it's actual parent project is `org.springframework.boot:spring-boot-starter-parent`.
While the starter is a module of SDN itself, it's actual parent project is `org.springframework.boot:spring-boot-starter-parent`.
Thus we stay consistent with all other Spring Boot starters, that are actually part of Spring Boot.
==== Responsibilities
The starter and it's automatic configuration is responsible for configuring Spring Data Neo4j RX repositories and infrastructure.
The starter and it's automatic configuration is responsible for configuring Spring Data Neo4j repositories and infrastructure.
It needs a configured Neo4j Java Driver and therefor is itself dependent on `org.neo4j.driver:neo4j-java-driver-spring-boot-starter`,
the official starter for the Neo4j Java Driver.
Having the starter provide automatic configuration is in accordance with the plans for Spring Data Neo4j RX.
Spring Data Neo4j RX should only deal with configured, ready to use driver objects and not be responsible for configuring those.
Having the starter provide automatic configuration is in accordance with the plans for Spring Data Neo4j.
Spring Data Neo4j should only deal with configured, ready to use driver objects and not be responsible for configuring those.
=== Future plans

View File

@@ -105,7 +105,12 @@
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAfterCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAroundCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAroundCheck">
<property name="allowEmptyTypes" value="true" />
<property name="allowEmptyConstructors" value="true" />
<property name="allowEmptyMethods" value="true" />
<property name="allowEmptyCatches" value="true" />
</module>
<!-- We have some empty blocks and statements. -->
<module name="SuppressionCommentFilter"/>

View File

@@ -1,11 +1,11 @@
[[structure:Default]]
[role=group,includesConstraints="structure:mapping"]
Most of the time, the package structure under `org.neo4j.springframework.data` should reflect the main building parts.
Most of the time, the package structure under `org.springframework.data.neo4j` should reflect the main building parts.
[[structure:mapping]]
[source,cypher,role=constraint,requiresConcepts="dependency:Package"]
.The mapping package must not depend on any other SDN/RX packages than `schema` and `convert`
.The mapping package must not depend on any other SDN packages than `schema` and `convert`
----
MATCH (a:Main:Artifact)
OPTIONAL MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn in ['org.springframework.data.neo4j.core.schema', 'org.springframework.data.neo4j.core.convert']

View File

@@ -403,6 +403,11 @@
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,9 +1,9 @@
[[building-sdn-rx]]
= Building SDN/RX
[[building-SDN]]
= Building Spring Data Neo4j
== Requirements
* JDK 13+ (Can be https://openjdk.java.net[OpenJDK] or https://www.oracle.com/technetwork/java/index.html[Oracle JDK])
* JDK 8+ (Can be https://openjdk.java.net[OpenJDK] or https://www.oracle.com/technetwork/java/index.html[Oracle JDK])
* Maven 3.6.2 (We provide the Maven wrapper, see `mvnw` respectively `mvnw.cmd` in the project root; the wrapper downloads the appropriate Maven version automatically)
* A Neo4j 3.5.+ database, either
** running locally
@@ -11,18 +11,12 @@
=== About the JDK version
SDN/RX itself targets JDK 8 release but builds currently on JDK13.
We will track the current release cadence of Java.
Choosing JDK 8 is a decision influenced by various aspects
* SDN/RX is a Spring Data project.
Spring Data commons baseline is still JDK 8 and so is Spring Frameworks baseline.
Thus it is only natural to keep the JDK 8 baseline.
* While there is an increase of projects started with JDK 11 (which is Oracles current LTS release of Java),
many existing projects are still on JDK 8.
We don't want to lose them as users right from the start.
All examples (`/examples`) and also benchmarking (`/benchmarks`) will be compiled and run with the latest Java release, though.
* SDN is a Spring Data project.
Spring Data commons baseline is still JDK 8 and so is Spring Frameworks baseline.
Thus it is only natural to keep the JDK 8 baseline.
* While there is an increase of projects started with JDK 11 (which is Oracles current LTS release of Java), many existing projects are still on JDK 8. We don't want to lose them as users right from the start.
== Running the build
@@ -31,10 +25,10 @@ The following sections are alternatives and roughly sorted by increased effort.
All builds require a local copy of the project:
[source,console,subs="verbatim,attributes"]
[[checkout-sdn-rx]]
.Clone SDN/RX
[[checkout-SDN]]
.Clone SDN
----
$ git clone git@github.com:neo4j/sdn-rx.git
$ git clone git@github.com:spring-projects/spring-data-neo4j.git
----
Before you proceed, verify your locally installed JDK version.
@@ -64,33 +58,6 @@ Our build uses https://www.testcontainers.org/modules/databases/neo4j/[Testconta
.Build with default settings on Linux / macOS
----
$ ./mvnw clean verify
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Spring Data Neo4j RX [pom]
[INFO] SDN⚡RX [jar]
[INFO] SDN⚡RX Spring Boot Starter Parent [pom]
[INFO] SDN⚡RX Spring Boot Starter Autoconfiguration [jar]
[INFO] SDN⚡RX Spring Boot Starter [jar]
[INFO] SDN⚡RX Examples for Spring Boot [jar]
[INFO] SDN⚡RX Examples Mapping [jar]
...
[INFO] Reactor Summary:
[INFO]
[INFO] Spring Data Neo4j RX 1.0.0-SNAPSHOT ................ SUCCESS [ 5.937 s]
[INFO] SDN⚡RX 1.0.0-SNAPSHOT ............................. SUCCESS [09:37 min]
[INFO] SDN⚡RX Spring Boot Starter Parent 1.0.0-SNAPSHOT .. SUCCESS [ 1.734 s]
[INFO] SDN⚡RX Spring Boot Starter Autoconfiguration 1.0.0-SNAPSHOT SUCCESS [ 57.069 s]
[INFO] SDN⚡RX Spring Boot Starter 1.0.0-SNAPSHOT ......... SUCCESS [ 0.444 s]
[INFO] SDN⚡RX Examples for Spring Boot 999-SNAPSHOT ...... SUCCESS [ 4.055 s]
[INFO] SDN⚡RX Examples Mapping 999-SNAPSHOT .............. SUCCESS [ 0.683 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10:48 min
[INFO] Finished at: 2019-10-02T15:25:48+02:00
[INFO] ------------------------------------------------------------------------
----
On a Windows machine, use
@@ -104,7 +71,7 @@ $ mvnw.cmd clean verify
The output should be similar.
At the moment, this build tests agains Neo4j 3.5, as 4.0 is not yet available on Docker Hub.
At the moment, this build tests against Neo4j 3.5, as 4.0 is not yet available on Docker Hub.
As a consequence, tests requiring a reactive capable database, are skipped, as this is a feature of Neo4j 4.0.
==== Using another image
@@ -115,7 +82,7 @@ The image version to use can be configured through an environmental variable lik
[[build-other-image]]
.Build using a different Neo4j Docker image
----
$ SDN_RX_NEO4J_VERSION=3.5.11-enterprise SDN_RX_NEO4J_ACCEPT_COMMERCIAL_EDITION=yes ./mvnw clean verify
$ SDN_NEO4J_VERSION=3.5.11-enterprise SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION=yes ./mvnw clean verify
----
Here we are using 3.5.11 enterprise and also accept the license agreement.
@@ -133,8 +100,7 @@ You can get a copy of Neo4j at our https://neo4j.com/download-center/#enterprise
Especially you can get https://neo4j.com/download-center/?ref=blog/#prerelease[the current prelease of Neo4j 4.0], supporting all the reactive features.
Please download the version applicable to your operating system and follow the instructions to start it.
A required step is to open a browser and go to http://localhost:7474 after you started the database and
change the default password from `neo4j` to something of your liking.
A required step is to open a browser and go to http://localhost:7474 after you started the database and change the default password from `neo4j` to something of your liking.
After that, you can run a complete build by specifying the local `bolt` URL:
@@ -142,37 +108,37 @@ After that, you can run a complete build by specifying the local `bolt` URL:
[[build-using-locally-running-database]]
.Build using a locally running database
----
$ SDN_RX_NEO4J_URL=bolt://localhost:7687 SDN_RX_NEO4J_PASSWORD=secret ./mvnw clean verify
$ SDN_NEO4J_URL=bolt://localhost:7687 SDN_NEO4J_PASSWORD=secret ./mvnw clean verify
----
== Summary of environment variables controlling the build
[cols="3,1,3", options="header"]
[cols="3,1,3",options="header"]
|===
|Name|Default value|Meaning
|`SDN_RX_NEO4J_VERSION`
|`SDN_NEO4J_VERSION`
|3.5.6
|Version of the Neo4j docker image to use, see https://hub.docker.com/_/neo4j[Neo4j Docker Official Images]
|`SDN_RX_NEO4J_ACCEPT_COMMERCIAL_EDITION`
|`SDN_NEO4J_ACCEPT_COMMERCIAL_EDITION`
|no
|Some tests may require the enterprise edition of Neo4j.
We build and test against the enterprise edition internally, but we won't force you
to accept the license if you don't want to.
We build and test against the enterprise edition internally, but we won't force you
to accept the license if you don't want to.
|`SDN_RX_NEO4J_URL`
|`SDN_NEO4J_URL`
|not set
|Setting this environment allows connecting to a locally running Neo4j instance.
We use this a lot during development.
We use this a lot during development.
|`SDN_RX_NEO4J_PASSWORD`
|`SDN_NEO4J_PASSWORD`
|not set
|Password for the `neo4j` user of the instance configured with `SDN_RX_NEO4J_URL`.
|Password for the `neo4j` user of the instance configured with `SDN_NEO4J_URL`.
|===
NOTE: You need to set both `SDN_RX_NEO4J_URL` and `SDN_RX_NEO4J_PASSWORD` to use a local instance.
NOTE: You need to set both `SDN_NEO4J_URL` and `SDN_NEO4J_PASSWORD` to use a local instance.
== Checkstyle and friends
@@ -186,7 +152,7 @@ Your build will break on formatting errors or something like unused imports.
We also use https://jqassistant.org[jQAssistant], a Neo4j-based tool, to verify some aspects of our architecture.
The rules are described with Cypher and your build will break when they are violated:
include::../../etc/jqassistant/index.adoc[leveloffset=4]
include::../../../../etc/jqassistant/index.adoc[leveloffset=4]
==== Accessing the jQAssistant database
@@ -198,7 +164,7 @@ When the build finishes, execute the following command:
[[start-jqassistant]]
.Start jQAssistant
----
$ ./mvnw -pl org.neo4j.springframework.data:spring-data-neo4j-rx jqassistant:server
$ ./mvnw -pl org.springframework.data.neo4j:spring-data-neo4j jqassistant:server
----
Access the standard Neo4j browser at http://localhost:7474 and a dedicated jQA-Dashboard at http://localhost:7474/jqassistant/dashboard/.
@@ -209,6 +175,6 @@ The scanning and analyzing can be triggered individually, without going through
[[scan-with-jqassistant]]
.Manually scan and analyze the main project
----
$ ./mvnw -pl org.neo4j.springframework.data:spring-data-neo4j-rx jqassistant:scan@jqassistant-scan
$ ./mvnw -pl org.neo4j.springframework.data:spring-data-neo4j-rx jqassistant:analyze@jqassistant-analyze
$ ./mvnw -pl org.springframework.data.neo4j:spring-data-neo4j jqassistant:scan@jqassistant-scan
$ ./mvnw -pl org.springframework.data.neo4j:spring-data-neo4j jqassistant:analyze@jqassistant-analyze
----

View File

@@ -6,7 +6,7 @@ Find the list of supported cypher types in the official drivers manual: https://
Primitive types of wrapper types are equally supported.
[cols="3,3,1", options="header"]
[cols="3,3,1",options="header"]
|===
|Domain type|Cypher type|Maps directly to native type
@@ -57,8 +57,8 @@ Primitive types of wrapper types are equally supported.
|`java.util.Date`
|String formatted as ISO 8601 Date (`yyyy-MM-dd'T'HH:mm:ss.SSSZ`).
Notice the `Z`: SDN/RX will store all `java.util.Date` instances in `UTC`.
If you require the time zone, use a type that supports it (i.e. `ZoneDateTime`) or store the zone as a separate property.
Notice the `Z`: SDN will store all `java.util.Date` instances in `UTC`.
If you require the time zone, use a type that supports it (i.e. `ZoneDateTime`) or store the zone as a separate property.
|
|`double[]`
@@ -137,19 +137,19 @@ Primitive types of wrapper types are equally supported.
|Point
|✔
|`org.neo4j.springframework.data.types.GeographicPoint2d`
|`org.springframework.data.neo4j.types.GeographicPoint2d`
|Point with CRS 4326
|
|`org.neo4j.springframework.data.types.GeographicPoint3d`
|`org.springframework.data.neo4j.types.GeographicPoint3d`
|Point with CRS 4979
|
|`org.neo4j.springframework.data.types.CartesianPoint2d`
|`org.springframework.data.neo4j.types.CartesianPoint2d`
|Point with CRS 7203
|
|`org.neo4j.springframework.data.types.CartesianPoint3d`
|`org.springframework.data.neo4j.types.CartesianPoint3d`
|Point with CRS 9157
|
@@ -169,27 +169,25 @@ Primitive types of wrapper types are equally supported.
== Custom conversions
If you prefer to work with your own types in the entities or as parameters for `@Query` annotated methods,
you can define and provide a custom converter implementation.
If you prefer to work with your own types in the entities or as parameters for `@Query` annotated methods, you can define and provide a custom converter implementation.
First you have to implement a `GenericConverter` and register the types your converter should handle.
For entity property type converters you need to take care of converting your type to *and* from a Neo4j Java Driver `Value`.
If your converter is supposed to work only with custom query methods in the repositories, it is sufficient
to provide the one-way conversion to the `Value` type.
If your converter is supposed to work only with custom query methods in the repositories, it is sufficient to provide the one-way conversion to the `Value` type.
.Example of a custom converter implementation
[source,java,indent=0]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/conversion/MyCustomTypeConverter.java[tag=custom-converter.implementation]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/conversion/MyCustomTypeConverter.java[tag=custom-converter.implementation]
----
To make SDN/RX aware of your converter, it has to be registered in the `Neo4jConversions`.
To do this, you have to create a `@Bean` with the type `org.neo4j.springframework.data.core.convert.Neo4jConversions`.
To make SDN aware of your converter, it has to be registered in the `Neo4jConversions`.
To do this, you have to create a `@Bean` with the type `org.springframework.data.neo4j.core.convert.Neo4jConversions`.
Otherwise, the `Neo4jConversions` will get created in the background with the internal default converters only.
.Example of a custom converter implementation
[source,java,indent=0]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/conversion/MyCustomTypeConverter.java[tag=custom-converter.neo4jConversions]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/conversion/MyCustomTypeConverter.java[tag=custom-converter.neo4jConversions]
----
If you need multiple converters in your application, you can add as many as you need in the `Neo4jConversions` constructor.

View File

@@ -1,12 +1,11 @@
:leveloffset: +1
[[appendix]]
= Appendix
[[sdn-appendix]]
= Spring Data Neo4j Appendix
:numbered!:
:leveloffset: +1
include::conversions.adoc[]
include::sdc-repository-query-keywords-reference.adoc[]
include::sdc-repository-query-return-types-reference.adoc[]
include::neo4j-client.adoc[]
include::query-creation.adoc[]
include::migrating.adoc[]

View File

@@ -1,5 +1,5 @@
[[Migrating]]
= Migrating from SDN+OGM to SDN/RX
= Migrating from SDN+OGM to SDN
== Known issues with past SDN+OGM migrations
@@ -25,11 +25,11 @@ It's not easy to get the terms right.
We wrote the building blocks of an SDN+OGM setting https://michael-simons.github.io/neo4j-sdn-ogm-tips/what_are_the_building_blocks_of_sdn_and_ogm.html[here].
It may be so that all of them have been added by coincidence and you're dealing with a lof of conflicting dependencies.
TIP: Backed by those observations, we recommend to make sure you're using only the Bolt or http transport in your current application before switching from SDN+OGM to SDN/RX.
TIP: Backed by those observations, we recommend to make sure you're using only the Bolt or http transport in your current application before switching from SDN+OGM to SDN.
Thus, your application and the access layer of your application is to large extend independent from the databases version.
From that state, consider moving from SDN+OGM to SDN/RX.
From that state, consider moving from SDN+OGM to SDN.
== Prepare the migration from SDN+OGM Lovelace or SDN+OGM Moore to SDN/RX
== Prepare the migration from SDN+OGM Lovelace or SDN+OGM Moore to SDN
NOTE: The _Lovelace_ release train corresponds to SDN 5.1.x and OGM 3.1.x, while the _Moore_ is SDN 5.2.x and OGM 3.2.x.
@@ -44,14 +44,12 @@ The above dependencies have to be removed.
Migrating from the embedded solution is probably the toughest migration, as you need to setup a server, too.
It is however the one that gives you much value in itself:
In the future, you will be able to upgrade the database itself without having to consider your application framework,
and your data access framework as well.
In the future, you will be able to upgrade the database itself without having to consider your application framework, and your data access framework as well.
=== You're using the HTTP transport
You have added `org.neo4j:neo4j-ogm-http-driver` and configured an url like `http://user:password@localhost:7474`.
The dependency has to be replaced with `org.neo4j:neo4j-ogm-bolt-driver` and you need to configure a Bolt url
like `bolt://localhost:7687` or use the new `neo4j://` scheme, which takes care of routing, too.
The dependency has to be replaced with `org.neo4j:neo4j-ogm-bolt-driver` and you need to configure a Bolt url like `bolt://localhost:7687` or use the new `neo4j://` scheme, which takes care of routing, too.
=== You're already using Bolt indirectly
@@ -60,18 +58,16 @@ You can keep your existing URL.
== Migrating
Once you have made sure, that your SDN+OGM application works over Bolt as expected, you can start migrating to SDN/RX.
Once you have made sure, that your SDN+OGM application works over Bolt as expected, you can start migrating to SDN.
* Remove all `org.neo4j:neo4j-ogm-*` dependencies
* Remove `org.springframework.data:spring-data-neo4j`
* Configuring SDN/RX through a `org.neo4j.ogm.config.Configuration` bean is not supported, instead of, all configuration of the driver goes through https://github.com/neo4j/neo4j-java-driver-spring-boot-starter[our new starter].
Any of https://github.com/neo4j/neo4j-java-driver-spring-boot-starter/blob/master/docs/configuration-options.adoc[those properties] can be configured through standard Spring Boot means.
You will especially have to adapt the properties for the url and authentication, see <<migrating-auth>>
* Configuring SDN through a `org.neo4j.ogm.config.Configuration` bean is not supported, instead of, all configuration of the driver goes through our new Java driver starter.
You will especially have to adapt the properties for the url and authentication, see <<migrating-auth>>
TIP: You cannot configure SDN/RX through XML.
In case you did this with your SDN+OGM application, make sure you learn about annotation-driven or functional configuration of Spring Applications.
The easiest choice these days is Spring Boot.
With our starter in place, all the necessary bits apart from the connection URL and the authentication is already configured for you.
TIP: You cannot configure SDN through XML.
In case you did this with your SDN+OGM application, make sure you learn about annotation-driven or functional configuration of Spring Applications.
The easiest choice these days is Spring Boot.
With our starter in place, all the necessary bits apart from the connection URL and the authentication is already configured for you.
[source,properties]
[[migrating-auth]]
@@ -84,37 +80,37 @@ spring.data.neo4j.username=neo4j
spring.data.neo4j.password=secret
# New
org.neo4j.driver.uri=bolt://localhost:7687
org.neo4j.driver.authentication.username=neo4j
org.neo4j.driver.authentication.password=secret
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=secret
----
WARNING: Those new properties might change in the future again when SDN/RX and the driver will eventually replace the old setup fully.
WARNING: Those new properties might change in the future again when SDN and the driver will eventually replace the old setup fully.
And finally, add the new dependency, see <<getting-started>> for both Gradle and Maven.
You're than ready to replace annotations:
[cols="2*", options="header"]
[cols="2*",options="header"]
|===
|Old
|New
|`org.neo4j.ogm.annotation.NodeEntity`
|`org.neo4j.springframework.data.core.schema.Node`
|`org.springframework.data.neo4j.core.schema.Node`
|`org.neo4j.ogm.annotation.GeneratedValue`
|`org.neo4j.springframework.data.core.schema.GeneratedValue`
|`org.springframework.data.neo4j.core.schema.GeneratedValue`
|`org.neo4j.ogm.annotation.Id`
|`org.neo4j.springframework.data.core.schema.Id`
|`org.springframework.data.neo4j.core.schema.Id`
|`org.neo4j.ogm.annotation.Property`
|`org.neo4j.springframework.data.core.schema.Property`
|`org.springframework.data.neo4j.core.schema.Property`
|`org.neo4j.ogm.annotation.Relationship`
|`org.neo4j.springframework.data.core.schema.Relationship`
|`org.springframework.data.neo4j.core.schema.Relationship`
|`org.springframework.data.neo4j.annotation.EnableBookmarkManagement`
|No replacement, not needed
@@ -124,14 +120,13 @@ You're than ready to replace annotations:
|===
NOTE: Several Neo4j-OGM annotations have not yet a corresponding annotation in SDN/RX, some will never have.
We will add to the list above as we support additional features.
NOTE: Several Neo4j-OGM annotations have not yet a corresponding annotation in SDN, some will never have.
We will add to the list above as we support additional features.
=== Bookmarkmanagement
Both `@EnableBookmarkManagement` and `@UseBookmark` as well as the `org.springframework.data.neo4j.bookmark.BookmarkManager`
interface and it's only implementation `org.springframework.data.neo4j.bookmark.CaffeineBookmarkManager` are gone and are
not needed anymore.
interface and it's only implementation `org.springframework.data.neo4j.bookmark.CaffeineBookmarkManager` are gone and are not needed anymore.
SDN/RX uses Bookmarks for all transactions, without configuration.
SDN uses Bookmarks for all transactions, without configuration.
You can remove the bean declaration of `CaffeineBookmarkManager` as well as the the dependency to `com.github.ben-manes.caffeine:caffeine`.

View File

@@ -1,13 +1,11 @@
[[neo4j-client]]
= Neo4jClient
Spring Data Neo4j⚡RX comes with a Neo4j Client, providing a thin layer on top of Neo4j's Java driver.
Spring Data Neo4j comes with a Neo4j Client, providing a thin layer on top of Neo4j's Java driver.
While the https://github.com/neo4j/neo4j-java-driver[plain Java driver] is a very versatile tool
providing an asynchronous API in addition to the imperative and reactive versions, it doesn't
integrate with Spring application level transactions.
While the https://github.com/neo4j/neo4j-java-driver[plain Java driver] is a very versatile tool providing an asynchronous API in addition to the imperative and reactive versions, it doesn't integrate with Spring application level transactions.
SDN/RX uses the driver through the concept of a idiomatic client as directly as possible.
SDN uses the driver through the concept of a idiomatic client as directly as possible.
The client has the following main goals
@@ -16,14 +14,14 @@ The client has the following main goals
. Provide a consistent API for both imperative and reactive scenarios
. Don't add any mapping overhead
SDN/RX relies on all those features and uses them to fulfill its entity mapping features.
SDN relies on all those features and uses them to fulfill its entity mapping features.
Have a look at the <<sdn-rx-building-blocks, SDN/RX building blocks>> for where both the imperative and reactive Neo4 clients are positioned in our stack.
Have a look at the <<sdn-building-blocks, SDN building blocks>> for where both the imperative and reactive Neo4 clients are positioned in our stack.
The Neo4j Client comes in two flavors:
* `org.neo4j.springframework.data.core.Neo4jClient`
* `org.neo4j.springframework.data.core.ReactiveNeo4jClient`
* `org.springframework.data.neo4j.core.Neo4jClient`
* `org.springframework.data.neo4j.core.ReactiveNeo4jClient`
While both versions provide an API using the same vocabulary and syntax, they are not API compatible.
Both versions feature the same, fluent API to specify queries, bind parameters and extract results.
@@ -37,8 +35,7 @@ Interactions with a Neo4j Client usually ends with a call to
* `fetch().all()`
* `run()`
The imperative version will interact at this moment with the database
and get the requested results or summary, wrapped in a `Optional<>` or a `Collection`.
The imperative version will interact at this moment with the database and get the requested results or summary, wrapped in a `Optional<>` or a `Collection`.
The reactive version will in contrast return a publisher of the requested type.
Interaction with the database and retrieval of the results will not happen until the publisher is subscribed to.
@@ -46,7 +43,7 @@ The publisher can only be subscribed once.
== Getting an instance of the client
As with most things in SDN/RX, both clients depend on a configured driver instance.
As with most things in SDN, both clients depend on a configured driver instance.
[[neo4j-client-create-imperative-client]]
[source,java]
@@ -56,7 +53,7 @@ import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.springframework.data.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jClient;
public class Demo {
@@ -80,7 +77,7 @@ import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.springframework.data.core.ReactiveNeo4jClient;
import org.springframework.data.neo4j.core.ReactiveNeo4jClient;
public class Demo {
@@ -98,16 +95,14 @@ NOTE: Make sure you use the same driver instance for the client as you used for
in case you have enabled transactions.
The client won't be able to synchronize transactions if you use another instance of a driver.
Our Spring Boot starter provide a ready to use bean of the Neo4j Client that fit the environment (imperative or reactive)
and you usually don't have to configure your own instance.
Our Spring Boot starter provide a ready to use bean of the Neo4j Client that fit the environment (imperative or reactive) and you usually don't have to configure your own instance.
== Usage
[[neo4j-client-selecting-the-target-database]]
=== Selecting the target database
The Neo4j client is well prepared to be used with the multidatabase features of Neo4j 4.0.
The client uses the default database unless you specify otherwise.
The Neo4j client is well prepared to be used with the multidatabase features of Neo4j 4.0. The client uses the default database unless you specify otherwise.
The fluent API of the client allows to specify the target database exactly once, after the declaration of the query to execute.
<<neo4j-client-reactive-selecting-the-target-database>> demonstrates it with the reactive client:
@@ -148,8 +143,7 @@ Both reactive and imperative client offer
`first()`:: Expect results and return the first record
`all()`:: Retrieve all records returned
The imperative client returns `Optional<T>` and `Collection<T>` respectively,
while the reactive client returns `Mono<T>` and `Flux<T>`, the later one being executed only if subscribed to.
The imperative client returns `Optional<T>` and `Collection<T>` respectively, while the reactive client returns `Mono<T>` and `Flux<T>`, the later one being executed only if subscribed to.
If you don't expect any results from your query, than use `run()` after specificity the query.
@@ -219,7 +213,7 @@ Flux<Map<String, Object>> directorAndMovies = client
<.> There's a fluent API for binding simple types.
<.> Alternatively parameters can be bound via a map of named parameters.
SDN/RX does a lot of complex mapping and it uses the same API that you can use from the client.
SDN does a lot of complex mapping and it uses the same API that you can use from the client.
You can provide a `Function<T, Map<String, Object>>` for any given domain object like an owner of bicycles in <<neo4j-client-domain-example>>
to the Neo4j Client to map those domain objects to parameters the driver can understand.
@@ -318,8 +312,7 @@ Mono<Director> lily = client
=== Interacting directly with the driver while using managed transactions
In case you don't want or don't like the opinionated "client" approach of the `Neo4jClient` or the `ReactiveNeo4jClient`,
you can have the client delegate all interactions with the database to your code.
In case you don't want or don't like the opinionated "client" approach of the `Neo4jClient` or the `ReactiveNeo4jClient`, you can have the client delegate all interactions with the database to your code.
The interaction after the delegation is slightly different with the imperative and reactive versions of the client.
The imperative version takes in a `Function<StatementRunner, Optional<T>>` as a callback.

View File

@@ -9,7 +9,7 @@
= Query creation
This chapter is about the technical creation of queries when using SDN/RX's abstraction layers.
This chapter is about the technical creation of queries when using SDN's abstraction layers.
There will be some simplifications because we do not discuss every possible case but stick with the general idea behind it.
== Save
@@ -38,16 +38,15 @@ A save operation call in general issues multiple statements against the database
. For the next defined relationship on the root entity start with 2. but replace _first_ with _next_.
NOTE: As you can see SDN/RX does its best to keep your graph model in sync with the Java world.
This is one of the reasons why we really advise you to not load, manipulate and save sub-graphs
as this might cause relationships to get removed from the database.
NOTE: As you can see SDN does its best to keep your graph model in sync with the Java world.
This is one of the reasons why we really advise you to not load, manipulate and save sub-graphs as this might cause relationships to get removed from the database.
=== Multiple entities
The `save` operation is overloaded with the functionality for accepting multiple entities of the same type.
If you are working with generated id values or make use of optimistic locking, every entity will result in a separate `CREATE` call.
In other cases SDN/RX will create a parameter list with the entity information and provide it with a `MERGE` call.
In other cases SDN will create a parameter list with the entity information and provide it with a `MERGE` call.
`UNWIND ${neo4jEntities} AS entity MERGE (n:Person {customId: entity.${neo4jId}}) SET n = entity.{neo4jProperties} RETURN collect(n.customId) AS ${neo4jIds}`
@@ -64,7 +63,7 @@ It will match all nodes with the label of the type you queried for and does a fi
`MATCH (n:Person) WHERE id(n) = 1364`
If there is a custom id provided SDN/RX will use the property you have defined as the id.
If there is a custom id provided SDN will use the property you have defined as the id.
`MATCH (n:Person) WHERE n.customId = 'anId'`
@@ -83,5 +82,5 @@ The above return part will then look like:
`RETURN n{.first_name, ..., Person_Has_Hobby: [(n)-[:Has]->(n_hobbies:Hobby)|n_hobbies{{neo4jInternalId}: id(n_hobbies), .name, __nodeLabels__: labels(n_hobbies)}]}`
The map projection and pattern comprehension used by SDN/RX ensures that only the properties and relationships you have defined are getting queried.
The map projection and pattern comprehension used by SDN ensures that only the properties and relationships you have defined are getting queried.

View File

@@ -1,38 +0,0 @@
[[repository-query-keywords]]
= Repository query keywords
The following table lists the keywords supported by the Spring Data Neo4j⚡RX repository query derivation mechanism.
.Query keywords
[options="header", cols="1,3"]
|===============
|Logical keyword|Keyword expressions
|`AND`|`And`
|`OR`|`Or`
|`AFTER`|`After`, `IsAfter`
|`BEFORE`|`Before`, `IsBefore`
|`CONTAINING`|`Containing`, `IsContaining`, `Contains`
|`BETWEEN`|`Between`, `IsBetween`
|`ENDING_WITH`|`EndingWith`, `IsEndingWith`, `EndsWith`
|`EXISTS`|`Exists`
|`FALSE`|`False`, `IsFalse`
|`GREATER_THAN`|`GreaterThan`, `IsGreaterThan`
|`GREATER_THAN_EQUALS`|`GreaterThanEqual`, `IsGreaterThanEqual`
|`IN`|`In`, `IsIn`
|`IS`|`Is`, `Equals`, (or no keyword)
|`IS_EMPTY`|`IsEmpty`, `Empty`
|`IS_NOT_EMPTY`|`IsNotEmpty`, `NotEmpty`
|`IS_NOT_NULL`|`NotNull`, `IsNotNull`
|`IS_NULL`|`Null`, `IsNull`
|`LESS_THAN`|`LessThan`, `IsLessThan`
|`LESS_THAN_EQUAL`|`LessThanEqual`, `IsLessThanEqual`
|`LIKE`|`Like`, `IsLike`
|`NEAR`|`Near`, `IsNear`
|`NOT`|`Not`, `IsNot`
|`NOT_IN`|`NotIn`, `IsNotIn`
|`NOT_LIKE`|`NotLike`, `IsNotLike`
|`REGEX`|`Regex`, `MatchesRegex`, `Matches`
|`STARTING_WITH`|`StartingWith`, `IsStartingWith`, `StartsWith`
|`TRUE`|`True`, `IsTrue`
|`WITHIN`|`Within`, `IsWithin`
|===============

View File

@@ -1,43 +0,0 @@
[[repository-query-return-types]]
= Repository query return types
The following table lists the return types generally supported by SDN/RX repositories.
.Query return types
[options="header", cols="1,3"]
|===============
|Return type|Description
|`void`|Denotes no return value.
|Primitives|Java primitives.
|Wrapper types|Java wrapper types.
|`T`|An unique entity.
Expects the query method to return one result at most. If no result is found, `null` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`.
|`Iterator<T>`|An `Iterator`.
|`Collection<T>`|A `Collection`.
|`List<T>`|A `List`.
|`Optional<T>`|A Java 8 or Guava `Optional`.
Expects the query method to return one result at most. If no result is found, `Optional.empty()` or `Optional.absent()` is returned. More than one result triggers an `IncorrectResultSizeDataAccessException`.
|`Option<T>`|Either a Scala or Vavr `Option` type.
Semantically the same behavior as Java 8's `Optional`, described earlier.
|`Stream<T>`|A Java 8 `Stream`.
|`Streamable<T>`|A convenience extension of `Iterable` that directy exposes methods to stream, map and filter results, concatenate them etc.
|Types that implement `Streamable` and take a `Streamable` constructor or factory method argument|Types that expose a constructor or `….of(…)`/`….valueOf(…)` factory method taking a `Streamable` as argument.
See <<repositories.collections-and-iterables.streamable-wrapper>> for details.
|Vavr `Seq`, `List`, `Map`, `Set`|Vavr collection types. See <<repositories.collections-and-iterables.vavr>> for details.
|`Future<T>`|A `Future`.
Expects a method to be annotated with `@Async` and requires Spring's asynchronous method execution capability to be enabled.
|`CompletableFuture<T>`|A Java 8 `CompletableFuture`.
Expects a method to be annotated with `@Async` and requires Spring's asynchronous method execution capability to be enabled.
|`ListenableFuture`|A `org.springframework.util.concurrent.ListenableFuture`.
Expects a method to be annotated with `@Async` and requires Spring's asynchronous method execution capability to be enabled.
|`Slice`|A sized chunk of data with an indication of whether there is more data available.
Requires a `Pageable` method parameter.
|`Page<T>`|A `Slice` with additional information, such as the total number of results.
Requires a `Pageable` method parameter.
|`Mono<T>`|A Project Reactor `Mono` emitting zero or one element using reactive repositories.
Expects the query method to return one result at most.
If no result is found, `Mono.empty()` is returned.
More than one result triggers an `IncorrectResultSizeDataAccessException`.
|`Flux<T>`|A Project Reactor `Flux` emitting zero, one, or many elements using reactive repositories.
Queries returning `Flux` can emit also an infinite number of elements.
|===============

View File

@@ -1 +0,0 @@
include::sdc-auditing.adoc[leveloffset=+1]

View File

@@ -1,71 +0,0 @@
[[auditing]]
= Auditing
[[auditing.basics]]
== Basics
Spring Data provides sophisticated support to transparently keep track of who created or changed an entity and when the change happened.
To benefit from that functionality, you have to equip your entity classes with auditing metadata that can be defined either using annotations or by implementing an interface.
[[auditing.annotations]]
=== Annotation-based Auditing Metadata
We provide `@CreatedBy` and `@LastModifiedBy` to capture the user who created or modified the entity as well as `@CreatedDate` and `@LastModifiedDate` to capture when the change happened.
.An audited entity
====
[source, java]
----
class Customer {
@CreatedBy
private User user;
@CreatedDate
private DateTime createdDate;
// … further properties omitted
}
----
====
As you can see, the annotations can be applied selectively, depending on which information you want to capture.
The annotations capturing when changes were made can be used on properties of type Joda-Time, `DateTime`, legacy Java `Date` and `Calendar`, JDK8 date and time types, and `long` or `Long`.
[[auditing.interfaces]]
=== Interface-based Auditing Metadata
In case you do not want to use annotations to define auditing metadata, you can let your domain class implement the `Auditable` interface.
It exposes setter methods for all of the auditing properties.
There is also a convenience base class, `AbstractAuditable`, which you can extend to avoid the need to manually implement the interface methods
Doing so increases the coupling of your domain classes to Spring Data, which might be something you want to avoid.
Usually, the annotation-based way of defining auditing metadata is preferred as it is less invasive and more flexible.
[[auditing.auditor-aware]]
=== `AuditorAware`
In case you use either `@CreatedBy` or `@LastModifiedBy`, the auditing infrastructure somehow needs to become aware of the current principal.
To do so, we provide an `AuditorAware<T>` SPI interface that you have to implement to tell the infrastructure who the current user or system interacting with the application is.
The generic type `T` defines what type the properties annotated with `@CreatedBy` or `@LastModifiedBy` have to be.
The following example shows an implementation of the interface that uses Spring Security's `Authentication` object:
.Implementation of AuditorAware based on Spring Security
====
[source, java]
----
class SpringSecurityAuditorAware implements AuditorAware<User> {
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
----
====
The implementation accesses the `Authentication` object provided by Spring Security and looks up the custom `UserDetails` instance
that you have created in your `UserDetailsService` implementation.
We assume here that you are exposing the domain user through the `UserDetails` implementation but that, based on the `Authentication` found, you could also look it up from anywhere.

View File

@@ -1,27 +1,25 @@
[[faq]]
= Frequently Asked Questions
Here are a couple of more frequently asked question in addition to the ones in the <<what-is-sdn-rx,preface>>.
Here are a couple of more frequently asked question in addition to the ones in the <<what-is-sdn,preface>>.
== Neo4j 4.0 supports multiple databases - How can I use them?
You can either statically configure the database name or run your own database name provider.
Bear in mind that SDN/RX will not create the databases for you.
Bear in mind that SDN will not create the databases for you.
You can do this with the help of a https://github.com/michael-simons/neo4j-migrations[migrations tool]
or of course with a simple script upfront.
=== Statically configured
Configure the database name to use in your Spring Boot configuration like this
(The same property applies of course for YML or environment based configuration, with Spring Boots conventions applied):
Configure the database name to use in your Spring Boot configuration like this (The same property applies of course for YML or environment based configuration, with Spring Boots conventions applied):
[source,properties]
----
org.neo4j.data.database = yourDatabase
----
With that configuration in place, all queries generated by all instances of SDN/RX repositories (both reactive and imperative)
and by the `ReactiveNeo4jTemplate` respectively `Neo4jTemplate` will be executed against the database `yourDatabase`.
With that configuration in place, all queries generated by all instances of SDN repositories (both reactive and imperative) and by the `ReactiveNeo4jTemplate` respectively `Neo4jTemplate` will be executed against the database `yourDatabase`.
=== Dynamically configured
@@ -34,18 +32,17 @@ Here is a working example for an imperative application secured with Spring Secu
[[faq.databaseSelectionProvider]]
.Neo4jConfig.java
----
include::../../examples/multi-database/src/main/java/org/neo4j/springframework/data/examples/spring_boot/Neo4jConfig.java[tags=faq.multidatabase]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/Neo4jConfig.java[tags=faq.multidatabase]
----
NOTE: Be careful that you don't mix up entities retrieved from one database with another database.
The database name is requested for each new transaction, so you might end up with less or more
entities than expected when changing the database name in between calls.
Or worse, you could inevitable store the wrong entities in the wrong database.
The database name is requested for each new transaction, so you might end up with less or more entities than expected when changing the database name in between calls.
Or worse, you could inevitable store the wrong entities in the wrong database.
== Do I need specific configuration so that transactions work seamless with a Neo4j Causal Cluster?
No, you don't.
SDN/RX uses Neo4j Causal Cluster bookmarks internally without any configuration on your side required.
SDN uses Neo4j Causal Cluster bookmarks internally without any configuration on your side required.
Transactions in the same thread or the same reactive stream following each other will be able to read their previously changed values as you would expect.
== Do I need to use Neo4j specific annotations?
@@ -53,20 +50,20 @@ Transactions in the same thread or the same reactive stream following each other
No.
You are free to use the following, equivalent Spring Data annotations:
[cols="4*", options="header"]
[cols="4*",options="header"]
|===
|SDN/RX Neo4j specific annotation
|SDN specific annotation
|Spring Data common annotation
|Purpose
|Difference
|`org.neo4j.springframework.data.core.schema.Id`
|`org.springframework.data.neo4j.core.schema.Id`
|`org.springframework.data.annotation.Id`
|Marks the annotated attribute as the unique id.
|Specific annotation has no additional features.
|`org.neo4j.springframework.data.core.schema.Node`
|`org.springframework.data.neo4j.core.schema.Node`
|`org.springframework.data.annotation.Persistent`
|Marks the class as persistent entity.
|`@Node` allows customizing the labels
@@ -80,7 +77,7 @@ See this https://medium.com/neo4j/neo4j-ogm-and-spring-data-neo4j-a55a866df68c[b
== How do I use externally generated ids?
We provide the interface `org.neo4j.springframework.data.core.schema.IdGenerator`.
We provide the interface `org.springframework.data.neo4j.core.schema.IdGenerator`.
Implement it anyway you want and configure your implementation like this:
[source,java]
@@ -117,8 +114,7 @@ NOTE: Setters are not required on non-final fields for the id.
== Do I have to create repositories for each domain class?
No.
Have a look at the <<sdn-rx-building-blocks, SDN/RX building blocks>> and
find the `Neo4jTemplate` respectively the `ReactiveNeo4jTemplate.`
Have a look at the <<sdn-building-blocks, SDN building blocks>> and find the `Neo4jTemplate` respectively the `ReactiveNeo4jTemplate.`
Those templates know your domain and provide all necessary basic CRUD methods for retrieving, writing and counting entities.
@@ -128,7 +124,7 @@ This is our canonical movie example with the imperative template:
[[imperative-template-example]]
.TemplateExampleTest.java
----
include::../../examples/imperative-web/src/test/java/org/neo4j/springframework/data/examples/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java[tags=faq.template-imperative]
----
And here is the reactive version, omitting the setup for brevity:
@@ -137,27 +133,26 @@ And here is the reactive version, omitting the setup for brevity:
[[reactive-template-example]]
.ReactiveTemplateExampleTest.java
----
include::../../examples/reactive-web/src/test/java/org/neo4j/springframework/data/examples/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/ReactiveTemplateExampleTest.java[tags=faq.template-reactive]
----
== How do I specify parameters in custom queries?
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);
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);
[source,java,indent=0]
[[custom-queries-with-parameters]]
.ARepository.java
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/domain_events/ARepository.java[tags=standard-parameter]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java[tags=standard-parameter]
----
<.> Here we are referring to the parameter by its name.
You can also use `$0` etc. instead.
You can also use `$0` etc. instead.
NOTE: You need to compile your Java 8+ project with `-parameters` to make named parameters
work without further annotations. The Spring Boot Maven and Gradle plugins do this
automatically for you. If this is not feasible for any reason, you can either add
`@Param` and specify the name explicitly or use the parameters index.
NOTE: You need to compile your Java 8+ project with `-parameters` to make named parameters work without further annotations.
The Spring Boot Maven and Gradle plugins do this automatically for you.
If this is not feasible for any reason, you can either add
`@Param` and specify the name explicitly or use the parameters index.
== How do I use Spring Expression Language in custom queries?
@@ -170,7 +165,7 @@ The following example basically defines the same query as above, but uses a `WHE
[[custom-queries-with-spel]]
.ARepository.java
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/domain_events/ARepository.java[tags=spel]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/repositories/domain_events/ARepository.java[tags=spel]
----
The SpEL blocked starts with `:#{` and than refers to the given `String` parameters by name (`#pt1`).
@@ -190,11 +185,10 @@ Those are
== How do I use "Find by example"?
"Find by example" is a new feature in SDN/RX.
"Find by example" is a new feature in SDN.
You instantiate an entity or use an existing one.
With this instance you create an `org.springframework.data.domain.Example`.
If your repository extends `org.neo4j.springframework.data.repository.Neo4jRepository` or `org.neo4j.springframework.data.repository.ReactiveNeo4jRepository`,
you can immediately use the available `findBy` methods taking in an example, like shown in <<find-by-example-example>>
If your repository extends `org.springframework.data.neo4j.repository.Neo4jRepository` or `org.springframework.data.neo4j.repository.ReactiveNeo4jRepository`, you can immediately use the available `findBy` methods taking in an example, like shown in <<find-by-example-example>>
[source,java]
[[find-by-example-example]]

View File

@@ -1,14 +1,13 @@
[[getting-started]]
= Getting started
We provide a Spring Boot starter for SDN/RX.
We provide a Spring Boot starter for SDN.
Please include the starter module via your dependency management and configure the bolt URL to use, for example `org.neo4j.driver.uri=bolt://localhost:7687`.
The starter assumes that the server has disabled authentication.
As the SDN/RX starter depends on the starter for the Java Driver, all things regarding configuration said there, apply here as well.
For a reference of the available properties, use your IDEs autocompletion in the `org.neo4j.driver` namespace
or look at the link:{java-driver-starter-href}/blob/master/docs/manual.adoc[dedicated manual].
As the SDN starter depends on the starter for the Java Driver, all things regarding configuration said there, apply here as well.
For a reference of the available properties, use your IDEs autocompletion in the `org.neo4j.driver` namespace or look at the link:{java-driver-starter-href}/blob/master/docs/manual.adoc[dedicated manual].
SDN/RX supports
SDN supports
* The well known and understood imperative programming model (much like Spring Data JDBC or JPA)
* Reactive programming based on https://www.reactive-streams.org[Reactive Streams], including full support for https://spring.io/blog/2019/05/16/reactive-transactions-with-spring[reactive transactions].
@@ -19,8 +18,7 @@ Have a look at the link:{gh-base}/tree/master/examples[examples directory] for a
== Prepare the database
For this example, we stay within the https://neo4j.com/developer/movie-database/[movie graph],
as it comes for free with every Neo4j instance.
For this example, we stay within the https://neo4j.com/developer/movie-database/[movie graph], as it comes for free with every Neo4j instance.
If you don't have a running database but Docker installed, please run:
@@ -39,11 +37,10 @@ Execute it to fill your database with some test data.
== Create a new Spring Boot project
The easiest way to setup a Spring Boot project is https://start.spring.io[start.spring.io]
(which is integrated in the major IDEs as well, in case you don't want to use the website).
(which is integrated in the major IDEs as well, in case you don't want to use the website).
Select the "Spring Web Starter" to get all the dependencies needed for creating a Spring based web application.
The Spring Initializr will take care of creating a valid project structure for you,
with all the files and settings in place for the selected build tool.
The Spring Initializr will take care of creating a valid project structure for you, with all the files and settings in place for the selected build tool.
WARNING: Don't choose Spring Data Neo4j here, as it will get you the previous generation of Spring Data Neo4j including OGM and additional abstraction over the driver.
@@ -69,12 +66,12 @@ As this starter is not yet on the initializer, you will have to add the followin
[source,xml,subs="verbatim,attributes"]
[[dependencies-maven]]
.Inclusion of the spring-data-neo4j-rx-spring-boot-starter in a Maven project
.Inclusion of the spring-data-neo4j-spring-boot-starter in a Maven project
----
<dependency>
<groupId>{groupId}</groupId>
<artifactId>{artifactIdStarter}</artifactId>
<version>{spring-data-neo4j-rx-version}</version>
<version>{version}</version>
</dependency>
----
@@ -99,10 +96,10 @@ curl https://start.spring.io/starter.tgz \
The dependency for Gradle looks like this and must be added to `build.gradle`:
[source,groovy,subs="verbatim,attributes"]
.Inclusion of the spring-data-neo4j-rx-spring-boot-starter in a Gradle project
.Inclusion of the spring-data-neo4j-spring-boot-starter in a Gradle project
----
dependencies {
implementation '{groupId}:{artifactIdStarter}:{spring-data-neo4j-rx-version}'
implementation '{groupId}:{artifactIdStarter}:{version}'
}
----
@@ -123,7 +120,7 @@ org.neo4j.driver.authentication.password=secret
This is the bare minimum of what you need to connect to a Neo4j instance.
NOTE: It is not necessary to add any programmatically configuration of the driver when you use this starter.
SDN/RX repositories will be automatically enabled by this starter.
SDN repositories will be automatically enabled by this starter.
== Create your domain
@@ -134,45 +131,42 @@ Our domain layer should accomplish two things:
=== Example Node-Entity
SDN/RX fully supports unmodifiable entities, for both Java and `data` classes in Kotlin.
SDN fully supports unmodifiable entities, for both Java and `data` classes in Kotlin.
Therefor we will focus on immutable entities here, <<movie-entity>> shows a such an entity.
NOTE: SDN/RX supports all data types the Neo4j Java Driver supports,
see https://neo4j.com/docs/driver-manual/current/cypher-values/#driver-neo4j-type-system[Map Neo4j types to native language types] inside the chapter "The Cypher type system".
Future versions will support additional converters.
NOTE: SDN supports all data types the Neo4j Java Driver supports, see https://neo4j.com/docs/driver-manual/current/cypher-values/#driver-neo4j-type-system[Map Neo4j types to native language types] inside the chapter "The Cypher type system".
Future versions will support additional converters.
[source,java]
[[movie-entity]]
.MovieEntity.java
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/domain/MovieEntity.java[tags=mapping.annotations]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java[tags=mapping.annotations]
----
<.> `@Node` is used to mark this class as a managed entity.
It also is used to configure the Neo4j label.
The label defaults to the name of the class, if you're just using plain `@Node`.
It also is used to configure the Neo4j label.
The label defaults to the name of the class, if you're just using plain `@Node`.
<.> Each entity has to have an id.
The movie class shown here uses the attribute `title` as a unique business key.
If you don't have such a unique key, you can use the combination of `@Id` and `@GeneratedValue`
to configure SDN/RX to use Neo4j's internal id.
We also provide generators for UUIDs.
The movie class shown here uses the attribute `title` as a unique business key.
If you don't have such a unique key, you can use the combination of `@Id` and `@GeneratedValue`
to configure SDN to use Neo4j's internal id.
We also provide generators for UUIDs.
<.> This shows `@Property` as a way to use a different name for the field than for the graph property.
<.> This defines a relationship to a class of type `PersonEntity` and the relationship type `ACTED_IN`
<.> This is the constructor to be used by your application code.
As a general remark: Immutable entities using internally generated ids are a bit contradictory,
as SDN/RX needs a way to set the field with the value generated by the database.
As a general remark: Immutable entities using internally generated ids are a bit contradictory, as SDN needs a way to set the field with the value generated by the database.
If you don't find a good business key or don't want to use a generator for IDs, here's the same entity using
the internally generated id together with a businesses constructor and a so called _wither_-Method, that is used by SDN/RX:
If you don't find a good business key or don't want to use a generator for IDs, here's the same entity using the internally generated id together with a businesses constructor and a so called _wither_-Method, that is used by SDN:
[source,java]
[[movie-entity-with-wither]]
.MovieEntity.java
----
import org.neo4j.springframework.data.core.schema.GeneratedValue;
import org.neo4j.springframework.data.core.schema.Id;
import org.neo4j.springframework.data.core.schema.Node;
import org.neo4j.springframework.data.core.schema.Property;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.annotation.PersistenceConstructor;
@@ -204,21 +198,18 @@ public class MovieEntity {
}
}
----
<.> This is the constructor to be used by your application code.
It sets the id to null, as the field containing the internal id should never be manipulated.
<.> This is the constructor to be used by your application code.
It sets the id to null, as the field containing the internal id should never be manipulated.
<.> This is a so-called _wither_ for the `id`-attribute.
It creates a new entity and sets the field accordingly,
without modifying the original entity, thus making it immutable.
It creates a new entity and sets the field accordingly, without modifying the original entity, thus making it immutable.
You can of course use SDN/RX with https://kotlinlang.org/[Kotlin] and model your domain with Kotlin's data classes.
You can of course use SDN with https://kotlinlang.org/[Kotlin] and model your domain with Kotlin's data classes.
https://projectlombok.org/[Project Lombok] is an alternative if you want or need to stay purely within Java.
=== Declaring Spring Data repositories
You basically have two options here:
You can work store agnostic with SDN/RX and make your domain specific extends one of
You can work store agnostic with SDN and make your domain specific extends one of
* `org.springframework.data.repository.Repository`
* `org.springframework.data.repository.CrudRepository`
@@ -227,9 +218,8 @@ You can work store agnostic with SDN/RX and make your domain specific extends on
Choose imperative and reactive accordingly.
WARNING: While technically not prohibited, it is not recommended to mix imperative and reactive database access
in the same application.
We won't support you with scenarios like this.
WARNING: While technically not prohibited, it is not recommended to mix imperative and reactive database access in the same application.
We won't support you with scenarios like this.
The other option is to settle on a store specific implementation and gain all the methods we support out of the box.
The advantage of this approach is also it's biggest disadvantage: Once out, all those methods will be part of your API.
@@ -243,17 +233,8 @@ A repository fitting to any of the movie entities above looks like this:
[[movie-repository]]
.MovieRepository.java
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/domain/MovieRepository.java[tags=getting.started]
----
This repository can be used in any Spring component like this:
[source,java]
[[movie-controller]]
.MovieController.java
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/web/MovieController.java[tags=getting.started]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/MovieRepository.java[tags=getting.started]
----
TIP: Testing reactive code is done with a `reactor.test.StepVerifier`.
Have a look at the corresponding https://projectreactor.io/docs/core/release/reference/#testing[documentation of Project Reactor] or see our example code.
Have a look at the corresponding https://projectreactor.io/docs/core/release/reference/#testing[documentation of Project Reactor] or see our example code.

View File

@@ -1,4 +1,4 @@
= Spring Data Neo4jRX
= Spring Data Neo4j
Gerrit Meier <gerrit.meier@neo4j.com>; Michael Simons <michael.simons@neo4j.com>
:toc:
:doctype: book
@@ -10,32 +10,35 @@ Gerrit Meier <gerrit.meier@neo4j.com>; Michael Simons <michael.simons@neo4j.com>
:sectanchors: true
:numbered: true
:xrefstyle: short
:revnumber: {version}
:revdate: {localdate}
ifndef::manualIncludeDir[]
:manualIncludeDir: ../
:manualIncludeDir: ../../../
endif::[]
include::{manualIncludeDir}/README.adoc[tags=properties]
:copyright: 2020 Neo4j, Inc.,
:gh-base: https://github.com/neo4j/sdn-rx
:gh-base: https://github.com/neo4j/SDN
:java-driver-starter-href: https://github.com/neo4j/neo4j-java-driver-spring-boot-starter
:springVersion: 5.2.0.RELEASE
:spring-framework-docs: https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference
:spring-framework-javadoc: https://docs.spring.io/spring/docs/{springVersion}/javadoc-api
:spring-data-commons-docs: ../../../../../other-spring-data/spring-data-commons/src/main/asciidoc/
(C) 2008-2020 The original authors.
(C) {copyright}
NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
License: link:license.html[Creative Commons 4.0]
[[preface]]
= Preface
[abstract]
--
This is the SDN/RX manual version {spring-data-neo4j-rx-version}.
include::introduction-and-preface/index.adoc[]
include::{spring-data-commons-docs}/dependencies.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repositories.adoc[leveloffset=+1]
It contains excerpts of the shared https://docs.spring.io/spring-data/commons/docs/current/reference/html[Spring Data Commons documentation],
adapted to contain only supported features and annotation.
--
[[reference]]
= Reference Documentation
_Who should read this?_
@@ -44,18 +47,22 @@ This manual is written for:
* the enterprise architect investigating Spring integration for Neo4j.
* the engineer developing Spring Data based applications with Neo4j.
include::introduction-and-preface/index.adoc[]
include::getting-started/index.adoc[]
include::object-mapping/index.adoc[]
include::repositories/index.adoc[]
include::{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+1]
include::testing/index.adoc[]
include::auditing/index.adoc[]
include::{spring-data-commons-docs}/auditing.adoc[leveloffset=+1]
include::faq/index.adoc[]
include::appendix/index.adoc[]
[[appendix]]
= Appendix
:numbered!:
include::{spring-data-commons-docs}/repository-query-keywords-reference.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-query-return-types-reference.adoc[leveloffset=+1]

View File

@@ -1,19 +1,18 @@
[[building-blocks]]
= Building blocks
SDN/RX consists of compossible building blocks.
SDN consists of compossible building blocks.
It builds on top of the https://github.com/neo4j/neo4j-java-driver[Neo4j Java Driver].
The instance of the Java driver is provided by our link:{java-driver-starter-href}[starter].
All configuration options of the driver can be configured through the starter in the namespace `org.neo4j.driver`.
The driver bean provides imperative, asynchronous and reactive methods to interact with Neo4j.
You can use all transaction methods the driver provides on that bean
such as https://neo4j.com/docs/driver-manual/4.0/terminology/#term-auto-commit[auto-commit transactions],
You can use all transaction methods the driver provides on that bean such as https://neo4j.com/docs/driver-manual/4.0/terminology/#term-auto-commit[auto-commit transactions],
https://neo4j.com/docs/driver-manual/4.0/terminology/#term-auto-commit[transaction functions] and unmanaged transactions.
Be aware that those transactions are not tight to an ongoing Spring transaction.
Integration with Spring Data and Spring's platform or reactive transaction manager starts at the <<neo4j-client,Neo4j Client>>.
The client is part of SDN/RX and SDN/RX is configured through a separate starter, `{artifactIdStarter}`.
The client is part of SDN is configured through a separate starter, `{artifactIdStarter}`.
The configuration namespace of that starter is `org.neo4j.data`.
The client is mapping agnostic.
@@ -21,22 +20,21 @@ It doesn't know about your domain classes and you are responsible for mapping a
The next higher level of abstraction is the Neo4j Template.
It is aware of your domain and you can use it to query arbitrary domain objects.
The template comes in handy in scenarios with a large number of domain classes or custom queries for which you don't want
to create an additional repository abstraction each.
The template comes in handy in scenarios with a large number of domain classes or custom queries for which you don't want to create an additional repository abstraction each.
The highest level of abstraction is a Spring Data repository.
All abstractions of SDN/RX come in both imperative and reactive fashions.
All abstractions of SDN come in both imperative and reactive fashions.
It is not recommend to mix both programming styles in the same application.
The reactive infrastructure requires a Neo4j 4.0+ database.
[[sdn-rx-building-blocks]]
[ditaa, sdn-rx-buildingblocks, png]
.SDN/RX building blocks
[[sdn-building-blocks]]
[ditaa,sdn-buildingblocks,png]
.SDN building blocks
----
+---------------------------------------------------------+
| |
| SDN/RX +----------------------------+ |
| SDN +----------------------------+ |
| | | |
| | Spring Data Repositories | |
| | | |
@@ -46,7 +44,7 @@ The reactive infrastructure requires a Neo4j 4.0+ database.
| v |
| +-------------+--------------+ | +---------------------------------------+
| | | | Provides | |
| | Neo4j Template | | <------------------+ sdn-rx-spring-boot-starter |
| | Neo4j Template | | <------------------+ sdn-spring-boot-starter |
| | | | | |
| +-------------+--------------+ | +---------------------------------------+
| | |
@@ -71,5 +69,5 @@ The reactive infrastructure requires a Neo4j 4.0+ database.
The template mechanism is similar to the templates of others stores.
Find some more information about it in <<template-support,our FAQ>>.
The Neo4j Client as such is unique to SDN/RX.
The Neo4j Client as such is unique to SDN.
You will find it's documentation in the <<neo4j-client,appendix>>.

View File

@@ -1,7 +1,5 @@
[[introduction]]
= Introduction
== Your way through this document
= Your way through this document
If you already familiar with the core concepts of Spring Data, head straight to <<getting-started>>.
This chapter will walk you through different options of configuring an application to connect to a Neo4j instance and how to model your domain.
@@ -10,12 +8,12 @@ In most cases, you will need a domain.
Go to <<mapping>> to learn about how to map nodes and relationships to your domain model.
After that, you will need some means to query the domain.
Choices are <<neo4j-repositories, Neo4j repositories>>, <<neo4j-template, the Neo4j Template>> or on a lower level, <<neo4j-client, the Neo4j Client>>.
All of them are available in a <<reactive-programming,reactive fashion>> as well.
Choices are Neo4j repositories, the Neo4j Template or on a lower level, the Neo4j Client.
All of them are available in a reactive fashion as well.
Apart from the paging mechanism, all the features of standard repositories are available in the reactive variant.
You will find the building blocks in the next <<building-blocks,chapter>>.
To learn more about the general concepts of repositories, head over to <<repositories>>.
You can of course read on, continuing with the preface and a gentle getting started guide.
You can of course read on, continuing with the preface, and a gentle getting started guide.

View File

@@ -1,12 +1,8 @@
[[preface]]
= Preface
[[preface.nosql]]
== NoSQL and Graph databases
= NoSQL and Graph databases
A graph database is a storage engine that is specialized in storing and retrieving vast networks of information.
It efficiently stores data as nodes with relationships to other or even the same nodes,
thus allowing high-performance retrieval and querying of those structures.
It efficiently stores data as nodes with relationships to other or even the same nodes, thus allowing high-performance retrieval and querying of those structures.
Properties can be added to both nodes and relationships.
Nodes can be labelled by zero or more labels, relationships are always directed and named.
@@ -16,9 +12,8 @@ In most other modeling approaches, the relationships between things are reduced
Graph databases allow to keep the rich relationships that originate from the domain equally well-represented in the database without resorting to also modeling the relationships as "things".
There is very little "impedance mismatch" when putting real-life domains into a graph database.
[[preface.nosql.neo4j]]
=== Introducing Neo4j
== Introducing Neo4j
https://neo4j.com/[Neo4j] is an open source NoSQL graph database.
It is a fully transactional database (ACID) that stores data structured as graphs consisting of nodes, connected by relationships.
@@ -33,16 +28,13 @@ Here is a list of useful resources:
* https://neo4j.com/docs/ogm-manual/current/[Neo4j Object Graph Mapper (OGM) Library]
* Several https://neo4j.com/books/[books] available for purchase and https://www.youtube.com/neo4j[videos] to watch.
[[preface.spring-data]]
== Spring and Spring Data
Spring Data uses Spring Framework's https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html[core] functionality,
such as the https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#beans[IoC] container,
Spring Data uses Spring Framework's https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html[core] functionality, such as the https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#beans[IoC] container,
https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#core-convert[type conversion system],
https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#expressions[expression language],
https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/integration.html#jmx[JMX integration],
and portable https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html#dao-exceptions[DAO exception hierarchy].
https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/integration.html#jmx[JMX integration], and portable https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html#dao-exceptions[DAO exception hierarchy].
While it is not necessary to know all the Spring APIs, understanding the concepts behind them is.
At a minimum, the idea behind IoC should be familiar.
@@ -57,81 +49,70 @@ On a lower level, you can grab the Bolt driver instance, but than you have to ma
To learn more about Spring, you can refer to the comprehensive documentation that explains in detail the Spring Framework.
There are a lot of articles, blog entries and books on the matter - take a look at the Spring Framework https://spring.io/docs[home page ] for more information.
[[what-is-sdn-rx]]
== What is Spring Data Neo4jRX
[[what-is-sdn]]
== What is Spring Data Neo4j
Spring Data Neo4j⚡RX is the successor to Spring Data Neo4j + Neo4j-OGM.
The current Spring Data Neo4j is the successor to Spring Data Neo4j + Neo4j-OGM.
The separate layer of Neo4j-OGM (Neo4j Object Graph Mapper) has been replaced by Spring infrastructure, but the basic concepts of an Object Graph Mapper (OGM) still apply:
An OGM maps nodes and relationships in the graph to objects and references in a domain model.
Object instances are mapped to nodes while object references are mapped using relationships, or serialized to properties (e.g. references to a Date).
JVM primitives are mapped to node or relationship properties.
An OGM abstracts the database and provides a convenient way to persist your domain model in the graph and query it without having to use low level drivers directly.
It also provides the flexibility to the developer to supply custom queries where the queries generated by SDN/RX are insufficient.
It also provides the flexibility to the developer to supply custom queries where the queries generated by SDN are insufficient.
=== What's in the box?
Spring Data Neo4j⚡RX or in short SDN/RX is a next-generation https://spring.io/projects/spring-data[Spring Data] module,
created and maintained by https://neo4j.com[Neo4j, Inc.] in close collaboration with https://pivotal.io[Pivotal's] Spring Data Team.
Spring Data Neo4j or in short SDN is a next-generation https://spring.io/projects/spring-data[Spring Data] module, created and maintained by https://neo4j.com[Neo4j, Inc.] in close collaboration with https://pivotal.io[Pivotal's] Spring Data Team.
SDN/RX relies completely on the https://github.com/neo4j/neo4j-java-driver[Neo4j Java Driver],
without introducing another "driver" or "transport" layer between the mapping framework and the driver.
SDN relies completely on the https://github.com/neo4j/neo4j-java-driver[Neo4j Java Driver], without introducing another "driver" or "transport" layer between the mapping framework and the driver.
The Neo4j Java Driver - sometimes dubbed Bolt or the Bolt driver - is used as a protocol much like JDBC is with relational databases.
Noteworthy features that differentiate SDN/RX from Spring Data Neo4j + OGM are
Noteworthy features that differentiate the new SDN from Spring Data Neo4j + OGM are
* Full support for immutable entities and thus full support for Kotlin's data classes
* Full support for the reactive programming model in the Spring Framework itself and Spring Data
* Brand new Neo4j client and reactive client feature, resurrecting the idea of a template over the plain driver, easing database access
SDN/RX is currently developed with https://github.com/spring-projects/spring-data-neo4j[Spring Data Neo4j] in parallel and will replace it eventually when they are on feature parity in regards of repository support and mapping.
=== Why should I use SDN in favor of SDN+OGM
=== Why should I use SDN/RX in favor of SDN+OGM
SDN/RX has several features not present in SDN+OGM, notably
SDN has several features not present in SDN+OGM, notably
* Full support for Springs reactive story, including reactive transaction
* Full support for https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example[Query By Example]
* Full support for fully immutable entities
* Support for all modifiers and variations of derived finder methods, including spatial queries
=== Do I need both SDN/RX and Spring Data Neo4j?
No.
They are mutually exclusive and you cannot mix them in one project.
=== How does SDN/RX relate to Neo4j-OGM?
=== How does SDN relate to Neo4j-OGM?
https://neo4j.com/docs/ogm-manual/current/[Neo4j-OGM] is an Object Graph Mapping library, which is mainly used by Spring Data Neo4j as its backend for the heavy lifting of mapping nodes and relationships into domain object.
SDN/RX *does not need* and *does not support* Neo4j-OGM.
SDN/RX uses Spring Data's mapping context exclusively for scanning classes and building the meta model.
The new SDN *does not need* and *does not support* Neo4j-OGM.
SDN uses Spring Data's mapping context exclusively for scanning classes and building the meta model.
While this pins SDN/RX to the Spring eco systems, it has several advantages, among them the smaller footprint in regards of CPU and memory usage and especially, all the features of Springs mapping context.
While this pins SDN to the Spring eco systems, it has several advantages, among them the smaller footprint in regards of CPU and memory usage and especially, all the features of Springs mapping context.
=== Does SDN/RX support connections over HTTP to Neo4j?
=== Does SDN support connections over HTTP to Neo4j?
No.
=== Does SDN/RX support embedded Neo4j?
=== Does SDN support embedded Neo4j?
Embedded Neo4j has multiple facets to it:
==== Does SDN/RX provide an embedded instance for your application?
==== Does SDN provide an embedded instance for your application?
No.
==== Does SDN/RX interact directly with an embedded instance?
==== Does SDN interact directly with an embedded instance?
No.
An embedded database is usually represented by an instance of `org.neo4j.graphdb.GraphDatabaseService` and has no Bolt connector out of the box.
SDN/RX can however work very much with Neo4j's test harness, the test harness is specially meant to be a drop-in replacement for the real database.
SDN can however work very much with Neo4j's test harness, the test harness is specially meant to be a drop-in replacement for the real database.
Support for both Neo4j 3.5 and 4.0 test harness is implemented via link:{java-driver-starter-href}[the Spring Boot starter for the driver].
Have a look at the corresponding module `org.neo4j.driver:neo4j-java-driver-test-harness-spring-boot-autoconfigure`.
==== Can I use SDN/RX without Spring Boot?
==== Can I use SDN without Spring Boot?
Yes, see our `README`.
We provide `org.neo4j.springframework.data.config.AbstractNeo4jConfig` and `org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig` for that purpose.
We provide `org.springframework.data.neo4j.config.AbstractNeo4jConfig` and `org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig` for that purpose.

View File

@@ -10,15 +10,13 @@ It gives valuable tips on general mapping, why you should prefer immutable domai
[[mapping.annotations]]
== Metadata-based Mapping
To take full advantage of the object mapping functionality inside SDN/RX, you should annotate your mapped objects with the `@Node` annotation.
Although it is not necessary for the mapping framework to have this annotation (your POJOs are mapped correctly, even without any annotations),
it lets the classpath scanner find and pre-process your domain objects to extract the necessary metadata.
If you do not use this annotation, your application takes a slight performance hit the first time you store a domain object,
because the mapping framework needs to build up its internal metadata model so that it knows about the properties of your domain object and how to persist them.
To take full advantage of the object mapping functionality inside SDN, you should annotate your mapped objects with the `@Node` annotation.
Although it is not necessary for the mapping framework to have this annotation (your POJOs are mapped correctly, even without any annotations), it lets the classpath scanner find and pre-process your domain objects to extract the necessary metadata.
If you do not use this annotation, your application takes a slight performance hit the first time you store a domain object, because the mapping framework needs to build up its internal metadata model so that it knows about the properties of your domain object and how to persist them.
=== Mapping Annotation Overview
==== From SDN/RX:
==== From SDN:
* `@Node`: Applied at the class level to indicate this class is a candidate for mapping to the database.
* `@Id`: Applied at the field level to mark the field used for identity purpose.
@@ -29,15 +27,15 @@ because the mapping framework needs to build up its internal metadata model so t
==== From Spring Data commons
* `@org.springframework.data.annotation.Id` same as `@Id` from SDN/RX, in fact, `@Id` is annotated with Spring Data Common's Id-annotation.
* `@org.springframework.data.annotation.Id` same as `@Id` from SDN, in fact, `@Id` is annotated with Spring Data Common's Id-annotation.
* `@CreatedBy`: Applied at the field level to indicate the creator of a node.
* `@CreatedDate`: Applied at the field level to indicate the creation date of a node.
* `@LastModifiedBy`: Applied at the field level to indicate the author of the last change to a node.
* `@LastModifiedDate`: Applied at the field level to indicate the last modification date of a node.
* `@LastModifiedBy`: Applied at the field level to indicate the author of the last change to a node.
* `@LastModifiedDate`: Applied at the field level to indicate the last modification date of a node.
* `@PersistenceConstructor`: Applied at one constructor to mark it as a the preferred constructor when reading entities.
* `@Persistent`: Applied at the class level to indicate this class is a candidate for mapping to the database.
* `@Version`: Applied at field level is used for optimistic locking and checked for modification on save operations.
The initial value is zero which is bumped automatically on every update.
The initial value is zero which is bumped automatically on every update.
Have a look at <<auditing>> for all annotations regarding auditing support.
@@ -58,8 +56,7 @@ The first element in the array will considered as the primary label.
The primary label should always be the most concrete label that reflects your domain class.
For each instance of an annotated class that is written through a repository or through the Neo4j template,
one node in the graph with at least the primary label will be written.
For each instance of an annotated class that is written through a repository or through the Neo4j template, one node in the graph with at least the primary label will be written.
Vice versa, all nodes with the primary label will be mapped to the instances of the annotated class.
The `@Node` annotation is not inherited from super-types and interfaces.
@@ -79,12 +76,11 @@ If this annotation is present, all labels present on a node and not statically m
During writes, all labels of the node will be replaced with the statically defined labels plus the contents of the collection.
WARNING: If you have other applications add additional labels to nodes, don't use `@DynamicLabels`.
If `@DynamicLabels` is present on a managed entity, the resulting set of labels will be "the truth" written to the database.
If `@DynamicLabels` is present on a managed entity, the resulting set of labels will be "the truth" written to the database.
=== Identifying instances: `@Id`
While `@Node` creates a mapping between a class and nodes having a specific label,
we also need to make the connection between individual instances of that class (objects) and instances of the node.
While `@Node` creates a mapping between a class and nodes having a specific label, we also need to make the connection between individual instances of that class (objects) and instances of the node.
This is where `@Id` comes into play.
`@Id` marks an attribute of the class to be the unique identifier of the object.
@@ -97,8 +93,7 @@ Peoples names for example are seldom unique, change over time or worse, not ever
We therefore support two different kind of _surrogate keys_.
On an attribute of type `long` or `Long`, `@Id` can be used with `@GeneratedValue`.
This maps the Neo4j internal id, which is *not* a property on a node or relationship and usually not visible,
to the attribute and allows SDN/RX to retrieve individual instances of the class.
This maps the Neo4j internal id, which is *not* a property on a node or relationship and usually not visible, to the attribute and allows SDN to retrieve individual instances of the class.
`@GeneratedValue` provides the attribute `generatorClass`.
`generatorClass` can be used to specify a class implementing `IdGenerator`.
@@ -124,7 +119,7 @@ The `@Relationship` annotation can be used on all attributes that are not a simp
It is applicable on attributes of other types annotated with `@Node` or collections and maps thereof.
The `type` or the `value` attribute allow configuration of the relationship's type, `direction` allows specifying the direction.
The default direction in SDN/RX is `Relationship.Direction#OUTGOING`.
The default direction in SDN is `Relationship.Direction#OUTGOING`.
We support dynamic relationships.
Dynamic relationships are represented as a `Map<String, AnnotatedDomainClass>`.
@@ -133,7 +128,7 @@ In such a case, the type of the relationship to the other domain class is given
==== Map relationship properties
Neo4j supports defining properties not only on nodes but also on relationships.
To express those properties in the model SDN/RX provides `@RelationshipProperties` to be applied on a simple Java class.
To express those properties in the model SDN provides `@RelationshipProperties` to be applied on a simple Java class.
In the entity class the relationship can be modelled as before but its type has to be a `Map` with the related node as the key and the relation property class as value.
@@ -142,19 +137,19 @@ A relationship property class and its usage may look like this:
.Relationship properties `Roles`
[source,java]
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/domain/Roles.java[tags=mapping.relationship.properties]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/Roles.java[tags=mapping.relationship.properties]
----
.Defining relationship properties for an entity
[source,java,indent=0]
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/domain/MovieEntity.java[tags=mapping.relationship.properties]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java[tags=mapping.relationship.properties]
----
==== Relationship query limit
In general there is no limitation of relationships / hops for creating the queries.
SDN/RX parses the whole reachable graph from your modelled nodes.
SDN parses the whole reachable graph from your modelled nodes.
It is possible to have self-referencing entities and self-referencing concrete instances.
If these entities or instances for a cycle we have a strict limit of *2* repetitions of walking the same path through the graph.
@@ -170,16 +165,16 @@ We use movies and people with different roles:
====
[source,java]
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/domain/MovieEntity.java[tags=mapping.annotations]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/MovieEntity.java[tags=mapping.annotations]
----
<.> `@Node` is used to mark this class as a managed entity.
It also is used to configure the Neo4j label.
The label defaults to the name of the class, if you're just using plain `@Node`.
It also is used to configure the Neo4j label.
The label defaults to the name of the class, if you're just using plain `@Node`.
<.> Each entity has to have an id.
We use the movie's name as unique identifier.
We use the movie's name as unique identifier.
<.> This shows `@Property` as a way to use a different name for the field than for the graph property.
<.> This configures an incoming relationship to a person.
<.> This is the constructor to be used by your application code as well as by SDN/RX.
<.> This is the constructor to be used by your application code as well as by SDN.
====
People are mapped in two roles here, `actors` and `directors`.
@@ -190,17 +185,16 @@ The domain class is the same:
====
[source,java]
----
include::../../examples/reactive-web/src/main/java/org/neo4j/springframework/data/examples/spring_boot/domain/PersonEntity.java[tags=mapping.annotations]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/domain/PersonEntity.java[tags=mapping.annotations]
----
====
NOTE: We haven't modelled the relationship between movies and people in both direction.
Why is that?
We see the `MovieEntity` as the aggregate root, owning the relationships.
On the other hand, we want to be able to pull all people from the database without selecting all the movies associated with them.
Please consider your applications use case before you try to map every relationship in your database in every direction.
While you can do this, you may end up rebuilding a graph database inside your object graph and this is not the intention of a mapping framework.
Why is that?
We see the `MovieEntity` as the aggregate root, owning the relationships.
On the other hand, we want to be able to pull all people from the database without selecting all the movies associated with them.
Please consider your applications use case before you try to map every relationship in your database in every direction.
While you can do this, you may end up rebuilding a graph database inside your object graph and this is not the intention of a mapping framework.
[[mapping.id-handling]]
== Handling and provisioning of unique IDs
@@ -229,7 +223,7 @@ public class MovieEntity {
----
====
You don't need to provide a setter for the field, SDN/RX will use reflection to assign the field, but use a setter if there is one.
You don't need to provide a setter for the field, SDN will use reflection to assign the field, but use a setter if there is one.
If you want to create an immutable entity with an internally generated id, you have to provide a _wither_.
.Immutable MovieEntity with internal Neo4j id
@@ -266,7 +260,7 @@ public class MovieEntity {
<.> Public constructor, used by the application and Spring Data
<.> Internally used constructor
<.> This is a so-called _wither_ for the `id`-attribute.
It creates a new entity and set's the field accordingly, without modifying the original entity, thus making it immutable.
It creates a new entity and set's the field accordingly, without modifying the original entity, thus making it immutable.
====
You either have to provide a setter for the id attribute or something like a _wither_, if you want to have
@@ -277,8 +271,8 @@ You either have to provide a setter for the id attribute or something like a _wi
=== Use externally provided surrogate keys
The `@GeneratedValue` annotation can take a class implementing `org.neo4j.springframework.data.core.schema.IdGenerator` as parameter.
SDN/RX provides `InternalIdGenerator` (the default) and `UUIDStringGenerator` out of the box.
The `@GeneratedValue` annotation can take a class implementing `org.springframework.data.neo4j.core.schema.IdGenerator` as parameter.
SDN provides `InternalIdGenerator` (the default) and `UUIDStringGenerator` out of the box.
The later generates new UUIDs for each entity and returns them as `java.lang.String`.
An application entity using that would look like this:
@@ -301,19 +295,17 @@ We have to discuss two separate things regarding advantages and disadvantages.
The assignment itself and the UUID-Strategy.
A https://en.wikipedia.org/wiki/Universally_unique_identifier[universally unique identifier] is meant to be unique for practical purposes.
To quote Wikipedia:
“Thus, anyone can create a UUID and use it to identify something with near certainty that the identifier does not duplicate one that has already been,
or will be, created to identify something else.”
Our strategy uses Java internal UUID mechanism, employing a cryptographically strong pseudo random number generator.
“Thus, anyone can create a UUID and use it to identify something with near certainty that the identifier does not duplicate one that has already been, or will be, created to identify something else.” Our strategy uses Java internal UUID mechanism, employing a cryptographically strong pseudo random number generator.
In most cases that should work fine, but your mileage might vary.
That leaves the assignment itself:
* Advantage: The application is in full control and can generate a unique key that is just unique enough for the purpose of the application.
The generated value will be stable and there wont be a need to change it later on.
The generated value will be stable and there wont be a need to change it later on.
* Disadvantage: The generated strategy is applied on the application side of things.
In those days most applications will be deployed in more than one instance to scale nicely.
If your strategy is prone to generate duplicates than inserts will fail as uniques of the primary key will be violated.
So while you dont have to think about a unique business key in this scenario, you have to think more what to generate.
In those days most applications will be deployed in more than one instance to scale nicely.
If your strategy is prone to generate duplicates than inserts will fail as uniques of the primary key will be violated.
So while you dont have to think about a unique business key in this scenario, you have to think more what to generate.
You have several options to role your own ID generator.
One is a POJO implementing a generator:
@@ -324,7 +316,7 @@ One is a POJO implementing a generator:
----
import java.util.concurrent.atomic.AtomicInteger;
import org.neo4j.springframework.data.core.schema.IdGenerator;
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.util.StringUtils;
public class TestSequenceGenerator implements IdGenerator<String> {
@@ -390,7 +382,7 @@ The name of the person is assigned at construction time, both by your applicatio
This is only possible, if you find a stable, unique business key, but makes great immutable domain objects.
* Advantages: Using a business or natural key as primary key is natural.
The entity in question is clearly identified and it feels most of the time just right in the further modelling of your domain.
The entity in question is clearly identified and it feels most of the time just right in the further modelling of your domain.
* Disadvantages: Business keys as primary keys will be hard to update once you realise that the key you found is not as stable as you thought.
Often it turns out that it can change, even when promised otherwise.
Apart from that, finding identifier that are truly unique for a thing is hard.
Often it turns out that it can change, even when promised otherwise.
Apart from that, finding identifier that are truly unique for a thing is hard.

View File

@@ -20,19 +20,17 @@ Other constructors will be ignored.
2. If there is a single constructor taking arguments, it will be used.
3. If there are multiple constructors taking arguments, the one to be used by Spring Data will have to be annotated with `@PersistenceConstructor`.
The value resolution assumes constructor argument names to match the property names of the entity,
i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
The value resolution assumes constructor argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
This also requires either parameter names information available in the class file or an `@ConstructorProperties` annotation being present on the constructor.
[[mapping.fundamentals.object-creation.details]]
.Object creation internals
****
To avoid the overhead of reflection, Spring Data object creation uses a factory class generated at runtime by default,
which will call the domain classes constructor directly.
To avoid the overhead of reflection, Spring Data object creation uses a factory class generated at runtime by default, which will call the domain classes constructor directly.
I.e. for this example type:
[source, java]
[source,java]
----
class Person {
Person(String firstname, String lastname) { … }
@@ -41,7 +39,7 @@ class Person {
we will create a factory class semantically equivalent to this one at runtime:
[source, java]
[source,java]
----
class PersonObjectInstantiator implements ObjectInstantiator {
@@ -66,8 +64,7 @@ If any of these criteria match, Spring Data will fall back to entity instantiati
== Property population
Once an instance of the entity has been created, Spring Data populates all remaining persistent properties of that class.
Unless already populated by the entity's constructor (i.e. consumed through its constructor argument list),
the identifier property will be populated first to allow the resolution of cyclic object references.
Unless already populated by the entity's constructor (i.e. consumed through its constructor argument list), the identifier property will be populated first to allow the resolution of cyclic object references.
After that, all non-transient properties that have not already been populated by the constructor are set on the entity instance.
For that we use the following algorithm:
@@ -78,10 +75,9 @@ For that we use the following algorithm:
[[mapping.fundamentals.property-population.details]]
.Property population internals
****
Similarly to our <<mapping.object-creation.details,optimizations in object construction>> we also use
Spring Data runtime generated accessor classes to interact with the entity instance.
Similarly to our <<mapping.object-creation.details,optimizations in object construction>> we also use Spring Data runtime generated accessor classes to interact with the entity instance.
[source, java]
[source,java]
----
class Person {
@@ -109,7 +105,7 @@ class Person {
.A generated Property Accessor
====
[source, java]
[source,java]
----
class PersonPropertyAccessor implements PersistentPropertyAccessor {
@@ -132,11 +128,12 @@ class PersonPropertyAccessor implements PersistentPropertyAccessor {
}
----
<.> PropertyAccessor's hold a mutable instance of the underlying object.
This is, to enable mutations of otherwise immutable properties.
This is, to enable mutations of otherwise immutable properties.
<.> By default, Spring Data uses field-access to read and write property values.
As per visibility rules of `private` fields, `MethodHandles` are used to interact with fields.
<.> The class exposes a `withId(…)` method that's used to set the identifier,
e.g. when an instance is inserted into the datastore and an identifier has been generated. Calling `withId(…)` creates a new `Person` object. All subsequent mutations will take place in the new instance leaving the previous untouched.
As per visibility rules of `private` fields, `MethodHandles` are used to interact with fields.
<.> The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated.
Calling `withId(…)` creates a new `Person` object.
All subsequent mutations will take place in the new instance leaving the previous untouched.
<.> Using property-access allows direct method invocations without using `MethodHandles`.
====
@@ -147,7 +144,7 @@ For the domain class to be eligible for such optimization, it needs to adhere to
- Types and their constructors must be `public`
- Types that are inner classes must be `static`.
- The used Java Runtime must allow for declaring classes in the originating `ClassLoader`.
Java 9 and newer impose certain limitations.
Java 9 and newer impose certain limitations.
By default, Spring Data attempts to use generated property accessors and falls back to reflection-based ones if a limitation is detected.
****
@@ -156,7 +153,7 @@ Let's have a look at the following entity:
.A sample entity
====
[source, java]
[source,java]
----
class Person {
@@ -194,39 +191,35 @@ class Person {
----
====
<.> The identifier property is final but set to `null` in the constructor.
The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated.
The original `Person` instance stays unchanged as a new one is created.
The same pattern is usually applied for other properties that are store managed but might have to be changed for persistence operations.
The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated.
The original `Person` instance stays unchanged as a new one is created.
The same pattern is usually applied for other properties that are store managed but might have to be changed for persistence operations.
<.> The `firstname` and `lastname` properties are ordinary immutable properties potentially exposed through getters.
<.> The `age` property is an immutable but derived one from the `birthday` property.
With the design shown, the database value will trump the defaulting as Spring Data uses the only declared constructor.
Even if the intent is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it)
as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no wither being present.
With the design shown, the database value will trump the defaulting as Spring Data uses the only declared constructor.
Even if the intent is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no wither being present.
<.> The `comment` property is mutable is populated by setting its field directly.
<.> The `remarks` properties are mutable and populated by setting the `comment` field directly or by invoking the setter method for
<.> The class exposes a factory method and a constructor for object creation.
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceConstructor`.
Instead, defaulting of properties is handled within the factory method.
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceConstructor`.
Instead, defaulting of properties is handled within the factory method.
== General recommendations
* _Try to stick to immutable objects_ --
Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only.
Also, this avoids your domain objects to be littered with setter methods that allow client code to manipulate the objects state.
If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of co-located types.
Constructor-only materialization is up to 30% faster than properties population.
Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only.
Also, this avoids your domain objects to be littered with setter methods that allow client code to manipulate the objects state.
If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of co-located types.
Constructor-only materialization is up to 30% faster than properties population.
* _Provide an all-args constructor_ --
Even if you cannot or don't want to model your entities as immutable values,
there's still value in providing a constructor that takes all properties of the entity as arguments, including the mutable ones,
as this allows the object mapping to skip the property population for optimal performance.
Even if you cannot or don't want to model your entities as immutable values, there's still value in providing a constructor that takes all properties of the entity as arguments, including the mutable ones, as this allows the object mapping to skip the property population for optimal performance.
* _Use factory methods instead of overloaded constructors to avoid ``@PersistenceConstructor``_ --
With an all-argument constructor needed for optimal performance, we usually want to expose more application use case specific constructors
that omit things like auto-generated identifiers etc.
It's an established pattern to rather use static factory methods to expose these variants of the all-args constructor.
With an all-argument constructor needed for optimal performance, we usually want to expose more application use case specific constructors that omit things like auto-generated identifiers etc.
It's an established pattern to rather use static factory methods to expose these variants of the all-args constructor.
* _Make sure you adhere to the constraints that allow the generated instantiator and property accessor classes to be used_
* _For identifiers to be generated, still use a final field in combination with a wither method_
* _Use Lombok to avoid boilerplate code_ --
As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repetition of boilerplate parameter to field assignments that can best be avoided by using Lombok's `@AllArgsConstructor`.
As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repetition of boilerplate parameter to field assignments that can best be avoided by using Lombok's `@AllArgsConstructor`.
[[mapping.fundamentals.kotlin]]
== Kotlin support
@@ -260,8 +253,7 @@ data class Person(var id: String, val name: String) {
====
Kotlin supports parameter optionality by allowing default values to be used if a parameter is not provided.
When Spring Data detects a constructor with parameter defaulting, then it leaves these parameters absent
if the data store does not provide a value (or simply returns `null`) so Kotlin can apply parameter defaulting.
When Spring Data detects a constructor with parameter defaulting, then it leaves these parameters absent if the data store does not provide a value (or simply returns `null`) so Kotlin can apply parameter defaulting.
Consider the following class that applies parameter defaulting for `name`
====
@@ -286,5 +278,4 @@ data class Person(val id: String, val name: String)
====
This class is effectively immutable.
It allows to create new instances as Kotlin generates a `copy(…)` method that creates new object instances
copying all property values from the existing object and applying property values provided as arguments to the method.
It allows to create new instances as Kotlin generates a `copy(…)` method that creates new object instances copying all property values from the existing object and applying property values provided as arguments to the method.

View File

@@ -1,2 +0,0 @@
include::sdc-repositories.adoc[leveloffset=+1]
include::sdc-projections.adoc[leveloffset=+1]

View File

@@ -1,182 +0,0 @@
// This is part of the documentation usually pulled in from Spring Data Commons
:springVersion: 5.2.0.RELEASE
:spring-framework-docs: https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference
:spring-framework-javadoc: https://docs.spring.io/spring/docs/{springVersion}/javadoc-api
[[projections]]
= Projections
Spring Data query methods usually return one or multiple instances of the aggregate root managed by the repository.
However, it might sometimes be desirable to create projections based on certain attributes of those types.
Spring Data allows modeling dedicated return types, to more selectively retrieve partial views of the managed aggregates.
Imagine a repository and aggregate root type such as the following example:
.A sample aggregate and repository
====
[source, java, subs="+attributes"]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/Person.java[tag=projection.entity]
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/Person.java[tags=projection.repository;projection.repository.concrete;!projection.repository.interface]
----
====
Now imagine that we want to retrieve the person's name attributes only.
What means does Spring Data offer to achieve this?
The rest of this chapter answers that question.
[[projections.interfaces]]
== Interface-based Projections
The easiest way to limit the result of the queries to only the name attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example:
.A projection interface to retrieve a subset of attributes
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnly.java[tags=projection.interface;!projection.interface.default-method;!projection.interface.open-projection;!projection.interface.bean-access;!projection.interface.method-parameters]
----
====
The important bit here is that the properties defined here exactly match properties in the aggregate root.
Doing so lets a query method be added as follows:
.A repository using an interface based projection with a query method
====
[source, java, subs="+attributes"]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/Person.java[tags=projection.repository;projection.repository.interface;!projection.repository.concrete]
----
====
The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.
[[projections.interfaces.nested]]
Projections can be used recursively.
If you want to include some of the `Address` information as well, create a projection interface for that and return that interface from the declaration of `getAddress()`, as shown in the following example:
.A projection interface to retrieve a subset of attributes
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/PersonSummary.java[tag=projection.interface.nested]
----
====
On method invocation, the `address` property of the target instance is obtained and wrapped into a projecting proxy in turn.
[[projections.interfaces.closed]]
=== Closed Projections
A projection interface whose accessor methods all match properties of the target aggregate is considered to be a closed projection.
The following example (which we used earlier in this chapter, too) is a closed projection:
.A closed projection
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnly.java[tags=projection.interface;!projection.interface.default-method;!projection.interface.open-projection;!projection.interface.bean-access;!projection.interface.method-parameters]
----
====
If you use a closed projection, Spring Data can optimize the query execution, because we know about all the attributes that are needed to back the projection proxy.
For more details on that, see the module-specific part of the reference documentation.
[[projections.interfaces.open]]
=== Open Projections
Accessor methods in projection interfaces can also be used to compute new values by using the `@Value` annotation, as shown in the following example:
[[projections.interfaces.open.simple]]
.An Open Projection
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnly.java[tags=projection.interface;!projection.interface.default-method;!projection.interface.closed-projection;!projection.interface.bean-access;!projection.interface.method-parameters]
----
====
The aggregate root backing the projection is available in the `target` variable.
A projection interface using `@Value` is an open projection.
Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.
The expressions used in `@Value` should not be too complex -- you want to avoid programming in `String` variables.
For very simple expressions, one option might be to resort to default methods (introduced in Java 8), as shown in the following example:
[[projections.interfaces.open.default]]
.A projection interface using a default method for custom logic
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnly.java[tags=projection.interface;!projection.interface.open-projection]
----
====
This approach requires you to be able to implement logic purely based on the other accessor methods exposed on the projection interface.
A second, more flexible, option is to implement the custom logic in a Spring bean and then invoke that from the SpEL expression, as shown in the following example:
[[projections.interfaces.open.bean-reference]]
.Sample Person object
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnly.java[tags=projection.interface.bean-access;projection.interface;!projection.interface.open-projection;!projection.interface.closed-projection;!projection.interface.default-method;!projection.interface.method-parameters]
----
====
Notice how the SpEL expression refers to `nameBean` and invokes the `getFullName(…)` method and forwards the projection target as a method parameter.
Methods backed by SpEL expression evaluation can also use method parameters, which can then be referred to from the expression.
The method parameters are available through an `Object` array named `args`. The following example shows how to get a method parameter from the `args` array:
.Sample Person object
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnly.java[tags=projection.interface;!projection.interface.open-projection;!projection.interface.closed-projection;!projection.interface.default-method;!projection.interface.bean-access]
----
====
Again, for more complex expressions, you should use a Spring bean and let the expression invoke a method, as described <<projections.interfaces.open.bean-reference,earlier>>.
[[projections.dtos]]
== Class-based Projections (DTOs)
Another way of defining projections is by using value type DTOs (Data Transfer Objects) that hold properties for the fields that are supposed to be retrieved.
These DTO types can be used in exactly the same way projection interfaces are used, except that no proxying happens and no nested projections can be applied.
If the store optimizes the query execution by limiting the fields to be loaded, the fields to be loaded are determined from the parameter names of the constructor that is exposed.
The following example shows a projecting DTO:
.A projecting DTO
====
[source, java]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/NamesOnlyDto.java[tags=projection.class]
----
====
[[projection.dynamic]]
== Dynamic Projections
So far, we have used the projection type as the return type or element type of a collection.
However, you might want to select the type to be used at invocation time (which makes it dynamic).
To apply dynamic projections, use a query method such as the one shown in the following example:
.A repository using a dynamic projection parameter
====
[source, java, subs="+attributes"]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/Person.java[tags=projection.dynamic-projection-repository]
----
====
This way, the method can be used to obtain the aggregates as is or with a projection applied, as shown in the following example:
.Using a repository with dynamic projections
====
[source, java, subs="+attributes"]
----
include::../../examples/docs/src/test/java/org/neo4j/doc/springframework/data/docs/repositories/projection/Person.java[tags=projection.dynamic-projection-usage]
----
====

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
[[testing]]
= Testing
We offer `org.neo4j.springframework.boot.test.autoconfigure.data.DataNeo4jTest` and `org.neo4j.springframework.boot.test.autoconfigure.data.ReactiveDataNeo4jTest`
We offer `org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest` and `org.springframework.boot.test.autoconfigure.data.neo4j.ReactiveDataNeo4jTest`
inside an additional module under the following coordinates:
`org.neo4j.springframework.data:spring-data-neo4j-rx-spring-boot-test-autoconfigure`.
`org.springframework.data.neo4j:spring-data-neo4j-spring-boot-test-autoconfigure`.
Include the following dependency in your project setup
[source,xml,subs="verbatim,attributes"]
.spring-data-neo4j-rx-spring-boot-test-autoconfigure for Maven
.spring-data-neo4j-spring-boot-test-autoconfigure for Maven
----
<dependency>
<groupId>org.neo4j.springframework.data</groupId>
<artifactId>spring-data-neo4j-rx-spring-boot-test-autoconfigure</artifactId>
<version>{spring-data-neo4j-rx-version}</version>
<groupId>org.springframework.data.neo4j</groupId>
<artifactId>spring-data-neo4j-spring-boot-test-autoconfigure</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>
----
@@ -20,20 +20,20 @@ Include the following dependency in your project setup
Or with Gradle
[source,groovy,subs="verbatim,attributes"]
.spring-data-neo4j-rx-spring-boot-test-autoconfigure for Gradle
.spring-data-neo4j-spring-boot-test-autoconfigure for Gradle
----
dependencies {
testImplementation 'org.neo4j.springframework.data:spring-data-neo4j-rx-spring-boot-test-autoconfigure:{spring-data-neo4j-rx-version}'
testImplementation 'org.springframework.data.neo4j:spring-data-neo4j-spring-boot-test-autoconfigure:{version}'
}
----
Both `@DataNeo4jTest` and `@ReactiveDataNeo4jTest` are Spring Boot https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing[test slices].
By default, they provide the Neo4j test harness through the transitive dependency of https://github.com/neo4j/neo4j-java-driver-spring-boot-starter/tree/master/neo4j-java-driver-test-harness-spring-boot-autoconfigure[neo4j-java-driver-test-harness-spring-boot-autoconfigure].
Both slices provide all the necessary infrastructure for tests using Neo4j: A driver bean, a transaction manager, a client, a template and declared repositories,
in their imperative or reactive variants.
Both slices provide all the necessary infrastructure for tests using Neo4j: A driver bean, a transaction manager, a client, a template and declared repositories, in their imperative or reactive variants.
`@DataNeo4jTest` provides both variants if reactive repositories are enabled while `@ReactiveDataNeo4jTest` provides only reactive infrastructure.
The dependencies include the Neo4j test harness in version 3.5.x by default. Thus, reactive database access and multiple databases cannot be tested out of the box.
The dependencies include the Neo4j test harness in version 3.5.x by default.
Thus, reactive database access and multiple databases cannot be tested out of the box.
The reason for doing this is simple: Spring Data's JDK baseline is JDK 8. The Neo4j test harness in version 4 requires JDK 11+, so we cannot make it the default.
Here are the available options.
@@ -58,7 +58,7 @@ This class uses the included Neo4j test harness 3.5 by default.
[[dataneo4jtest-template-example]]
.TemplateExampleTest.java
----
include::../../examples/imperative-web/src/test/java/org/neo4j/springframework/data/examples/spring_boot/TemplateExampleTest.java[tags=testing.dataneo4jtest]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/TemplateExampleTest.java[tags=testing.dataneo4jtest]
----
=== With Neo4j 4.0 test harness
@@ -99,9 +99,9 @@ Bring in the required dependencies:
.Dependencies for Testcontainers
----
<dependency>
<groupId>org.neo4j.springframework.data</groupId>
<artifactId>spring-data-neo4j-rx-spring-boot-test-autoconfigure</artifactId>
<version>{spring-data-neo4j-rx-version}</version>
<groupId>org.springframework.data.neo4j</groupId>
<artifactId>spring-data-neo4j-spring-boot-test-autoconfigure</artifactId>
<version>{version}</version>
<scope>test</scope>
<exclusions>
<exclusion> <!--.-->
@@ -124,8 +124,8 @@ Bring in the required dependencies:
</dependency>
----
<.> Be aware of this exclusion. If you don't exclude that dependency, it will have
precedence over any manual configuration of the Neo4j URL.
<.> Be aware of this exclusion.
If you don't exclude that dependency, it will have precedence over any manual configuration of the Neo4j URL.
As of Spring Framework 5.2.5, the TestContext framework provides support for dynamic property sources via the `@DynamicPropertySource` annotation.
This annotation can be used in integration tests that need to add properties with dynamic values.
@@ -137,7 +137,7 @@ A `@DataNeo4jTest` using `@DynamicPropertySource` together with Testcontainers l
[[configure-your-container-source]]
.Configure your Neo4j URLs dynamically in a Spring Boot Test
----
import org.neo4j.springframework.boot.test.autoconfigure.data.DataNeo4jTest;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
@@ -189,13 +189,12 @@ class PriorToBoot226Test {
[IMPORTANT]
====
If you dont want to exclude `org.neo4j.test:neo4j-harness` as advised in <<testcontainers-dependencies>>, you can manually exclude
the `Neo4jTestHarnessAutoConfiguration.class` from the test slice like this:
If you dont want to exclude `org.neo4j.test:neo4j-harness` as advised in <<testcontainers-dependencies>>, you can manually exclude the `Neo4jTestHarnessAutoConfiguration.class` from the test slice like this:
[source,java]
----
import org.neo4j.driver.springframework.boot.test.autoconfigure.Neo4jTestHarnessAutoConfiguration;
import org.neo4j.springframework.boot.test.autoconfigure.data.DataNeo4jTest;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -211,8 +210,7 @@ public class TemplateExampleTest {
== `@ReactiveDataNeo4jTest`
Everything said about `@DataNeo4jTest` applies to `@ReactiveDataNeo4jTest` as well.
However, `@ReactiveDataNeo4jTest` enables reactive infrastructure only and is not meta-annotated with `@Transactional`,
thus it doesn't depend on a `PlatformTransactionManager`.
However, `@ReactiveDataNeo4jTest` enables reactive infrastructure only and is not meta-annotated with `@Transactional`, thus it doesn't depend on a `PlatformTransactionManager`.
`@ReactiveDataNeo4jTest` also checks whether the Neo4j instance configured is capable of handling reactive connections.
@@ -224,5 +222,5 @@ A typical example would look like this:
[[reactivedataneo4jtest-repository-example]]
.RepositoryIT.java
----
include::../../examples/reactive-web/src/test/java/org/neo4j/springframework/data/examples/spring_boot/RepositoryIT.java[tags=testing.reactivedataneo4jtest]
include::../../../../src/test/java/org/springframework/data/neo4j/documentation/spring_boot/RepositoryIT.java[tags=testing.reactivedataneo4jtest]
----

View File

@@ -29,8 +29,8 @@ import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigura
import org.springframework.transaction.PlatformTransactionManager;
/**
* Base class for imperative SDN-RX configuration using JavaConfig.
* This can be included in all scenarios in which Spring Boot is not an option.
* Base class for imperative SDN configuration using JavaConfig. This can be included in all scenarios in which Spring
* Boot is not an option.
*
* @author Michael J. Simons
* @author Gerrit Meier
@@ -61,7 +61,7 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
public Neo4jTemplate neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext,
DatabaseSelectionProvider databaseNameProvider) {
DatabaseSelectionProvider databaseNameProvider) {
return new Neo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
}
@@ -69,13 +69,12 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
/**
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
*
* @param driver The driver to synchronize against
* @param driver The driver to synchronize against
* @param databaseNameProvider The configured database name provider
* @return A platform transaction manager
*/
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
public PlatformTransactionManager transactionManager(Driver driver,
DatabaseSelectionProvider databaseNameProvider) {
public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) {
return new Neo4jTransactionManager(driver, databaseNameProvider);
}
@@ -83,7 +82,8 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
/**
* Configures the database name provider.
*
* @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on Neo4j 3.5 and prior.
* @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on
* Neo4j 3.5 and prior.
*/
@Bean
protected DatabaseSelectionProvider neo4jDatabaseNameProvider() {

View File

@@ -30,8 +30,8 @@ import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
/**
* Base class for reactive SDN-RX configuration using JavaConfig.
* This can be included in all scenarios in which Spring Boot is not an option.
* Base class for reactive SDN configuration using JavaConfig. This can be included in all scenarios in which Spring
* Boot is not an option.
*
* @author Gerrit Meier
* @author Michael J. Simons
@@ -62,7 +62,7 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
public ReactiveNeo4jTemplate neo4jTemplate(final ReactiveNeo4jClient neo4jClient,
final Neo4jMappingContext mappingContext, final ReactiveDatabaseSelectionProvider databaseNameProvider) {
final Neo4jMappingContext mappingContext, final ReactiveDatabaseSelectionProvider databaseNameProvider) {
return new ReactiveNeo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
}
@@ -74,7 +74,8 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp
* @return A platform transaction manager
*/
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseNameProvider) {
public ReactiveTransactionManager reactiveTransactionManager(Driver driver,
ReactiveDatabaseSelectionProvider databaseNameProvider) {
return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider);
}
@@ -82,7 +83,8 @@ public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupp
/**
* Configures the database name provider.
*
* @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on Neo4j 3.5 and prior.
* @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on
* Neo4j 3.5 and prior.
*/
@Bean
protected ReactiveDatabaseSelectionProvider reactiveNeo4jDatabaseNameProvider() {

View File

@@ -27,7 +27,7 @@ import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;
/**
* Annotation to enable auditing for SDN-RX entities via annotation configuration.
* Annotation to enable auditing for SDN entities via annotation configuration.
*
* @author Michael J. Simons
* @since 1.0
@@ -62,8 +62,8 @@ public @interface EnableNeo4jAuditing {
boolean modifyOnCreate() default true;
/**
* Configures a {@link DateTimeProvider} bean name that allows customizing actual date time class to be
* used for setting creation and modification dates.
* Configures a {@link DateTimeProvider} bean name that allows customizing actual date time class to be used for
* setting creation and modification dates.
*
* @return
*/

View File

@@ -39,7 +39,7 @@ import org.springframework.util.ClassUtils;
final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
private static final boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono",
Neo4jAuditingRegistrar.class.getClassLoader());
Neo4jAuditingRegistrar.class.getClassLoader());
private static final String AUDITING_HANDLER_BEAN_NAME = "neo4jAuditingHandler";
private static final String MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext";
@@ -81,19 +81,18 @@ final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSuppor
*/
@Override
protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition,
BeanDefinitionRegistry registry) {
BeanDefinitionRegistry registry) {
Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder
.rootBeanDefinition(AuditingBeforeBindCallback.class);
.rootBeanDefinition(AuditingBeforeBindCallback.class);
listenerBeanDefinitionBuilder
.addConstructorArgValue(
ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(),
AuditingBeforeBindCallback.class.getName(), registry);
AuditingBeforeBindCallback.class.getName(), registry);
if (PROJECT_REACTOR_AVAILABLE) {
registerReactiveAuditingEntityCallback(registry, auditingHandlerDefinition.getSource());
@@ -111,9 +110,8 @@ final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSuppor
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class);
BeanDefinitionBuilder persistentEntities = BeanDefinitionBuilder
.genericBeanDefinition(PersistentEntities.class)
.setFactoryMethod("of");
BeanDefinitionBuilder persistentEntities = BeanDefinitionBuilder.genericBeanDefinition(PersistentEntities.class)
.setFactoryMethod("of");
persistentEntities.addConstructorArgReference(MAPPING_CONTEXT_BEAN_NAME);
builder.addConstructorArgValue(persistentEntities.getBeanDefinition());
@@ -122,14 +120,12 @@ final class Neo4jAuditingRegistrar extends AuditingBeanDefinitionRegistrarSuppor
private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, Object source) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(ReactiveAuditingBeforeBindCallback.class);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingBeforeBindCallback.class);
builder.addConstructorArgValue(
ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
builder.getRawBeanDefinition().setSource(source);
registerInfrastructureBeanWithId(builder.getBeanDefinition(),
ReactiveAuditingBeforeBindCallback.class.getName(), registry);
registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingBeforeBindCallback.class.getName(),
registry);
}
}

View File

@@ -32,8 +32,8 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Internal support class for basic configuration. The support infrastructure here is basically all around finding out about
* which classes are to be mapped and which not. The driver needs to be configured from a class either extending
* Internal support class for basic configuration. The support infrastructure here is basically all around finding out
* about which classes are to be mapped and which not. The driver needs to be configured from a class either extending
* {@link AbstractNeo4jConfig} for imperative or {@link AbstractReactiveNeo4jConfig} for reactive programming model.
*
* @author Michael J. Simons
@@ -49,8 +49,7 @@ abstract class Neo4jConfigurationSupport {
}
/**
* Creates a {@link Neo4jMappingContext} equipped with entity classes
* scanned from the mapping base package.
* Creates a {@link Neo4jMappingContext} equipped with entity classes scanned from the mapping base package.
*
* @return A new {@link Neo4jMappingContext} with initial classes to scan for entities set.
* @see #getMappingBasePackages()
@@ -70,8 +69,8 @@ abstract class Neo4jConfigurationSupport {
* {@code com.acme.AppConfig} extending {@link Neo4jConfigurationSupport} the base package will be considered
* {@code com.acme} unless the method is overridden to implement alternate behavior.
*
* @return the base packages to scan for mapped {@link Node} classes
* or an empty collection to not enable scanning for entities.
* @return the base packages to scan for mapped {@link Node} classes or an empty collection to not enable scanning for
* entities.
*/
protected Collection<String> getMappingBasePackages() {
@@ -80,8 +79,8 @@ abstract class Neo4jConfigurationSupport {
}
/**
* Scans the mapping base package for classes annotated with {@link Node}.
* By default, it scans for entities in all packages returned by {@link #getMappingBasePackages()}.
* Scans the mapping base package for classes annotated with {@link Node}. By default, it scans for entities in all
* packages returned by {@link #getMappingBasePackages()}.
*
* @return initial set of domain classes
* @throws ClassNotFoundException if the given class cannot be found in the class path.
@@ -113,8 +112,8 @@ abstract class Neo4jConfigurationSupport {
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
ClassPathScanningCandidateComponentProvider componentProvider =
new ClassPathScanningCandidateComponentProvider(false);
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
false);
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class));
ClassLoader classLoader = Neo4jConfigurationSupport.class.getClassLoader();

View File

@@ -27,8 +27,9 @@ import org.springframework.data.neo4j.repository.event.IdGeneratingBeforeBindCal
import org.springframework.data.neo4j.repository.event.OptimisticLockingBeforeBindCallback;
/**
* This brings in the default callbacks required for the default implementation of {@link Neo4jOperations} to work.
* The offered support configuration class {@link AbstractNeo4jConfig} imports this and so does the Spring Boot autoconfiguration.
* This brings in the default callbacks required for the default implementation of {@link Neo4jOperations} to work. The
* offered support configuration class {@link AbstractNeo4jConfig} imports this and so does the Spring Boot
* autoconfiguration.
*
* @author Michael J. Simons
* @soundtrack AC/DC - High Voltage
@@ -38,11 +39,8 @@ import org.springframework.data.neo4j.repository.event.OptimisticLockingBeforeBi
public final class Neo4jDefaultCallbacksRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry,
BeanNameGenerator beanNameGenerator
) {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator beanNameGenerator) {
// Id Generator
RootBeanDefinition beanDefinition = new RootBeanDefinition(IdGeneratingBeforeBindCallback.class);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

View File

@@ -27,8 +27,9 @@ import org.springframework.data.neo4j.repository.event.ReactiveIdGeneratingBefor
import org.springframework.data.neo4j.repository.event.ReactiveOptimisticLockingBeforeBindCallback;
/**
* This brings in the default callbacks required for the default implementation of {@link Neo4jOperations} to work.
* The offered support configuration class {@link AbstractNeo4jConfig} imports this and so does the Spring Boot autoconfiguration.
* This brings in the default callbacks required for the default implementation of {@link Neo4jOperations} to work. The
* offered support configuration class {@link AbstractNeo4jConfig} imports this and so does the Spring Boot
* autoconfiguration.
*
* @author Michael J. Simons
* @soundtrack AC/DC - High Voltage
@@ -38,11 +39,8 @@ import org.springframework.data.neo4j.repository.event.ReactiveOptimisticLocking
public final class Neo4jDefaultReactiveCallbacksRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry,
BeanNameGenerator beanNameGenerator
) {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator beanNameGenerator) {
// Id Generator
RootBeanDefinition beanDefinition = new RootBeanDefinition(ReactiveIdGeneratingBeforeBindCallback.class);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

View File

@@ -21,8 +21,8 @@ import org.apiguardian.api.API;
import org.springframework.lang.Nullable;
/**
* A value holder indicating a database selection based on a optional name.
* {@literal null} indicates to let the server decide.
* A value holder indicating a database selection based on a optional name. {@literal null} indicates to let the server
* decide.
*
* @author Michael J. Simons
* @soundtrack Rage - Reign Of Fear

View File

@@ -19,13 +19,17 @@ import org.apiguardian.api.API;
import org.springframework.util.Assert;
/**
* A provider interface that knows in which database repositories or either the reactive or imperative template should work.
* <p>An instance of a database name provider is only relevant when SDN-RX is used with a Neo4j 4.0+ cluster or server.
* <p>To select the default database, return an empty optional. If you return a database name, it must not be empty.
* The empty optional indicates an unset database name on the client, so that the server can decide on the default to use.
* <p>The provider is asked before any interaction of a repository or template with the cluster or server. That means you can
* in theory return different database names for each interaction. Be aware that you might end up with no data on queries
* or data stored to wrong database if you don't pay meticulously attention to the database you interact with.
* A provider interface that knows in which database repositories or either the reactive or imperative template should
* work.
* <p>
* An instance of a database name provider is only relevant when SDN is used with a Neo4j 4.0+ cluster or server.
* <p>
* To select the default database, return an empty optional. If you return a database name, it must not be empty. The
* empty optional indicates an unset database name on the client, so that the server can decide on the default to use.
* <p>
* The provider is asked before any interaction of a repository or template with the cluster or server. That means you
* can in theory return different database names for each interaction. Be aware that you might end up with no data on
* queries or data stored to wrong database if you don't pay meticulously attention to the database you interact with.
*
* @author Michael J. Simons
* @soundtrack N.W.A. - Straight Outta Compton
@@ -36,12 +40,14 @@ import org.springframework.util.Assert;
public interface DatabaseSelectionProvider {
/**
* @return The selected database me to interact with. Use {@link DatabaseSelection#undecided()} to indicate the default database.
* @return The selected database me to interact with. Use {@link DatabaseSelection#undecided()} to indicate the
* default database.
*/
DatabaseSelection getDatabaseSelection();
/**
* Creates a statically configured database selection provider always selecting the database with the given name {@code databaseName}.
* Creates a statically configured database selection provider always selecting the database with the given name
* {@code databaseName}.
*
* @param databaseName The database name to use, must not be null nor empty.
* @return A statically configured database name provider.

View File

@@ -51,8 +51,8 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the database.
* TODO Micrometer hooks for statement results...
* Default implementation of {@link Neo4jClient}. Uses the Neo4j Java driver to connect to and interact with the
* database. TODO Micrometer hooks for statement results...
*
* @author Gerrit Meier
* @author Michael J. Simons
@@ -82,8 +82,7 @@ class DefaultNeo4jClient implements Neo4jClient {
}
return (AutoCloseableQueryRunner) Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class<?>[] { AutoCloseableQueryRunner.class },
new AutoCloseableQueryRunnerHandler(queryRunner));
new Class<?>[] { AutoCloseableQueryRunner.class }, new AutoCloseableQueryRunnerHandler(queryRunner));
}
/**
@@ -91,7 +90,8 @@ class DefaultNeo4jClient implements Neo4jClient {
*/
interface AutoCloseableQueryRunner extends QueryRunner, AutoCloseable {
@Override void close();
@Override
void close();
}
static class AutoCloseableQueryRunnerHandler implements InvocationHandler {
@@ -143,8 +143,8 @@ class DefaultNeo4jClient implements Neo4jClient {
}
/**
* Basically a holder of a cypher template supplier and a set of named parameters. It's main purpose is to
* orchestrate the running of things with a bit of logging.
* Basically a holder of a cypher template supplier and a set of named parameters. It's main purpose is to orchestrate
* the running of things with a bit of logging.
*/
class RunnableStatement {
@@ -185,7 +185,7 @@ class DefaultNeo4jClient implements Neo4jClient {
* @return
*/
private static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex,
PersistenceExceptionTranslator exceptionTranslator) {
PersistenceExceptionTranslator exceptionTranslator) {
RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex);
return resolved == null ? ex : resolved;
}
@@ -209,8 +209,7 @@ class DefaultNeo4jClient implements Neo4jClient {
class DefaultOngoingBindSpec<T> implements OngoingBindSpec<T, RunnableSpecTightToDatabase> {
@Nullable
private final T value;
@Nullable private final T value;
DefaultOngoingBindSpec(@Nullable T value) {
this.value = value;
@@ -247,15 +246,13 @@ class DefaultNeo4jClient implements Neo4jClient {
public <T> MappingSpec<T> fetchAs(Class<T> targetClass) {
return new DefaultRecordFetchSpec(this.targetDatabase, this.runnableStatement,
new SingleValueMappingFunction(conversionService, targetClass));
new SingleValueMappingFunction(conversionService, targetClass));
}
@Override
public RecordFetchSpec<Map<String, Object>> fetch() {
return new DefaultRecordFetchSpec<>(
this.targetDatabase,
this.runnableStatement, (t, r) -> r.asMap());
return new DefaultRecordFetchSpec<>(this.targetDatabase, this.runnableStatement, (t, r) -> r.asMap());
}
@Override
@@ -279,7 +276,7 @@ class DefaultNeo4jClient implements Neo4jClient {
private BiFunction<TypeSystem, Record, T> mappingFunction;
DefaultRecordFetchSpec(String targetDatabase, RunnableStatement runnableStatement,
BiFunction<TypeSystem, Record, T> mappingFunction) {
BiFunction<TypeSystem, Record, T> mappingFunction) {
this.targetDatabase = targetDatabase;
this.runnableStatement = runnableStatement;
this.mappingFunction = mappingFunction;
@@ -287,7 +284,7 @@ class DefaultNeo4jClient implements Neo4jClient {
@Override
public RecordFetchSpec<T> mappedBy(
@SuppressWarnings("HiddenField") BiFunction<TypeSystem, Record, T> mappingFunction) {
@SuppressWarnings("HiddenField") BiFunction<TypeSystem, Record, T> mappingFunction) {
this.mappingFunction = new DelegatingMappingFunctionWithNullCheck<>(mappingFunction);
return this;
@@ -298,9 +295,7 @@ class DefaultNeo4jClient implements Neo4jClient {
try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) {
Result result = runnableStatement.runWith(statementRunner);
return result.hasNext() ?
Optional.of(mappingFunction.apply(typeSystem, result.single())) :
Optional.empty();
return result.hasNext() ? Optional.of(mappingFunction.apply(typeSystem, result.single())) : Optional.empty();
} catch (RuntimeException e) {
throw potentiallyConvertRuntimeException(e, persistenceExceptionTranslator);
}

View File

@@ -69,30 +69,25 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
Mono<RxStatementRunnerHolder> retrieveRxStatementRunnerHolder(String targetDatabase) {
return ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, targetDatabase)
.map(rxTransaction -> new RxStatementRunnerHolder(rxTransaction, Mono.empty(), Mono.empty())) //
.switchIfEmpty(
Mono.using(() -> driver.rxSession(Neo4jTransactionUtils.defaultSessionConfig(targetDatabase)),
session -> Mono.from(session.beginTransaction())
.map(tx -> new RxStatementRunnerHolder(tx, tx.commit(), tx.rollback())), RxSession::close)
);
.map(rxTransaction -> new RxStatementRunnerHolder(rxTransaction, Mono.empty(), Mono.empty())) //
.switchIfEmpty(Mono.using(() -> driver.rxSession(Neo4jTransactionUtils.defaultSessionConfig(targetDatabase)),
session -> Mono.from(session.beginTransaction())
.map(tx -> new RxStatementRunnerHolder(tx, tx.commit(), tx.rollback())),
RxSession::close));
}
<T> Mono<T> doInQueryRunnerForMono(final String targetDatabase, Function<RxQueryRunner, Mono<T>> func) {
return Mono.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase),
holder -> func.apply(holder.getRxQueryRunner()),
RxStatementRunnerHolder::getCommit,
(holder, ex) -> holder.getRollback(),
RxStatementRunnerHolder::getCommit);
holder -> func.apply(holder.getRxQueryRunner()), RxStatementRunnerHolder::getCommit,
(holder, ex) -> holder.getRollback(), RxStatementRunnerHolder::getCommit);
}
<T> Flux<T> doInStatementRunnerForFlux(final String targetDatabase, Function<RxQueryRunner, Flux<T>> func) {
return Flux.usingWhen(retrieveRxStatementRunnerHolder(targetDatabase),
holder -> func.apply(holder.getRxQueryRunner()),
RxStatementRunnerHolder::getCommit,
(holder, ex) -> holder.getRollback(),
RxStatementRunnerHolder::getCommit);
holder -> func.apply(holder.getRxQueryRunner()), RxStatementRunnerHolder::getCommit,
(holder, ex) -> holder.getRollback(), RxStatementRunnerHolder::getCommit);
}
@Override
@@ -131,8 +126,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
class DefaultOngoingBindSpec<T> implements Neo4jClient.OngoingBindSpec<T, RunnableSpecTightToDatabase> {
@Nullable
private final T value;
@Nullable private final T value;
DefaultOngoingBindSpec(@Nullable T value) {
this.value = value;
@@ -169,23 +163,19 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
public <R> MappingSpec<R> fetchAs(Class<R> targetClass) {
return new DefaultRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters,
new SingleValueMappingFunction(conversionService, targetClass));
new SingleValueMappingFunction(conversionService, targetClass));
}
@Override
public RecordFetchSpec<Map<String, Object>> fetch() {
return new DefaultRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters,
(t, r) -> r.asMap());
return new DefaultRecordFetchSpec<>(targetDatabase, cypherSupplier, parameters, (t, r) -> r.asMap());
}
@Override
public Mono<ResultSummary> run() {
return new DefaultRecordFetchSpec<>(
this.targetDatabase,
this.cypherSupplier,
this.parameters).run();
return new DefaultRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters).run();
}
}
@@ -199,14 +189,12 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
private BiFunction<TypeSystem, Record, T> mappingFunction;
DefaultRecordFetchSpec(String targetDatabase, Supplier<String> cypherSupplier,
NamedParameters parameters) {
DefaultRecordFetchSpec(String targetDatabase, Supplier<String> cypherSupplier, NamedParameters parameters) {
this(targetDatabase, cypherSupplier, parameters, null);
}
DefaultRecordFetchSpec(
String targetDatabase, Supplier<String> cypherSupplier, NamedParameters parameters,
@Nullable BiFunction<TypeSystem, Record, T> mappingFunction) {
DefaultRecordFetchSpec(String targetDatabase, Supplier<String> cypherSupplier, NamedParameters parameters,
@Nullable BiFunction<TypeSystem, Record, T> mappingFunction) {
this.targetDatabase = targetDatabase;
this.cypherSupplier = cypherSupplier;
this.parameters = parameters;
@@ -240,39 +228,33 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
@Override
public Mono<T> one() {
return doInQueryRunnerForMono(
targetDatabase,
(runner) -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty()
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
return doInQueryRunnerForMono(targetDatabase,
(runner) -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).singleOrEmpty())
.onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
}
@Override
public Mono<T> first() {
return doInQueryRunnerForMono(
targetDatabase,
runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next()
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
return doInQueryRunnerForMono(targetDatabase,
runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner)).next())
.onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
}
@Override
public Flux<T> all() {
return doInStatementRunnerForFlux(
targetDatabase,
runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner))
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
return doInStatementRunnerForFlux(targetDatabase,
runner -> prepareStatement().flatMapMany(t -> executeWith(t, runner))).onErrorMap(RuntimeException.class,
DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
}
Mono<ResultSummary> run() {
return doInQueryRunnerForMono(
targetDatabase,
runner -> prepareStatement().flatMap(t -> {
RxResult rxResult = runner.run(t.getT1(), t.getT2());
return Flux.from(rxResult.records()).then(Mono.from(rxResult.consume()));
})
).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
return doInQueryRunnerForMono(targetDatabase, runner -> prepareStatement().flatMap(t -> {
RxResult rxResult = runner.run(t.getT1(), t.getT2());
return Flux.from(rxResult.records()).then(Mono.from(rxResult.consume()));
})).onErrorMap(RuntimeException.class, DefaultReactiveNeo4jClient.this::potentiallyConvertRuntimeException);
}
}
@@ -298,8 +280,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
this(callback, null);
}
DefaultRunnableDelegation(Function<RxQueryRunner, Mono<T>> callback,
@Nullable String targetDatabase) {
DefaultRunnableDelegation(Function<RxQueryRunner, Mono<T>> callback, @Nullable String targetDatabase) {
this.callback = callback;
this.targetDatabase = targetDatabase;
}
@@ -314,10 +295,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
@Override
public Mono<T> run() {
return doInQueryRunnerForMono(
targetDatabase,
callback
);
return doInQueryRunnerForMono(targetDatabase, callback);
}
}

View File

@@ -21,8 +21,8 @@ import org.neo4j.driver.Record;
import org.neo4j.driver.types.TypeSystem;
/**
* A delegating mapping function that first calls the delegate to get a record map and than checks the returned
* value for {@literal null} and in the case of a null value, an {@link IllegalStateException} will be thrown.
* A delegating mapping function that first calls the delegate to get a record map and than checks the returned value
* for {@literal null} and in the case of a null value, an {@link IllegalStateException} will be thrown.
* <p>
* This class has been introduced instead of {@code Function#andThen} notion to be able throw a decent exception
* containing some information about the delegate used and which record was problematic.
@@ -45,7 +45,7 @@ class DelegatingMappingFunctionWithNullCheck<T> implements BiFunction<TypeSystem
T t = delegate.apply(typeSystem, record);
if (t == null) {
throw new IllegalStateException(
"Mapping function " + delegate + " returned illegal null value for record " + record);
"Mapping function " + delegate + " returned illegal null value for record " + record);
}
return t;
}

View File

@@ -31,8 +31,7 @@ import org.springframework.data.neo4j.core.schema.Constants;
*
* @author Michael J. Simons
*/
final class DynamicLabels
implements UnaryOperator<OngoingMatchAndUpdate> {
final class DynamicLabels implements UnaryOperator<OngoingMatchAndUpdate> {
public static final DynamicLabels EMPTY = new DynamicLabels(Collections.emptyList(), Collections.emptyList());

View File

@@ -39,7 +39,8 @@ final class NamedParameters {
* Adds all of the values contained in {@code newParameters} to this list of named parameters.
*
* @param newParameters Additional parameters to add
* @throws IllegalStateException when any value in {@code newParameters} exists under the same name in the current parameters.
* @throws IllegalStateException when any value in {@code newParameters} exists under the same name in the current
* parameters.
*/
void addAll(Map<String, Object> newParameters) {
newParameters.forEach(this::add);
@@ -48,7 +49,7 @@ final class NamedParameters {
/**
* Adds a new parameter under the key {@code name} with the value {@code value}.
*
* @param name The name of the new parameter
* @param name The name of the new parameter
* @param value The value of the new parameter
* @throws IllegalStateException when a parameter with the given name already exists
*/
@@ -57,11 +58,8 @@ final class NamedParameters {
if (this.parameters.containsKey(name)) {
Object previousValue = this.parameters.get(name);
throw new IllegalArgumentException(String.format(
"Duplicate parameter name: '%s' already in the list of named parameters with value '%s'. New value would be '%s'",
name,
previousValue == null ? "null" : previousValue.toString(),
value == null ? "null" : value.toString()
));
"Duplicate parameter name: '%s' already in the list of named parameters with value '%s'. New value would be '%s'",
name, previousValue == null ? "null" : previousValue.toString(), value == null ? "null" : value.toString()));
}
this.parameters.put(name, value);
}
@@ -79,11 +77,8 @@ final class NamedParameters {
@Override
public String toString() {
return parameters
.entrySet()
.stream()
.map(e -> String.format("%s: %s", e.getKey(), formatValue(e.getValue())))
.collect(joining(", ", ":params {", "}"));
return parameters.entrySet().stream().map(e -> String.format("%s: %s", e.getKey(), formatValue(e.getValue())))
.collect(joining(", ", ":params {", "}"));
}
private static Object formatValue(Object value) {
@@ -93,8 +88,7 @@ final class NamedParameters {
return Cypher.quote((String) value);
} else if (value instanceof Map) {
return ((Map<?, ?>) value).entrySet().stream()
.map(e -> String.format("%s: %s", e.getKey(), formatValue(e.getValue())))
.collect(joining(", ", "{", "}"));
.map(e -> String.format("%s: %s", e.getKey(), formatValue(e.getValue()))).collect(joining(", ", "{", "}"));
} else if (value instanceof Collection) {
return ((Collection) value).stream().map(NamedParameters::formatValue).collect(joining(", ", "[", "]"));
}

View File

@@ -73,21 +73,24 @@ public interface Neo4jClient {
/**
* Delegates interaction with the default database to the given callback.
*
* @param callback A function receiving a statement runner for database interaction that can optionally return a result.
* @param <T> The type of the result being produced
* @param callback A function receiving a statement runner for database interaction that can optionally return a
* result.
* @param <T> The type of the result being produced
* @return A single result object or an empty optional if the callback didn't produce a result
*/
<T> OngoingDelegation<T> delegateTo(Function<QueryRunner, Optional<T>> callback);
/**
* Contract for a runnable query that can be either run returning it's result, run without results or be parameterized.
* Contract for a runnable query that can be either run returning it's result, run without results or be
* parameterized.
*
* @since 1.0
*/
interface RunnableSpec extends RunnableSpecTightToDatabase {
/**
* Pins the previously defined query to a specific database. A value of {@literal null} chooses the default database.
* The empty string {@literal ""} is not permitted.
* Pins the previously defined query to a specific database. A value of {@literal null} chooses the default
* database. The empty string {@literal ""} is not permitted.
*
* @param targetDatabase selected database to use
* @return A runnable query specification that is now tight to a given database.
@@ -97,6 +100,7 @@ public interface Neo4jClient {
/**
* Contract for a runnable query inside a dedicated database.
*
* @since 1.0
*/
interface RunnableSpecTightToDatabase extends BindSpec<RunnableSpecTightToDatabase> {
@@ -105,7 +109,7 @@ public interface Neo4jClient {
* Create a mapping for each record return to a specific type.
*
* @param targetClass The class each record should be mapped to
* @param <T> The type of the class
* @param <T> The type of the class
* @return A mapping spec that allows specifying a mapping function.
*/
<T> MappingSpec<T> fetchAs(Class<T> targetClass);
@@ -118,8 +122,8 @@ public interface Neo4jClient {
RecordFetchSpec<Map<String, Object>> fetch();
/**
* Execute the query and discard the results. It returns the drivers result summary, including various counters
* and other statistics.
* Execute the query and discard the results. It returns the drivers result summary, including various counters and
* other statistics.
*
* @return The native summary of the query.
*/
@@ -176,8 +180,8 @@ public interface Neo4jClient {
interface MappingSpec<T> extends RecordFetchSpec<T> {
/**
* The mapping function is responsible to turn one record into one domain object. It will receive the record
* itself and in addition, the type system that the Neo4j Java-Driver used while executing the query.
* The mapping function is responsible to turn one record into one domain object. It will receive the record itself
* and in addition, the type system that the Neo4j Java-Driver used while executing the query.
*
* @param mappingFunction The mapping function used to create new domain objects
* @return A specification how to fetch one or more records.
@@ -258,7 +262,7 @@ public interface Neo4jClient {
String newTargetDatabase = databaseName == null ? null : databaseName.trim();
if (newTargetDatabase != null && newTargetDatabase.isEmpty()) {
throw new IllegalArgumentException(
"Either use null to indicate the default database or a valid database name. The empty string is not permitted.");
"Either use null to indicate the default database or a valid database name. The empty string is not permitted.");
}
return newTargetDatabase;
}

View File

@@ -53,7 +53,7 @@ public interface Neo4jOperations {
/**
* Counts the number of entities of a given type.
*
* @param statement the Cypher {@link Statement} that returns the count.
* @param statement the Cypher {@link Statement} that returns the count.
* @param parameters Map of parameters. Must not be {@code null}.
* @return the number of instances stored in the database. Guaranteed to be not {@code null}.
*/
@@ -80,7 +80,7 @@ public interface Neo4jOperations {
* Load all entities of a given type.
*
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> List<T> findAll(Class<T> domainType);
@@ -88,9 +88,9 @@ public interface Neo4jOperations {
/**
* Load all entities of a given type by executing given statement.
*
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> List<T> findAll(Statement statement, Class<T> domainType);
@@ -98,10 +98,10 @@ public interface Neo4jOperations {
/**
* Load all entities of a given type by executing given statement with parameters.
*
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> List<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType);
@@ -109,10 +109,10 @@ public interface Neo4jOperations {
/**
* Load one entity of a given type by executing given statement with parameters.
*
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Optional<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> domainType);
@@ -120,9 +120,9 @@ public interface Neo4jOperations {
/**
* Load all entities of a given type by executing given statement.
*
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> List<T> findAll(String cypherQuery, Class<T> domainType);
@@ -130,10 +130,10 @@ public interface Neo4jOperations {
/**
* Load all entities of a given type by executing given statement with parameters.
*
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> List<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> domainType);
@@ -141,10 +141,10 @@ public interface Neo4jOperations {
/**
* Load one entity of a given type by executing given statement with parameters.
*
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Optional<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> domainType);
@@ -152,9 +152,9 @@ public interface Neo4jOperations {
/**
* Load an entity from the database.
*
* @param id the id of the entity to load. Must not be {@code null}.
* @param id the id of the entity to load. Must not be {@code null}.
* @param domainType the type of the entity. Must not be {@code null}.
* @param <T> the type of the entity.
* @param <T> the type of the entity.
* @return the loaded entity. Might return an empty optional.
*/
<T> Optional<T> findById(Object id, Class<T> domainType);
@@ -162,9 +162,9 @@ public interface Neo4jOperations {
/**
* Load all entities of a given type that are identified by the given ids.
*
* @param ids of the entities identifying the entities to load. Must not be {@code null}.
* @param ids of the entities identifying the entities to load. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> List<T> findAllById(Iterable<?> ids, Class<T> domainType);
@@ -173,7 +173,7 @@ public interface Neo4jOperations {
* Saves an instance of an entity, including all the related entities of the entity.
*
* @param instance the entity to be saved. Must not be {@code null}.
* @param <T> the type of the entity.
* @param <T> the type of the entity.
* @return the saved instance.
*/
<T> T save(T instance);
@@ -182,7 +182,7 @@ public interface Neo4jOperations {
* Saves several instances of an entity, including all the related entities of the entity.
*
* @param instances the instances to be saved. Must not be {@code null}.
* @param <T> the type of the entity.
* @param <T> the type of the entity.
* @return the saved instances.
*/
<T> List<T> saveAll(Iterable<T> instances);
@@ -190,18 +190,18 @@ public interface Neo4jOperations {
/**
* Deletes a single entity including all entities related to that entity.
*
* @param id the id of the entity to be deleted. Must not be {@code null}.
* @param id the id of the entity to be deleted. Must not be {@code null}.
* @param domainType the type of the entity
* @param <T> the type of the entity.
* @param <T> the type of the entity.
*/
<T> void deleteById(Object id, Class<T> domainType);
/**
* Deletes all entities with one of the given ids, including all entities related to that entity.
*
* @param ids the ids of the entities to be deleted. Must not be {@code null}.
* @param ids the ids of the entities to be deleted. Must not be {@code null}.
* @param domainType the type of the entity
* @param <T> the type of the entity.
* @param <T> the type of the entity.
*/
<T> void deleteAllById(Iterable<?> ids, Class<T> domainType);
@@ -217,8 +217,8 @@ public interface Neo4jOperations {
* an optional mapping function, and turns it into an executable query.
*
* @param preparedQuery prepared query that should get converted to an executable query
* @param <T> The type of the objects returned by this query.
* @return An executable query
* @param <T> The type of the objects returned by this query.
* @return An executable query
*/
<T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery);
@@ -248,4 +248,3 @@ public interface Neo4jOperations {
T getRequiredSingleResult();
}
}

View File

@@ -90,7 +90,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
this(neo4jClient, new Neo4jMappingContext(), DatabaseSelectionProvider.getDefaultSelectionProvider());
}
public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, DatabaseSelectionProvider databaseSelectionProvider) {
public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext,
DatabaseSelectionProvider databaseSelectionProvider) {
Assert.notNull(neo4jClient, "The Neo4jClient is required");
Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required");
@@ -108,8 +109,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
public long count(Class<?> domainType) {
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
.returning(Functions.count(asterisk())).build();
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData).returning(Functions.count(asterisk())).build();
return count(statement);
}
@@ -132,10 +132,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
@Override
public long count(String cypherQuery, Map<String, Object> parameters) {
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class)
.withCypherQuery(cypherQuery)
.withParameters(parameters)
.build();
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery)
.withParameters(parameters).build();
return toExecutableQuery(preparedQuery).getRequiredSingleResult();
}
@@ -144,7 +142,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
return createExecutableQuery(domainType, statement).getResults();
}
@@ -182,27 +180,27 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
public <T> Optional<T> findById(Object id, Class<T> domainType) {
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
.build();
return createExecutableQuery(domainType, statement, singletonMap(Constants.NAME_OF_ID, convertIdValues(id))).getSingleResult();
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
return createExecutableQuery(domainType, statement, singletonMap(Constants.NAME_OF_ID, convertIdValues(id)))
.getSingleResult();
}
@Override
public <T> List<T> findAllById(Iterable<?> ids, Class<T> domainType) {
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS))))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
.build();
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS))))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
return createExecutableQuery(domainType, statement, singletonMap(Constants.NAME_OF_IDS, convertIdValues(ids))).getResults();
return createExecutableQuery(domainType, statement, singletonMap(Constants.NAME_OF_IDS, convertIdValues(ids)))
.getResults();
}
private Object convertIdValues(Object idValues) {
return neo4jMappingContext.getConverter()
.writeValueFromProperty(idValues, ClassTypeInformation.from(idValues.getClass()));
return neo4jMappingContext.getConverter().writeValueFromProperty(idValues,
ClassTypeInformation.from(idValues.getClass()));
}
@Override
@@ -219,11 +217,10 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData, inDatabase);
Optional<Long> optionalInternalId = neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels)))
.in(inDatabase)
.bind((T) entityToBeSaved)
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()))
.fetchAs(Long.class).one();
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels))).in(inDatabase)
.bind((T) entityToBeSaved)
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()))
.fetchAs(Long.class).one();
if (entityMetaData.hasVersionProperty() && !optionalInternalId.isPresent()) {
throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE);
@@ -241,29 +238,25 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
}
}
private <T> DynamicLabels determineDynamicLabels(
T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData, @Nullable String inDatabase
) {
private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData,
@Nullable String inDatabase) {
return entityMetaData.getDynamicLabelsProperty().map(p -> {
PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
Neo4jClient.RunnableSpecTightToDatabase runnableQuery = neo4jClient
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
.in(inDatabase)
.bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty())).to(Constants.NAME_OF_ID)
.bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM);
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
.in(inDatabase).bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty()))
.to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM);
if (entityMetaData.hasVersionProperty()) {
runnableQuery = runnableQuery
.bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty()) - 1)
.to(Constants.NAME_OF_VERSION_PARAM);
.bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty()) - 1)
.to(Constants.NAME_OF_VERSION_PARAM);
}
Optional<Map<String, Object>> optionalResult = runnableQuery.fetch().one();
return new DynamicLabels(
optionalResult.map(r -> (Collection<String>) r.get(Constants.NAME_OF_LABELS)).orElseGet(Collections::emptyList),
(Collection<String>) propertyAccessor.getProperty(p)
);
return new DynamicLabels(optionalResult.map(r -> (Collection<String>) r.get(Constants.NAME_OF_LABELS))
.orElseGet(Collections::emptyList), (Collection<String>) propertyAccessor.getProperty(p));
}).orElse(DynamicLabels.EMPTY);
}
@@ -289,31 +282,24 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
if (entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()) {
log.debug("Saving entities using single statements.");
return entities.stream()
.map(e -> saveImpl(e, databaseName))
.collect(toList());
return entities.stream().map(e -> saveImpl(e, databaseName)).collect(toList());
}
List<T> entitiesToBeSaved = entities.stream()
.map(eventSupport::maybeCallBeforeBind)
.collect(toList());
List<T> entitiesToBeSaved = entities.stream().map(eventSupport::maybeCallBeforeBind).collect(toList());
// Save roots
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
List<Map<String, Object>> entityList = entitiesToBeSaved.stream()
.map(binderFunction).collect(toList());
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(binderFunction).collect(toList());
ResultSummary resultSummary = neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.in(databaseName)
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
.run();
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))).in(databaseName)
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run();
// Save related
entitiesToBeSaved.forEach(entityToBeSaved -> processRelations(entityMetaData, entityToBeSaved, databaseName));
SummaryCounters counters = resultSummary.counters();
log.debug(() -> String
.format("Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
log.debug(() -> String.format(
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
counters.relationshipsDeleted(), counters.propertiesSet()));
@@ -330,13 +316,11 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
log.debug(() -> String.format("Deleting entity with id %s ", id));
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
.in(getDatabaseName())
.bind(id).to(nameOfParameter)
.run();
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).bind(id)
.to(nameOfParameter).run();
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
summary.counters().relationshipsDeleted()));
summary.counters().relationshipsDeleted()));
}
@Override
@@ -349,13 +333,11 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
log.debug(() -> String.format("Deleting all entities with the following ids: %s ", ids));
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
.in(getDatabaseName())
.bind(ids).to(nameOfParameter)
.run();
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).bind(ids)
.to(nameOfParameter).run();
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
summary.counters().relationshipsDeleted()));
summary.counters().relationshipsDeleted()));
}
@Override
@@ -368,7 +350,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).run();
log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
summary.counters().relationshipsDeleted()));
summary.counters().relationshipsDeleted()));
}
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, Statement statement) {
@@ -380,30 +362,29 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
}
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, Statement statement,
Map<String, Object> parameters) {
Map<String, Object> parameters) {
return createExecutableQuery(domainType, renderer.render(statement), parameters);
}
private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, String cypherStatement,
Map<String, Object> parameters) {
Map<String, Object> parameters) {
PreparedQuery<T> preparedQuery = PreparedQuery.queryFor(domainType)
.withCypherQuery(cypherStatement)
.withParameters(parameters)
.usingMappingFunction(neo4jMappingContext.getRequiredMappingFunctionFor(domainType))
.build();
PreparedQuery<T> preparedQuery = PreparedQuery.queryFor(domainType).withCypherQuery(cypherStatement)
.withParameters(parameters).usingMappingFunction(neo4jMappingContext.getRequiredMappingFunctionFor(domainType))
.build();
return toExecutableQuery(preparedQuery);
}
private void processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
@Nullable String inDatabase) {
@Nullable String inDatabase) {
processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine());
processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase,
new NestedRelationshipProcessingStateMachine());
}
private void processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
@Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
@Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);
Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty());
@@ -411,18 +392,17 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
// create context to bundle parameters
NestedRelationshipContext relationshipContext = NestedRelationshipContext
.of(association, propertyAccessor, neo4jPersistentEntity);
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
neo4jPersistentEntity);
Collection<?> relatedValuesToStore = Relationships
.unifyRelationshipValue(relationshipContext.getInverse(), relationshipContext.getValue());
Collection<?> relatedValuesToStore = Relationships.unifyRelationshipValue(relationshipContext.getInverse(),
relationshipContext.getValue());
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse();
// break recursive procession and deletion of previously created relationships
ProcessState processState = stateMachine
.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
ProcessState processState = stateMachine.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
if (processState == ProcessState.PROCESSED_BOTH) {
return;
}
@@ -431,14 +411,13 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
// this avoids the usage of cache but might have significant impact on overall performance
if (!neo4jPersistentEntity.isNew(parentObject)) {
Neo4jPersistentEntity<?> previouslyRelatedPersistentEntity = neo4jMappingContext
.getPersistentEntity(relationshipContext.getAssociationTargetType());
.getPersistentEntity(relationshipContext.getAssociationTargetType());
Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity,
relationshipDescription, previouslyRelatedPersistentEntity);
relationshipDescription, previouslyRelatedPersistentEntity);
neo4jClient.query(renderer.render(relationshipRemoveQuery))
.in(inDatabase)
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME).run();
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase).bind(convertIdValues(fromId))
.to(Constants.FROM_ID_PARAMETER_NAME).run();
}
// nothing to do because there is nothing to map
@@ -451,33 +430,27 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
for (Object relatedValueToStore : relatedValuesToStore) {
// here map entry is not always anymore a dynamic association
Object valueToBeSavedPreEvt = relationshipContext
.identifyAndExtractRelationshipValue(relatedValueToStore);
Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipValue(relatedValueToStore);
valueToBeSavedPreEvt = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt);
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
Long relatedInternalId = saveRelatedNode(valueToBeSavedPreEvt,
relationshipContext.getAssociationTargetType(),
targetNodeDescription, inDatabase);
Long relatedInternalId = saveRelatedNode(valueToBeSavedPreEvt, relationshipContext.getAssociationTargetType(),
targetNodeDescription, inDatabase);
RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement(
neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId,
relatedValueToStore);
RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement(neo4jMappingContext,
neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValueToStore);
neo4jClient.query(renderer.render(statementHolder.getRelationshipCreationQuery()))
.in(inDatabase)
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME)
.bindAll(statementHolder.getProperties())
.run();
neo4jClient.query(renderer.render(statementHolder.getRelationshipCreationQuery())).in(inDatabase)
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME).bindAll(statementHolder.getProperties())
.run();
// if an internal id is used this must get set to link this entity in the next iteration
if (targetNodeDescription.isUsingInternalIds()) {
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
.getPropertyAccessor(valueToBeSavedPreEvt);
targetPropertyAccessor
.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId);
.getPropertyAccessor(valueToBeSavedPreEvt);
targetPropertyAccessor.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId);
}
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
processNestedRelations(targetNodeDescription, valueToBeSavedPreEvt, inDatabase, stateMachine);
@@ -486,15 +459,15 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
});
}
private <Y> Long saveRelatedNode(Object entity, Class<Y> entityType, NodeDescription targetNodeDescription, @Nullable String inDatabase) {
private <Y> Long saveRelatedNode(Object entity, Class<Y> entityType, NodeDescription targetNodeDescription,
@Nullable String inDatabase) {
DynamicLabels dynamicLabels = determineDynamicLabels(entity, (Neo4jPersistentEntity) targetNodeDescription, inDatabase);
DynamicLabels dynamicLabels = determineDynamicLabels(entity, (Neo4jPersistentEntity) targetNodeDescription,
inDatabase);
Optional<Long> optionalSavedNodeId = neo4jClient
.query(() -> renderer
.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels)))
.in(inDatabase)
.bind((Y) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
.fetchAs(Long.class).one();
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels)))
.in(inDatabase).bind((Y) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
.fetchAs(Long.class).one();
if (((Neo4jPersistentEntity) targetNodeDescription).hasVersionProperty() && !optionalSavedNodeId.isPresent()) {
throw new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE);
@@ -517,15 +490,10 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
@Override
public <T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery) {
Neo4jClient.MappingSpec<T> mappingSpec = this
.neo4jClient.query(preparedQuery.getCypherQuery())
.in(getDatabaseName())
.bindAll(preparedQuery.getParameters())
.fetchAs(preparedQuery.getResultType());
Neo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery
.getOptionalMappingFunction()
.map(f -> mappingSpec.mappedBy(f))
.orElse(mappingSpec);
Neo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(preparedQuery.getCypherQuery())
.in(getDatabaseName()).bindAll(preparedQuery.getParameters()).fetchAs(preparedQuery.getResultType());
Neo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery.getOptionalMappingFunction()
.map(f -> mappingSpec.mappedBy(f)).orElse(mappingSpec);
return new DefaultExecutableQuery<>(preparedQuery, fetchSpec);
}
@@ -555,14 +523,13 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
}
public T getRequiredSingleResult() {
return fetchSpec.one()
.orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
return fetchSpec.one().orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
}
}
/**
* Utility class that orchestrates {@link EntityCallbacks}.
* All the methods provided here check for their availability and do nothing when an event cannot be published.
* Utility class that orchestrates {@link EntityCallbacks}. All the methods provided here check for their availability
* and do nothing when an event cannot be published.
*/
final class Neo4jEvents {

View File

@@ -25,10 +25,9 @@ import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.lang.Nullable;
/**
* Working on nested relationships happens in a certain algorithmic context.
* This context enables a tight cohesion between the algorithmic steps and the data, these steps are performed on.
* In our the interaction happens between the data that describes the relationship and the specific steps of
* the algorithm.
* Working on nested relationships happens in a certain algorithmic context. This context enables a tight cohesion
* between the algorithmic steps and the data, these steps are performed on. In our the interaction happens between the
* data that describes the relationship and the specific steps of the algorithm.
*
* @author Philipp Tölle
* @author Gerrit Meier
@@ -43,7 +42,7 @@ final class NestedRelationshipContext {
private final boolean inverseValueIsEmpty;
private NestedRelationshipContext(Neo4jPersistentProperty inverse, @Nullable Object value,
RelationshipDescription relationship, Class<?> associationTargetType, boolean inverseValueIsEmpty) {
RelationshipDescription relationship, Class<?> associationTargetType, boolean inverseValueIsEmpty) {
this.inverse = inverse;
this.value = value;
this.relationship = relationship;
@@ -92,25 +91,20 @@ final class NestedRelationshipContext {
}
static NestedRelationshipContext of(Association<Neo4jPersistentProperty> handler,
PersistentPropertyAccessor<?> propertyAccessor,
Neo4jPersistentEntity<?> neo4jPersistentEntity) {
PersistentPropertyAccessor<?> propertyAccessor, Neo4jPersistentEntity<?> neo4jPersistentEntity) {
Neo4jPersistentProperty inverse = handler.getInverse();
boolean inverseValueIsEmpty = propertyAccessor.getProperty(inverse) == null;
Object value = propertyAccessor.getProperty(inverse);
RelationshipDescription relationship = neo4jPersistentEntity
.getRelationships().stream()
.filter(r -> r.getFieldName().equals(inverse.getName()))
.findFirst().get();
RelationshipDescription relationship = neo4jPersistentEntity.getRelationships().stream()
.filter(r -> r.getFieldName().equals(inverse.getName())).findFirst().get();
// if we have a relationship with properties, the targetNodeType is the map key
Class<?> associationTargetType = relationship.hasRelationshipProperties()
? inverse.getComponentType()
: inverse.getAssociationTargetType();
Class<?> associationTargetType = relationship.hasRelationshipProperties() ? inverse.getComponentType()
: inverse.getAssociationTargetType();
return new NestedRelationshipContext(inverse, value, relationship, associationTargetType,
inverseValueIsEmpty);
return new NestedRelationshipContext(inverse, value, relationship, associationTargetType, inverseValueIsEmpty);
}
}

View File

@@ -34,10 +34,7 @@ import org.springframework.lang.Nullable;
final class NestedRelationshipProcessingStateMachine {
enum ProcessState {
PROCESSED_NONE,
PROCESSED_BOTH,
PROCESSED_ONLY_RELATIONSHIP,
PROCESSED_ALL_VALUES
PROCESSED_NONE, PROCESSED_BOTH, PROCESSED_ONLY_RELATIONSHIP, PROCESSED_ALL_VALUES
}
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
@@ -56,7 +53,7 @@ final class NestedRelationshipProcessingStateMachine {
/**
* @param relationshipDescription Check whether this relationship description has been processed
* @param valuesToStore Check whether all the values in the collection have been processed
* @param valuesToStore Check whether all the values in the collection have been processed
* @return The state of things processed
*/
ProcessState getStateOf(RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {
@@ -84,7 +81,7 @@ final class NestedRelationshipProcessingStateMachine {
* Marks the passed objects as processed
*
* @param relationshipDescription To be marked as processed
* @param valuesToStore If not {@literal null}, all non-null values will be marked as processed
* @param valuesToStore If not {@literal null}, all non-null values will be marked as processed
*/
void markAsProcessed(RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {

View File

@@ -27,13 +27,12 @@ import org.neo4j.driver.types.TypeSystem;
import org.springframework.lang.Nullable;
/**
* Typed preparation of a query that is used to create either an executable query.
* Executable queries come in two fashions: imperative and reactive. Depending on which client is used to retrieve one,
* you get one or the other.
* Typed preparation of a query that is used to create either an executable query. Executable queries come in two
* fashions: imperative and reactive. Depending on which client is used to retrieve one, you get one or the other.
* <p>
* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure
* that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected
* by the mapping function to work correctly.
* that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is
* expected by the mapping function to work correctly.
*
* @param <T> The type of the objects returned by this query.
* @author Michael J. Simons
@@ -118,8 +117,7 @@ public final class PreparedQuery<T> {
return this;
}
public OptionalBuildSteps<CT> usingMappingFunction(
@Nullable BiFunction<TypeSystem, Record, ?> newMappingFunction) {
public OptionalBuildSteps<CT> usingMappingFunction(@Nullable BiFunction<TypeSystem, Record, ?> newMappingFunction) {
this.mappingFunction = newMappingFunction;
return this;
}

View File

@@ -38,7 +38,8 @@ public interface ReactiveDatabaseSelectionProvider {
Mono<DatabaseSelection> getDatabaseSelection();
/**
* Creates a statically configured database selection provider always selecting the database with the given name {@code databaseName}.
* Creates a statically configured database selection provider always selecting the database with the given name
* {@code databaseName}.
*
* @param databaseName The database name to use, must not be null nor empty.
* @return A statically configured database name provider.

View File

@@ -73,8 +73,9 @@ public interface ReactiveNeo4jClient {
/**
* Delegates interaction with the default database to the given callback.
*
* @param callback A function receiving a reactive statement runner for database interaction that can optionally return a publisher with none or exactly one element
* @param <T> The type of the result being produced
* @param callback A function receiving a reactive statement runner for database interaction that can optionally
* return a publisher with none or exactly one element
* @param <T> The type of the result being produced
* @return A single publisher containing none or exactly one element that will be produced by the callback
*/
<T> OngoingDelegation<T> delegateTo(Function<RxQueryRunner, Mono<T>> callback);
@@ -86,8 +87,8 @@ public interface ReactiveNeo4jClient {
interface MappingSpec<T> extends RecordFetchSpec<T> {
/**
* The mapping function is responsible to turn one record into one domain object. It will receive the record
* itself and in addition, the type system that the Neo4j Java-Driver used while executing the query.
* The mapping function is responsible to turn one record into one domain object. It will receive the record itself
* and in addition, the type system that the Neo4j Java-Driver used while executing the query.
*
* @param mappingFunction The mapping function used to create new domain objects
* @return A specification how to fetch one or more records.
@@ -124,7 +125,9 @@ public interface ReactiveNeo4jClient {
}
/**
* Contract for a runnable query that can be either run returning it's result, run without results or be parameterized.
* Contract for a runnable query that can be either run returning it's result, run without results or be
* parameterized.
*
* @since 1.0
*/
interface RunnableSpec extends RunnableSpecTightToDatabase {
@@ -140,6 +143,7 @@ public interface ReactiveNeo4jClient {
/**
* Contract for a runnable query inside a dedicated database.
*
* @since 1.0
*/
interface RunnableSpecTightToDatabase extends BindSpec<RunnableSpecTightToDatabase> {
@@ -148,7 +152,7 @@ public interface ReactiveNeo4jClient {
* Create a mapping for each record return to a specific type.
*
* @param targetClass The class each record should be mapped to
* @param <T> The type of the class
* @param <T> The type of the class
* @return A mapping spec that allows specifying a mapping function
*/
<T> MappingSpec<T> fetchAs(Class<T> targetClass);
@@ -161,8 +165,8 @@ public interface ReactiveNeo4jClient {
RecordFetchSpec<Map<String, Object>> fetch();
/**
* Execute the query and discard the results. It returns the drivers result summary, including various counters
* and other statistics.
* Execute the query and discard the results. It returns the drivers result summary, including various counters and
* other statistics.
*
* @return A mono containing the native summary of the query.
*/

View File

@@ -52,7 +52,7 @@ public interface ReactiveNeo4jOperations {
/**
* Counts the number of entities of a given type.
*
* @param statement the Cypher {@link Statement} that returns the count.
* @param statement the Cypher {@link Statement} that returns the count.
* @param parameters Map of parameters. Must not be {@code null}.
* @return the number of instances stored in the database. Guaranteed to be not {@code null}.
*/
@@ -79,7 +79,7 @@ public interface ReactiveNeo4jOperations {
* Load all entities of a given type.
*
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Flux<T> findAll(Class<T> domainType);
@@ -87,9 +87,9 @@ public interface ReactiveNeo4jOperations {
/**
* Load all entities of a given type by executing given statement.
*
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Flux<T> findAll(Statement statement, Class<T> domainType);
@@ -97,10 +97,10 @@ public interface ReactiveNeo4jOperations {
/**
* Load all entities of a given type by executing given statement with parameters.
*
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Flux<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType);
@@ -108,10 +108,10 @@ public interface ReactiveNeo4jOperations {
/**
* Load one entity of a given type by executing given statement with parameters.
*
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param statement Cypher {@link Statement}. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Mono<T> findOne(Statement statement, Map<String, Object> parameters, Class<T> domainType);
@@ -119,9 +119,9 @@ public interface ReactiveNeo4jOperations {
/**
* Load all entities of a given type by executing given statement.
*
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Flux<T> findAll(String cypherQuery, Class<T> domainType);
@@ -129,10 +129,10 @@ public interface ReactiveNeo4jOperations {
/**
* Load all entities of a given type by executing given statement with parameters.
*
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Flux<T> findAll(String cypherQuery, Map<String, Object> parameters, Class<T> domainType);
@@ -140,10 +140,10 @@ public interface ReactiveNeo4jOperations {
/**
* Load one entity of a given type by executing given statement with parameters.
*
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param cypherQuery Cypher query string. Must not be {@code null}.
* @param parameters Map of parameters. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Mono<T> findOne(String cypherQuery, Map<String, Object> parameters, Class<T> domainType);
@@ -151,9 +151,9 @@ public interface ReactiveNeo4jOperations {
/**
* Load an entity from the database.
*
* @param id the id of the entity to load. Must not be {@code null}.
* @param id the id of the entity to load. Must not be {@code null}.
* @param domainType the type of the entity. Must not be {@code null}.
* @param <T> the type of the entity.
* @param <T> the type of the entity.
* @return the loaded entity. Might return an empty optional.
*/
<T> Mono<T> findById(Object id, Class<T> domainType);
@@ -161,9 +161,9 @@ public interface ReactiveNeo4jOperations {
/**
* Load all entities of a given type that are identified by the given ids.
*
* @param ids of the entities identifying the entities to load. Must not be {@code null}.
* @param ids of the entities identifying the entities to load. Must not be {@code null}.
* @param domainType the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @param <T> the type of the entities. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
*/
<T> Flux<T> findAllById(Iterable<?> ids, Class<T> domainType);
@@ -172,7 +172,7 @@ public interface ReactiveNeo4jOperations {
* Saves an instance of an entity, including all the related entities of the entity.
*
* @param instance the entity to be saved. Must not be {@code null}.
* @param <T> the type of the entity.
* @param <T> the type of the entity.
* @return the saved instance.
*/
<T> Mono<T> save(T instance);
@@ -181,7 +181,7 @@ public interface ReactiveNeo4jOperations {
* Saves several instances of an entity, including all the related entities of the entity.
*
* @param instances the instances to be saved. Must not be {@code null}.
* @param <T> the type of the entity.
* @param <T> the type of the entity.
* @return the saved instances.
*/
<T> Flux<T> saveAll(Iterable<T> instances);
@@ -189,18 +189,18 @@ public interface ReactiveNeo4jOperations {
/**
* Deletes a single entity including all entities related to that entity.
*
* @param id the id of the entity to be deleted. Must not be {@code null}.
* @param id the id of the entity to be deleted. Must not be {@code null}.
* @param domainType the type of the entity
* @param <T> the type of the entity.
* @param <T> the type of the entity.
*/
<T> Mono<Void> deleteById(Object id, Class<T> domainType);
/**
* Deletes all entities with one of the given ids, including all entities related to that entity.
*
* @param ids the ids of the entities to be deleted. Must not be {@code null}.
* @param ids the ids of the entities to be deleted. Must not be {@code null}.
* @param domainType the type of the entity
* @param <T> the type of the entity.
* @param <T> the type of the entity.
*/
<T> Mono<Void> deleteAllById(Iterable<?> ids, Class<T> domainType);
@@ -216,7 +216,7 @@ public interface ReactiveNeo4jOperations {
* an optional mapping function, and turns it into an executable query.
*
* @param preparedQuery prepared query that should get converted to an executable query
* @param <T> The type of the objects returned by this query.
* @param <T> The type of the objects returned by this query.
* @return An executable query
*/
<T> Mono<ExecutableQuery<T>> toExecutableQuery(PreparedQuery<T> preparedQuery);

View File

@@ -90,7 +90,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
private final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
public ReactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext,
ReactiveDatabaseSelectionProvider databaseSelectionProvider) {
ReactiveDatabaseSelectionProvider databaseSelectionProvider) {
Assert.notNull(neo4jClient, "The Neo4jClient is required");
Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required");
@@ -107,8 +107,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
public Mono<Long> count(Class<?> domainType) {
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
.returning(Functions.count(asterisk())).build();
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData).returning(Functions.count(asterisk())).build();
return count(statement);
}
@@ -130,10 +129,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
@Override
public Mono<Long> count(String cypherQuery, Map<String, Object> parameters) {
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class)
.withCypherQuery(cypherQuery)
.withParameters(parameters)
.build();
PreparedQuery<Long> preparedQuery = PreparedQuery.queryFor(Long.class).withCypherQuery(cypherQuery)
.withParameters(parameters).build();
return this.toExecutableQuery(preparedQuery).flatMap(ExecutableQuery::getSingleResult);
}
@@ -142,7 +139,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator.prepareMatchOf(entityMetaData)
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
return createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults);
}
@@ -152,7 +149,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
return createExecutableQuery(domainType, statement).flatMapMany(ExecutableQuery::getResults);
}
@Override public <T> Flux<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
@Override
public <T> Flux<T> findAll(Statement statement, Map<String, Object> parameters, Class<T> domainType) {
return createExecutableQuery(domainType, statement, parameters).flatMapMany(ExecutableQuery::getResults);
}
@@ -183,12 +181,11 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
.build();
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
return createExecutableQuery(domainType, statement, singletonMap(Constants.NAME_OF_ID, convertIdValues(id)))
.flatMap(ExecutableQuery::getSingleResult);
.flatMap(ExecutableQuery::getSingleResult);
}
@Override
@@ -196,18 +193,17 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS))))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData))
.build();
.prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().in((parameter(Constants.NAME_OF_IDS))))
.returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build();
return createExecutableQuery(domainType, statement, singletonMap(Constants.NAME_OF_IDS, convertIdValues(ids)))
.flatMapMany(ExecutableQuery::getResults);
.flatMapMany(ExecutableQuery::getResults);
}
private Object convertIdValues(Object idValues) {
return neo4jMappingContext.getConverter()
.writeValueFromProperty(idValues, ClassTypeInformation.from(idValues.getClass()));
return neo4jMappingContext.getConverter().writeValueFromProperty(idValues,
ClassTypeInformation.from(idValues.getClass()));
}
@Override
@@ -219,67 +215,56 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
private <T> Mono<T> saveImpl(T instance, @Nullable String inDatabase) {
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(instance.getClass());
return Mono.just(instance)
.flatMap(eventSupport::maybeCallBeforeBind)
.flatMap(entity -> determineDynamicLabels(entity, entityMetaData, inDatabase))
.flatMap(t -> {
T entity = t.getT1();
DynamicLabels dynamicLabels = t.getT2();
return Mono.just(instance).flatMap(eventSupport::maybeCallBeforeBind)
.flatMap(entity -> determineDynamicLabels(entity, entityMetaData, inDatabase)).flatMap(t -> {
T entity = t.getT1();
DynamicLabels dynamicLabels = t.getT2();
Statement saveStatement = cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels);
Statement saveStatement = cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels);
Mono<Long> idMono =
this.neo4jClient.query(() -> renderer.render(saveStatement))
.in(inDatabase)
.bind((T) entity)
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entity.getClass()))
.fetchAs(Long.class).one()
.switchIfEmpty(Mono.defer(() -> {
if (entityMetaData.hasVersionProperty()) {
return Mono.error(
() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
}
return Mono.empty();
}));
Mono<Long> idMono = this.neo4jClient.query(() -> renderer.render(saveStatement)).in(inDatabase)
.bind((T) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entity.getClass()))
.fetchAs(Long.class).one().switchIfEmpty(Mono.defer(() -> {
if (entityMetaData.hasVersionProperty()) {
return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
}
return Mono.empty();
}));
if (!entityMetaData.isUsingInternalIds()) {
return idMono.then(processRelations(entityMetaData, entity, inDatabase)).thenReturn(entity);
} else {
return idMono.map(internalId -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entity);
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
if (!entityMetaData.isUsingInternalIds()) {
return idMono.then(processRelations(entityMetaData, entity, inDatabase)).thenReturn(entity);
} else {
return idMono.map(internalId -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entity);
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
return propertyAccessor.getBean();
}).flatMap(savedEntity -> processRelations(entityMetaData, savedEntity, inDatabase)
.thenReturn(savedEntity));
}
});
return propertyAccessor.getBean();
}).flatMap(
savedEntity -> processRelations(entityMetaData, savedEntity, inDatabase).thenReturn(savedEntity));
}
});
}
private <T> Mono<Tuple2<T, DynamicLabels>> determineDynamicLabels(
T entityToBeSaved, Neo4jPersistentEntity<?> entityMetaData, @Nullable String inDatabase
) {
private <T> Mono<Tuple2<T, DynamicLabels>> determineDynamicLabels(T entityToBeSaved,
Neo4jPersistentEntity<?> entityMetaData, @Nullable String inDatabase) {
return entityMetaData.getDynamicLabelsProperty().map(p -> {
PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
ReactiveNeo4jClient.RunnableSpecTightToDatabase runnableQuery = neo4jClient
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
.in(inDatabase)
.bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty())).to(Constants.NAME_OF_ID)
.bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM);
.query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData)))
.in(inDatabase).bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty()))
.to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM);
if (entityMetaData.hasVersionProperty()) {
runnableQuery = runnableQuery
.bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty()) - 1)
.to(Constants.NAME_OF_VERSION_PARAM);
.bind((Long) propertyAccessor.getProperty(entityMetaData.getRequiredVersionProperty()) - 1)
.to(Constants.NAME_OF_VERSION_PARAM);
}
return runnableQuery.fetch().one()
.map(m -> (Collection<String>) m.get(Constants.NAME_OF_LABELS))
.switchIfEmpty(Mono.just(Collections.emptyList()))
.zipWith(Mono.just((Collection<String>) propertyAccessor.getProperty(p)))
.map(t -> Tuples.of(entityToBeSaved, new DynamicLabels(t.getT1(), t.getT2())));
return runnableQuery.fetch().one().map(m -> (Collection<String>) m.get(Constants.NAME_OF_LABELS))
.switchIfEmpty(Mono.just(Collections.emptyList()))
.zipWith(Mono.just((Collection<String>) propertyAccessor.getProperty(p)))
.map(t -> Tuples.of(entityToBeSaved, new DynamicLabels(t.getT1(), t.getT2())));
}).orElse(Mono.just(Tuples.of(entityToBeSaved, DynamicLabels.EMPTY)));
}
@@ -304,37 +289,35 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
if (entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()) {
log.debug("Saving entities using single statements.");
return getDatabaseName().flatMapMany(databaseName ->
Flux.fromIterable(entities).flatMap(e -> this.saveImpl(e, databaseName.getValue())));
return getDatabaseName().flatMapMany(
databaseName -> Flux.fromIterable(entities).flatMap(e -> this.saveImpl(e, databaseName.getValue())));
}
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass);
return getDatabaseName().flatMapMany(databaseName ->
Flux.fromIterable(entities)
.flatMap(eventSupport::maybeCallBeforeBind)
.collectList()
.flatMapMany(
entitiesToBeSaved -> Mono
.defer(() -> { // Defer the actual save statement until the previous flux completes
List<Map<String, Object>> boundedEntityList = entitiesToBeSaved.stream()
.map(binderFunction)
.collect(toList());
return getDatabaseName().flatMapMany(databaseName -> Flux.fromIterable(entities)
.flatMap(eventSupport::maybeCallBeforeBind).collectList().flatMapMany(entitiesToBeSaved -> Mono.defer(() -> { // Defer
// the
// actual
// save
// statement
// until
// the
// previous
// flux
// completes
List<Map<String, Object>> boundedEntityList = entitiesToBeSaved.stream().map(binderFunction)
.collect(toList());
return neo4jClient
.query(() -> renderer
.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.in(databaseName.getValue())
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run();
})
.doOnNext(resultSummary -> {
SummaryCounters counters = resultSummary.counters();
log.debug(() -> String.format(
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
counters.relationshipsDeleted(), counters.propertiesSet()));
})
.thenMany(Flux.fromIterable(entitiesToBeSaved))
));
return neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.in(databaseName.getValue()).bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run();
}).doOnNext(resultSummary -> {
SummaryCounters counters = resultSummary.counters();
log.debug(() -> String.format(
"Created %d and deleted %d nodes, created %d and deleted %d relationships and set %d properties.",
counters.nodesCreated(), counters.nodesDeleted(), counters.relationshipsCreated(),
counters.relationshipsDeleted(), counters.propertiesSet()));
}).thenMany(Flux.fromIterable(entitiesToBeSaved))));
}
@Override
@@ -345,10 +328,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
Condition condition = entityMetaData.getIdExpression().in(parameter(nameOfParameter));
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
return getDatabaseName().flatMap(databaseName ->
this.neo4jClient.query(() -> renderer.render(statement))
.in(databaseName.getValue())
.bind(ids).to(nameOfParameter).run().then());
return getDatabaseName().flatMap(databaseName -> this.neo4jClient.query(() -> renderer.render(statement))
.in(databaseName.getValue()).bind(ids).to(nameOfParameter).run().then());
}
@Override
@@ -361,10 +342,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
Condition condition = entityMetaData.getIdExpression().isEqualTo(parameter(nameOfParameter));
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
return getDatabaseName().flatMap(databaseName ->
this.neo4jClient.query(() -> renderer.render(statement))
.in(databaseName.getValue())
.bind(id).to(nameOfParameter).run().then());
return getDatabaseName().flatMap(databaseName -> this.neo4jClient.query(() -> renderer.render(statement))
.in(databaseName.getValue()).bind(id).to(nameOfParameter).run().then());
}
@Override
@@ -372,8 +351,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType);
Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData);
return getDatabaseName().flatMap(databaseName ->
this.neo4jClient.query(() -> renderer.render(statement))
return getDatabaseName().flatMap(databaseName -> this.neo4jClient.query(() -> renderer.render(statement))
.in(databaseName.getValue()).run().then());
}
@@ -386,29 +364,29 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
}
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, Statement statement,
Map<String, Object> parameters) {
Map<String, Object> parameters) {
return createExecutableQuery(domainType, renderer.render(statement), parameters);
}
private <T> Mono<ExecutableQuery<T>> createExecutableQuery(Class<T> domainType, String cypherQuery,
Map<String, Object> parameters) {
Map<String, Object> parameters) {
PreparedQuery<T> preparedQuery = PreparedQuery.queryFor(domainType)
.withCypherQuery(cypherQuery)
.withParameters(parameters)
.usingMappingFunction(this.neo4jMappingContext.getRequiredMappingFunctionFor(domainType))
.build();
PreparedQuery<T> preparedQuery = PreparedQuery.queryFor(domainType).withCypherQuery(cypherQuery)
.withParameters(parameters)
.usingMappingFunction(this.neo4jMappingContext.getRequiredMappingFunctionFor(domainType)).build();
return this.toExecutableQuery(preparedQuery);
}
private Mono<Void> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject, @Nullable String inDatabase) {
private Mono<Void> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
@Nullable String inDatabase) {
return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine());
return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase,
new NestedRelationshipProcessingStateMachine());
}
private Mono<Void> processNestedRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject,
@Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
@Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) {
return Mono.defer(() -> {
PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);
@@ -418,19 +396,17 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) association -> {
// create context to bundle parameters
NestedRelationshipContext relationshipContext = NestedRelationshipContext
.of(association, propertyAccessor, neo4jPersistentEntity);
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
neo4jPersistentEntity);
Collection<?> relatedValuesToStore = Relationships
.unifyRelationshipValue(relationshipContext.getInverse(), relationshipContext.getValue());
Collection<?> relatedValuesToStore = Relationships.unifyRelationshipValue(relationshipContext.getInverse(),
relationshipContext.getValue());
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
RelationshipDescription relationshipDescriptionObverse = relationshipDescription
.getRelationshipObverse();
RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse();
// break recursive procession and deletion of previously created relationships
ProcessState processState = stateMachine
.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
ProcessState processState = stateMachine.getStateOf(relationshipDescriptionObverse, relatedValuesToStore);
if (processState == ProcessState.PROCESSED_BOTH) {
return;
}
@@ -439,16 +415,13 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
// this avoids the usage of cache but might have significant impact on overall performance
if (!neo4jPersistentEntity.isNew(parentObject)) {
Neo4jPersistentEntity<?> previouslyRelatedPersistentEntity = neo4jMappingContext
.getPersistentEntity(relationshipContext.getAssociationTargetType());
.getPersistentEntity(relationshipContext.getAssociationTargetType());
Statement relationshipRemoveQuery = cypherGenerator
.createRelationshipRemoveQuery(neo4jPersistentEntity, relationshipDescription,
previouslyRelatedPersistentEntity);
Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity,
relationshipDescription, previouslyRelatedPersistentEntity);
relationshipCreationMonos.add(
neo4jClient.query(renderer.render(relationshipRemoveQuery))
.in(inDatabase)
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME)
.run().checkpoint("delete relationships").then());
neo4jClient.query(renderer.render(relationshipRemoveQuery)).in(inDatabase).bind(convertIdValues(fromId))
.to(Constants.FROM_ID_PARAMETER_NAME).run().checkpoint("delete relationships").then());
}
// nothing to do because there is nothing to map
@@ -460,49 +433,41 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
for (Object relatedValueToStore : relatedValuesToStore) {
Object valueToBeSavedPreEvt = relationshipContext
.identifyAndExtractRelationshipValue(relatedValueToStore);
Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipValue(relatedValueToStore);
Mono<Void> createRelationship = eventSupport
.maybeCallBeforeBind(valueToBeSavedPreEvt)
.flatMap(valueToBeSaved -> {
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
return saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(),
targetNodeDescription, inDatabase)
.flatMap(relatedInternalId -> {
Mono<Void> createRelationship = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt)
.flatMap(valueToBeSaved -> {
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
return saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(),
targetNodeDescription, inDatabase).flatMap(relatedInternalId -> {
// if an internal id is used this must get set to link this entity in the next iteration
if (targetNodeDescription.isUsingInternalIds()) {
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
.getPropertyAccessor(valueToBeSaved);
targetPropertyAccessor
.setProperty(targetNodeDescription.getRequiredIdProperty(),
relatedInternalId);
}
// if an internal id is used this must get set to link this entity in the next iteration
if (targetNodeDescription.isUsingInternalIds()) {
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
.getPropertyAccessor(valueToBeSaved);
targetPropertyAccessor.setProperty(targetNodeDescription.getRequiredIdProperty(),
relatedInternalId);
}
RelationshipStatementHolder statementHolder = RelationshipStatementHolder
.createStatement(
neo4jMappingContext, neo4jPersistentEntity, relationshipContext,
relatedInternalId, relatedValueToStore);
RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement(
neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId,
relatedValueToStore);
// in case of no properties the bind will just return an empty map
Mono<ResultSummary> relationshipCreationMonoNested = neo4jClient
.query(renderer.render(statementHolder.getRelationshipCreationQuery()))
.in(inDatabase)
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME)
.bindAll(statementHolder.getProperties())
.run();
// in case of no properties the bind will just return an empty map
Mono<ResultSummary> relationshipCreationMonoNested = neo4jClient
.query(renderer.render(statementHolder.getRelationshipCreationQuery())).in(inDatabase)
.bind(convertIdValues(fromId)).to(Constants.FROM_ID_PARAMETER_NAME)
.bindAll(statementHolder.getProperties()).run();
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
return relationshipCreationMonoNested.checkpoint()
.then(processNestedRelations(targetNodeDescription, valueToBeSaved,
inDatabase, stateMachine));
} else {
return relationshipCreationMonoNested.checkpoint().then();
}
}).checkpoint();
});
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
return relationshipCreationMonoNested.checkpoint().then(
processNestedRelations(targetNodeDescription, valueToBeSaved, inDatabase, stateMachine));
} else {
return relationshipCreationMonoNested.checkpoint().then();
}
}).checkpoint();
});
relationshipCreationMonos.add(createRelationship);
}
});
@@ -512,31 +477,29 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
}
private <Y> Mono<Long> saveRelatedNode(Object relatedNode, Class<Y> entityType, NodeDescription targetNodeDescription,
@Nullable String inDatabase) {
@Nullable String inDatabase) {
return determineDynamicLabels((Y) relatedNode, (Neo4jPersistentEntity<?>) targetNodeDescription, inDatabase)
.flatMap(t -> {
Y entity = t.getT1();
DynamicLabels dynamicLabels = t.getT2();
.flatMap(t -> {
Y entity = t.getT1();
DynamicLabels dynamicLabels = t.getT2();
return neo4jClient.query(() -> renderer.render(
cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels)))
.in(inDatabase)
.bind((Y) entity)
.with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
.fetchAs(Long.class).one();
})
.switchIfEmpty(Mono.defer(() -> {
if (((Neo4jPersistentEntity) targetNodeDescription).hasVersionProperty()) {
return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
}
return Mono.empty();
}));
return neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels)))
.in(inDatabase).bind((Y) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
.fetchAs(Long.class).one();
}).switchIfEmpty(Mono.defer(() -> {
if (((Neo4jPersistentEntity) targetNodeDescription).hasVersionProperty()) {
return Mono.error(() -> new OptimisticLockingFailureException(OPTIMISTIC_LOCKING_ERROR_MESSAGE));
}
return Mono.empty();
}));
}
private Mono<DatabaseSelection> getDatabaseName() {
return this.databaseSelectionProvider.getDatabaseSelection().switchIfEmpty(Mono.just(DatabaseSelection.undecided()));
return this.databaseSelectionProvider.getDatabaseSelection()
.switchIfEmpty(Mono.just(DatabaseSelection.undecided()));
}
@Override
@@ -544,16 +507,11 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
return getDatabaseName().map(databaseName -> {
Class<T> resultType = preparedQuery.getResultType();
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this
.neo4jClient.query(preparedQuery.getCypherQuery())
.in(databaseName.getValue())
.bindAll(preparedQuery.getParameters())
.fetchAs(resultType);
ReactiveNeo4jClient.MappingSpec<T> mappingSpec = this.neo4jClient.query(preparedQuery.getCypherQuery())
.in(databaseName.getValue()).bindAll(preparedQuery.getParameters()).fetchAs(resultType);
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery
.getOptionalMappingFunction()
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction))
.orElse(mappingSpec);
ReactiveNeo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery.getOptionalMappingFunction()
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)).orElse(mappingSpec);
return new DefaultReactiveExecutableQuery<>(fetchSpec);
});
@@ -596,8 +554,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
}
/**
* Utility class that orchestrates {@link ReactiveEntityCallbacks}.
* All the methods provided here check for their availability and do nothing when an event cannot be published.
* Utility class that orchestrates {@link ReactiveEntityCallbacks}. All the methods provided here check for their
* availability and do nothing when an event cannot be published.
*/
final class ReactiveNeo4jEvents {

View File

@@ -28,9 +28,9 @@ import org.springframework.lang.NonNull;
/**
* The {@link RelationshipStatementHolder} holds the Cypher Statement to create a relationship as well as the optional
* properties that describe the relationship in case of more then a simple relationship.
* By holding the relationship creation cypher together with the properties, we can reuse the same logic in the
* {@link Neo4jTemplate} as well as in the {@link ReactiveNeo4jTemplate}.
* properties that describe the relationship in case of more then a simple relationship. By holding the relationship
* creation cypher together with the properties, we can reuse the same logic in the {@link Neo4jTemplate} as well as in
* the {@link ReactiveNeo4jTemplate}.
*
* @author Philipp Tölle
* @author Michael J. Simons
@@ -44,10 +44,8 @@ final class RelationshipStatementHolder {
this(relationshipCreationQuery, Collections.emptyMap());
}
private RelationshipStatementHolder(
@NonNull Statement relationshipCreationQuery,
@NonNull Map<String, Object> properties
) {
private RelationshipStatementHolder(@NonNull Statement relationshipCreationQuery,
@NonNull Map<String, Object> properties) {
this.relationshipCreationQuery = relationshipCreationQuery;
this.properties = properties;
}
@@ -61,33 +59,24 @@ final class RelationshipStatementHolder {
}
static RelationshipStatementHolder createStatement(Neo4jMappingContext neo4jMappingContext,
Neo4jPersistentEntity<?> neo4jPersistentEntity,
NestedRelationshipContext relationshipContext,
Long relatedInternalId,
Object relatedValue) {
Neo4jPersistentEntity<?> neo4jPersistentEntity, NestedRelationshipContext relationshipContext,
Long relatedInternalId, Object relatedValue) {
if (relationshipContext.hasRelationshipWithProperties()) {
return createStatementForRelationShipWithProperties(neo4jMappingContext, neo4jPersistentEntity,
relationshipContext, relatedInternalId, (Map.Entry) relatedValue);
relationshipContext, relatedInternalId, (Map.Entry) relatedValue);
} else {
return createStatementForRelationshipWithoutProperties(neo4jMappingContext, neo4jPersistentEntity,
relationshipContext, relatedInternalId, relatedValue);
relationshipContext, relatedInternalId, relatedValue);
}
}
private static RelationshipStatementHolder createStatementForRelationShipWithProperties(
Neo4jMappingContext neo4jMappingContext,
Neo4jPersistentEntity<?> neo4jPersistentEntity,
NestedRelationshipContext relationshipContext,
Long relatedInternalId,
Map.Entry relatedValue) {
Neo4jMappingContext neo4jMappingContext, Neo4jPersistentEntity<?> neo4jPersistentEntity,
NestedRelationshipContext relationshipContext, Long relatedInternalId, Map.Entry relatedValue) {
Statement relationshipCreationQuery = CypherGenerator.INSTANCE
.createRelationshipWithPropertiesCreationQuery(
neo4jPersistentEntity,
relationshipContext.getRelationship(),
relatedInternalId
);
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.createRelationshipWithPropertiesCreationQuery(
neo4jPersistentEntity, relationshipContext.getRelationship(), relatedInternalId);
Map<String, Object> propMap = new HashMap<>();
neo4jMappingContext.getConverter().write(relatedValue.getValue(), propMap);
@@ -95,29 +84,20 @@ final class RelationshipStatementHolder {
}
private static RelationshipStatementHolder createStatementForRelationshipWithoutProperties(
Neo4jMappingContext neo4jMappingContext,
Neo4jPersistentEntity<?> neo4jPersistentEntity,
NestedRelationshipContext relationshipContext,
Long relatedInternalId,
Object relatedValue
) {
Neo4jMappingContext neo4jMappingContext, Neo4jPersistentEntity<?> neo4jPersistentEntity,
NestedRelationshipContext relationshipContext, Long relatedInternalId, Object relatedValue) {
String relationshipType;
if (!relationshipContext.getRelationship().isDynamic()) {
relationshipType = null;
} else {
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation()
.getRequiredComponentType();
TypeInformation<?> keyType = relationshipContext.getInverse().getTypeInformation().getRequiredComponentType();
Object key = ((Map.Entry<?, ?>) relatedValue).getKey();
relationshipType = neo4jMappingContext.getConverter().writeValueFromProperty(key, keyType).asString();
}
Statement relationshipCreationQuery = CypherGenerator.INSTANCE
.createRelationshipCreationQuery(neo4jPersistentEntity,
relationshipContext.getRelationship(),
relationshipType,
relatedInternalId);
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.createRelationshipCreationQuery(
neo4jPersistentEntity, relationshipContext.getRelationship(), relationshipType, relatedInternalId);
return new RelationshipStatementHolder(relationshipCreationQuery);
}
}

View File

@@ -36,8 +36,7 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
private final Class<T> targetClass;
SingleValueMappingFunction(ConversionService conversionService,
Class<T> targetClass) {
SingleValueMappingFunction(ConversionService conversionService, Class<T> targetClass) {
this.conversionService = conversionService;
this.targetClass = targetClass;
}
@@ -50,8 +49,7 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
}
if (record.size() > 1) {
throw new IllegalArgumentException(
"Records with more than one value cannot be converted without a mapper.");
throw new IllegalArgumentException("Records with more than one value cannot be converted without a mapper.");
}
Value source = record.get(0);

View File

@@ -47,8 +47,8 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Additional types that are supported out of the box.
* Mostly all of {@link org.springframework.data.mapping.model.SimpleTypeHolder SimpleTypeHolder's} defaults.
* Additional types that are supported out of the box. Mostly all of
* {@link org.springframework.data.mapping.model.SimpleTypeHolder SimpleTypeHolder's} defaults.
*
* @author Michael J. Simons
* @author Gerrit Meier
@@ -82,12 +82,9 @@ final class AdditionalTypes {
hlp.add(reading(Value.class, short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value));
hlp.add(reading(Value.class, short[].class, AdditionalTypes::asShortArray).andWriting(AdditionalTypes::value));
hlp.add(reading(Value.class, String[].class, AdditionalTypes::asStringArray).andWriting(Values::value));
hlp.add(
reading(Value.class, BigDecimal.class, AdditionalTypes::asBigDecimal).andWriting(AdditionalTypes::value));
hlp.add(
reading(Value.class, BigInteger.class, AdditionalTypes::asBigInteger).andWriting(AdditionalTypes::value));
hlp.add(
reading(Value.class, TemporalAmount.class, AdditionalTypes::asTemporalAmount)
hlp.add(reading(Value.class, BigDecimal.class, AdditionalTypes::asBigDecimal).andWriting(AdditionalTypes::value));
hlp.add(reading(Value.class, BigInteger.class, AdditionalTypes::asBigInteger).andWriting(AdditionalTypes::value));
hlp.add(reading(Value.class, TemporalAmount.class, AdditionalTypes::asTemporalAmount)
.andWriting(AdditionalTypes::value));
hlp.add(reading(Value.class, Instant.class, AdditionalTypes::asInstant).andWriting(AdditionalTypes::value));
hlp.add(reading(Value.class, UUID.class, AdditionalTypes::asUUID).andWriting(AdditionalTypes::value));
@@ -216,12 +213,13 @@ final class AdditionalTypes {
}
/**
* This is a workaround for the fact that Spring Data Commons requires {@link GenericConverter generic converters}
* to have a non-null convertible pair since 2.3. Without it, they get filtered out and thus not registered in a
* conversion service. We do this as an after thought in {@link Neo4jConversions#registerConvertersIn(ConverterRegistry)}.
* This is a workaround for the fact that Spring Data Commons requires {@link GenericConverter generic converters} to
* have a non-null convertible pair since 2.3. Without it, they get filtered out and thus not registered in a
* conversion service. We do this as an after thought in
* {@link Neo4jConversions#registerConvertersIn(ConverterRegistry)}.
* <p>
* This class uses is a {@link GenericConverter} without a concrete pair of convertible types. By making it implement {@link ConditionalConverter} it
* works with Springs conversion service out of the box.
* This class uses is a {@link GenericConverter} without a concrete pair of convertible types. By making it implement
* {@link ConditionalConverter} it works with Springs conversion service out of the box.
*/
static final class EnumArrayConverter implements GenericConverter, ConditionalConverter {
@@ -248,8 +246,8 @@ final class AdditionalTypes {
}
private static boolean describesSupportedEnumVariant(TypeDescriptor typeDescriptor) {
return typeDescriptor.isArray() && Enum.class
.isAssignableFrom(typeDescriptor.getElementTypeDescriptor().getType());
return typeDescriptor.isArray()
&& Enum.class.isAssignableFrom(typeDescriptor.getElementTypeDescriptor().getType());
}
@Override
@@ -266,13 +264,14 @@ final class AdditionalTypes {
Object[] targetArray = (Object[]) Array.newInstance(elementTypeDescriptor.getType(), source.size());
Arrays.setAll(targetArray,
i -> delegate.convert(source.get(i), TypeDescriptor.valueOf(Value.class), elementTypeDescriptor));
i -> delegate.convert(source.get(i), TypeDescriptor.valueOf(Value.class), elementTypeDescriptor));
return targetArray;
} else {
Enum[] source = (Enum[]) object;
return Values.value(Arrays.stream(source).map(e -> delegate
.convert(e, sourceType.getElementTypeDescriptor(), TypeDescriptor.valueOf(Value.class))).toArray());
return Values.value(Arrays.stream(source)
.map(e -> delegate.convert(e, sourceType.getElementTypeDescriptor(), TypeDescriptor.valueOf(Value.class)))
.toArray());
}
}
}
@@ -412,6 +411,5 @@ final class AdditionalTypes {
return Values.value(values);
}
private AdditionalTypes() {
}
private AdditionalTypes() {}
}

View File

@@ -32,8 +32,8 @@ import org.neo4j.driver.types.IsoDuration;
import org.neo4j.driver.types.Point;
/**
* Conversions for all known Cypher types, directly supported by the driver.
* See <a href="https://neo4j.com/docs/driver-manual/current/cypher-values/">Working with Cypher values</a>.
* Conversions for all known Cypher types, directly supported by the driver. See
* <a href="https://neo4j.com/docs/driver-manual/current/cypher-values/">Working with Cypher values</a>.
*
* @author Michael J. Simons
* @since 1.0
@@ -66,6 +66,5 @@ final class CypherTypes {
CONVERTERS = Collections.unmodifiableList(hlp);
}
private CypherTypes() {
}
private CypherTypes() {}
}

View File

@@ -36,12 +36,11 @@ public interface Neo4jConverter extends EntityReader<Object, Record>, EntityWrit
/**
* Reads a {@link Value} returned by the driver and converts it into a {@link Neo4jSimpleTypes simple type} supported
* by Neo4j SDN/RX.
* If the value cannot be converted, a {@link TypeMismatchDataAccessException} will be thrown, it's cause indicating
* the failed conversion.
* by Neo4j SDN. If the value cannot be converted, a {@link TypeMismatchDataAccessException} will be thrown, it's
* cause indicating the failed conversion.
*
* @param value The value to be read, may be null.
* @param type The type information describing the target type.
* @param type The type information describing the target type.
* @return A simple type or null, if the value was {@literal null} or {@link org.neo4j.driver.Values#NULL}.
* @throws TypeMismatchDataAccessException In case the value cannot be converted to the target type
*/
@@ -52,7 +51,7 @@ public interface Neo4jConverter extends EntityReader<Object, Record>, EntityWrit
* Converts an {@link Object} to a driver's value object.
*
* @param value The value to get written, may be null.
* @param type The type information describing the target type.
* @param type The type information describing the target type.
* @return A driver compatible value object.
*/
Value writeValueFromProperty(@Nullable Object value, TypeInformation<?> type);

View File

@@ -45,7 +45,8 @@ import org.springframework.data.neo4j.types.GeographicPoint3d;
* to simple properties as well as to relationships to other things.
* <p>
* The Java driver itself has a good overview of the supported types:
* <a href="https://neo4j.com/docs/driver-manual/1.7/cypher-values/#driver-neo4j-type-system">The Cypher type system</a>.
* <a href="https://neo4j.com/docs/driver-manual/1.7/cypher-values/#driver-neo4j-type-system">The Cypher type
* system</a>.
*
* @author Michael J. Simons
* @since 1.0
@@ -90,6 +91,5 @@ public final class Neo4jSimpleTypes {
*/
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(NEO4J_NATIVE_TYPES, true);
private Neo4jSimpleTypes() {
}
private Neo4jSimpleTypes() {}
}

View File

@@ -37,18 +37,18 @@ import org.springframework.util.Assert;
/**
* Mapping of spatial types.
* <p>
* This replicates the behaviour of SDN+OGM. Spring Data Commons geographic points are x/y based and usually treat
* x/y as lat/long.
* This replicates the behaviour of SDN+OGM. Spring Data Commons geographic points are x/y based and usually treat x/y
* as lat/long.
* <p>
* Neo4j however stores x/y as long/lat when used with an Srid of 4326 or 4979 (those are geographic points). We
* take this into account with our dedicated spatial types which can be used alternatively.
* Neo4j however stores x/y as long/lat when used with an Srid of 4326 or 4979 (those are geographic points). We take
* this into account with our dedicated spatial types which can be used alternatively.
* <p>
* However, when converting an Spring Data Commons point to the internal value, you'll notice that we store y as x and vice versa.
* This is intentionally. We use a hardcoded WGS-84 Srid during storage, thus you'll get back your x as latitude, y as longitude, as
* described above.
* However, when converting an Spring Data Commons point to the internal value, you'll notice that we store y as x and
* vice versa. This is intentionally. We use a hardcoded WGS-84 Srid during storage, thus you'll get back your x as
* latitude, y as longitude, as described above.
* <p>
* The biggest degree of freedom will come from using an attribute of type {@link org.neo4j.driver.types.Point} directly.
* This will be passed on as is.
* The biggest degree of freedom will come from using an attribute of type {@link org.neo4j.driver.types.Point}
* directly. This will be passed on as is.
*
* @author Michael J. Simons
* @since 1.0
@@ -60,13 +60,10 @@ final class SpatialTypes {
static {
List<ConverterBuilder.ConverterAware> hlp = new ArrayList<>();
hlp.add(reading(Value.class, Point.class, SpatialTypes::asSpringDataPoint)
.andWriting(SpatialTypes::value));
hlp.add(reading(Value.class, Point[].class, SpatialTypes::asPointArray)
.andWriting(SpatialTypes::value));
hlp.add(reading(Value.class, Point.class, SpatialTypes::asSpringDataPoint).andWriting(SpatialTypes::value));
hlp.add(reading(Value.class, Point[].class, SpatialTypes::asPointArray).andWriting(SpatialTypes::value));
hlp.add(reading(Value.class, Neo4jPoint.class, SpatialTypes::asNeo4jPoint)
.andWriting(SpatialTypes::value));
hlp.add(reading(Value.class, Neo4jPoint.class, SpatialTypes::asNeo4jPoint).andWriting(SpatialTypes::value));
CONVERTERS = Collections.unmodifiableList(hlp);
}
@@ -92,8 +89,7 @@ final class SpatialTypes {
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude());
} else if (object instanceof GeographicPoint3d) {
GeographicPoint3d point = (GeographicPoint3d) object;
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude(),
point.getHeight());
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude(), point.getHeight());
} else {
throw new IllegalArgumentException("Unsupported point implementation: " + object.getClass());
}
@@ -134,6 +130,5 @@ final class SpatialTypes {
return Values.value(values);
}
private SpatialTypes() {
}
private SpatialTypes() {}
}

View File

@@ -24,75 +24,68 @@ import java.util.function.BiFunction;
import java.util.function.Function;
/**
* This adapter maps a Driver or embedded based {@link TemporalAmount} to a valid Java temporal amount. It tries
* to be as specific as possible: If the amount can be reliable mapped to a {@link Period}, it returns
* a period. If only fields are present that are no estimated time unites, than it returns a {@link Duration}.
* <br><br>
* In cases a user has used Cypher and its <code>duration()</code> function, i.e. like so
* <code>CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s</code>
* a duration object has been created that cannot be represented by either a {@link Period} or {@link Duration}. The user
* has to map it to a plain {@link TemporalAmount} in this cases.
* This adapter maps a Driver or embedded based {@link TemporalAmount} to a valid Java temporal amount. It tries to be
* as specific as possible: If the amount can be reliable mapped to a {@link Period}, it returns a period. If only
* fields are present that are no estimated time unites, than it returns a {@link Duration}. <br>
* <br>
* In cases a user has used Cypher and its <code>duration()</code> function, i.e. like so
* <code>CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s</code> a duration object has been
* created that cannot be represented by either a {@link Period} or {@link Duration}. The user has to map it to a plain
* {@link TemporalAmount} in this cases. <br>
* The Java Driver uses a <code>org.neo4j.driver.v1.types.IsoDuration</code>, embedded uses
* <code>org.neo4j.values.storable.DurationValue</code> for representing a temporal amount, but in the end, they can be
* treated the same.
* However be aware that the temporal amount returned in that case may not be equal to the other one, only represents
* the same amount after normalization.
* treated the same. However be aware that the temporal amount returned in that case may not be equal to the other one,
* only represents the same amount after normalization.
*
* @author Michael J. Simons
*/
final class TemporalAmountAdapter implements Function<TemporalAmount, TemporalAmount> {
private static final int PERIOD_MASK = 0b11100;
private static final int DURATION_MASK = 0b00011;
private static final TemporalUnit[] SUPPORTED_UNITS = {
ChronoUnit.YEARS,
ChronoUnit.MONTHS,
ChronoUnit.DAYS,
ChronoUnit.SECONDS,
ChronoUnit.NANOS
};
private static final int PERIOD_MASK = 0b11100;
private static final int DURATION_MASK = 0b00011;
private static final TemporalUnit[] SUPPORTED_UNITS = { ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.DAYS,
ChronoUnit.SECONDS, ChronoUnit.NANOS };
private static final short FIELD_YEAR = 0;
private static final short FIELD_MONTH = 1;
private static final short FIELD_DAY = 2;
private static final short FIELD_SECONDS = 3;
private static final short FIELD_NANOS = 4;
private static final short FIELD_YEAR = 0;
private static final short FIELD_MONTH = 1;
private static final short FIELD_DAY = 2;
private static final short FIELD_SECONDS = 3;
private static final short FIELD_NANOS = 4;
private static final BiFunction<TemporalAmount, TemporalUnit, Integer> TEMPORAL_UNIT_EXTRACTOR = (d, u) -> {
if (!d.getUnits().contains(u)) {
return 0;
}
return Math.toIntExact(d.get(u));
};
private static final BiFunction<TemporalAmount, TemporalUnit, Integer> TEMPORAL_UNIT_EXTRACTOR = (d, u) -> {
if (!d.getUnits().contains(u)) {
return 0;
}
return Math.toIntExact(d.get(u));
};
@Override
public TemporalAmount apply(TemporalAmount internalTemporalAmountRepresentation) {
@Override
public TemporalAmount apply(TemporalAmount internalTemporalAmountRepresentation) {
int[] values = new int[SUPPORTED_UNITS.length];
int type = 0;
for (int i = 0; i < SUPPORTED_UNITS.length; ++i) {
values[i] = TEMPORAL_UNIT_EXTRACTOR.apply(internalTemporalAmountRepresentation, SUPPORTED_UNITS[i]);
type |= (values[i] == 0) ? 0 : (0b10000 >> i);
}
int[] values = new int[SUPPORTED_UNITS.length];
int type = 0;
for (int i = 0; i < SUPPORTED_UNITS.length; ++i) {
values[i] = TEMPORAL_UNIT_EXTRACTOR.apply(internalTemporalAmountRepresentation, SUPPORTED_UNITS[i]);
type |= (values[i] == 0) ? 0 : (0b10000 >> i);
}
boolean couldBePeriod = couldBePeriod(type);
boolean couldBeDuration = couldBeDuration(type);
boolean couldBePeriod = couldBePeriod(type);
boolean couldBeDuration = couldBeDuration(type);
if (couldBePeriod && !couldBeDuration) {
return Period.of(values[FIELD_YEAR], values[FIELD_MONTH], values[FIELD_DAY]).normalized();
} else if (couldBeDuration && !couldBePeriod) {
return Duration.ofSeconds(values[FIELD_SECONDS]).plusNanos(values[FIELD_NANOS]);
} else {
return internalTemporalAmountRepresentation;
}
}
if (couldBePeriod && !couldBeDuration) {
return Period.of(values[FIELD_YEAR], values[FIELD_MONTH], values[FIELD_DAY]).normalized();
} else if (couldBeDuration && !couldBePeriod) {
return Duration.ofSeconds(values[FIELD_SECONDS]).plusNanos(values[FIELD_NANOS]);
} else {
return internalTemporalAmountRepresentation;
}
}
private static boolean couldBePeriod(int type) {
return (PERIOD_MASK & type) > 0;
}
private static boolean couldBePeriod(int type) {
return (PERIOD_MASK & type) > 0;
}
private static boolean couldBeDuration(int type) {
return (DURATION_MASK & type) > 0;
}
private static boolean couldBeDuration(int type) {
return (DURATION_MASK & type) > 0;
}
}

View File

@@ -75,9 +75,9 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jConverter.class));
/**
* The shared entity instantiators of this context. Those should not be recreated for each entity or even not for
* each query, as otherwise the cache of Spring's org.springframework.data.convert.ClassGeneratingEntityInstantiator
* won't apply
* The shared entity instantiators of this context. Those should not be recreated for each entity or even not for each
* query, as otherwise the cache of Spring's org.springframework.data.convert.ClassGeneratingEntityInstantiator won't
* apply
*/
private static final EntityInstantiators INSTANTIATORS = new EntityInstantiators();
@@ -100,8 +100,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
@Override
public <R> R read(Class<R> targetType, Record record) {
Neo4jPersistentEntity<R> rootNodeDescription =
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType);
Neo4jPersistentEntity<R> rootNodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore
.getNodeDescription(targetType);
try {
List<Value> recordValues = record.values();
@@ -128,7 +128,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
if (queryRoot == null) {
log.warn(() -> String.format("Could not find mappable nodes or relationships inside %s for %s", record,
rootNodeDescription));
rootNodeDescription));
return null; // todo should not be null because of the @nonnullapi annotation in the EntityReader. Fail?
} else {
return map(queryRoot, rootNodeDescription, new KnownObjects());
@@ -149,8 +149,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
if (!valueIsLiteralNullOrNullValue && isCollection(type)) {
Collection<Object> target = createCollection(rawType, type.getComponentType().getType(), value.size());
value.values().forEach(
element -> target.add(conversionService.convert(element, type.getComponentType().getType())));
value.values()
.forEach(element -> target.add(conversionService.convert(element, type.getComponentType().getType())));
return target;
}
@@ -172,8 +172,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
public void write(Object source, Map<String, Object> parameters) {
Map<String, Object> properties = new HashMap<>();
Neo4jPersistentEntity<?> nodeDescription =
(Neo4jPersistentEntity<?>) nodeDescriptionStore.getNodeDescription(source.getClass());
Neo4jPersistentEntity<?> nodeDescription = (Neo4jPersistentEntity<?>) nodeDescriptionStore
.getNodeDescription(source.getClass());
PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source);
nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> {
@@ -193,7 +193,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
if (nodeDescription.hasIdProperty()) {
Neo4jPersistentProperty idProperty = nodeDescription.getRequiredIdProperty();
parameters.put(Constants.NAME_OF_ID,
writeValueFromProperty(propertyAccessor.getProperty(idProperty), idProperty.getTypeInformation()));
writeValueFromProperty(propertyAccessor.getProperty(idProperty), idProperty.getTypeInformation()));
}
// in case of relationship properties ignore internal id property
if (nodeDescription.hasVersionProperty()) {
@@ -213,8 +213,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
if (isCollection(type)) {
Collection<?> sourceCollection = (Collection<?>) value;
Object[] targetCollection = (sourceCollection).stream().map(element ->
conversionService.convert(element, Value.class)).toArray();
Object[] targetCollection = (sourceCollection).stream()
.map(element -> conversionService.convert(element, Value.class)).toArray();
return Values.value(targetCollection);
}
@@ -233,7 +233,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
* Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too.
* Merge happens only when the record contains additional values.
*
* @param node Node whose attributes are about to be merged
* @param node Node whose attributes are about to be merged
* @param record Record that should be merged
* @return
*/
@@ -248,26 +248,24 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
}
/**
* @param queryResult The original query result
* @param queryResult The original query result
* @param nodeDescription The node description of the current entity to be mapped from the result
* @param knownObjects The current list of known objects
* @param <ET> As in entity type
* @param knownObjects The current list of known objects
* @param <ET> As in entity type
* @return
*/
private <ET> ET map(MapAccessor queryResult,
Neo4jPersistentEntity<ET> nodeDescription,
KnownObjects knownObjects) {
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects) {
List<String> allLabels = getLabels(queryResult);
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
.deriveConcreteNodeDescription(nodeDescription, allLabels);
.deriveConcreteNodeDescription(nodeDescription, allLabels);
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
.getNodeDescription();
.getNodeDescription();
Collection<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships,
nodeDescriptionAndLabels.getDynamicLabels());
nodeDescriptionAndLabels.getDynamicLabels());
PersistentPropertyAccessor<ET> propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance);
@@ -275,14 +273,14 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
// Fill simple properties
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
.getPersistenceConstructor()::isConstructorParameter;
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(
queryResult, propertyAccessor, isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels());
.getPersistenceConstructor()::isConstructorParameter;
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels());
concreteNodeDescription.doWithProperties(handler);
// Fill associations
concreteNodeDescription.doWithAssociations(
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationships, knownObjects));
populateFrom(queryResult, propertyAccessor, isConstructorParameter, relationships, knownObjects));
}
return instance;
}
@@ -306,22 +304,17 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
return labels;
}
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription,
MapAccessor values,
KnownObjects knownObjects,
Collection<RelationshipDescription> relationships,
Collection<String> surplusLabels) {
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, MapAccessor values, KnownObjects knownObjects,
Collection<RelationshipDescription> relationships, Collection<String> surplusLabels) {
ParameterValueProvider<Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<Neo4jPersistentProperty>() {
@Override
public Object getParameterValue(PreferredConstructor.Parameter parameter) {
Neo4jPersistentProperty matchingProperty = nodeDescription
.getRequiredPersistentProperty(parameter.getName());
Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(parameter.getName());
if (matchingProperty.isRelationship()) {
return createInstanceOfRelationships(matchingProperty, values, knownObjects, relationships)
.orElse(null);
return createInstanceOfRelationships(matchingProperty, values, knownObjects, relationships).orElse(null);
} else if (matchingProperty.isDynamicLabels()) {
return createDynamicLabelsProperty(matchingProperty.getTypeInformation(), surplusLabels);
}
@@ -329,38 +322,30 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
}
};
return INSTANTIATORS.getInstantiatorFor(nodeDescription)
.createInstance(nodeDescription, parameterValueProvider);
return INSTANTIATORS.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider);
}
private PropertyHandler<Neo4jPersistentProperty> populateFrom(
MapAccessor queryResult,
PersistentPropertyAccessor<?> propertyAccessor,
Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<String> surplusLabels
) {
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<String> surplusLabels) {
return property -> {
if (isConstructorParameter.test(property)) {
return;
}
if (property.isDynamicLabels()) {
propertyAccessor
.setProperty(property, createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels));
propertyAccessor.setProperty(property,
createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels));
} else {
propertyAccessor.setProperty(property,
readValueForProperty(extractValueOf(property, queryResult), property.getTypeInformation()));
readValueForProperty(extractValueOf(property, queryResult), property.getTypeInformation()));
}
};
}
private AssociationHandler<Neo4jPersistentProperty> populateFrom(
MapAccessor queryResult,
PersistentPropertyAccessor<?> propertyAccessor,
Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<RelationshipDescription> relationships,
KnownObjects knownObjects
) {
private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<RelationshipDescription> relationships, KnownObjects knownObjects) {
return association -> {
Neo4jPersistentProperty persistentProperty = association.getInverse();
@@ -369,30 +354,27 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
}
createInstanceOfRelationships(persistentProperty, queryResult, knownObjects, relationships)
.ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value));
.ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value));
};
}
private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty,
MapAccessor values,
KnownObjects knownObjects,
Collection<RelationshipDescription> relationshipDescriptions) {
private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values,
KnownObjects knownObjects, Collection<RelationshipDescription> relationshipDescriptions) {
RelationshipDescription relationshipDescription = relationshipDescriptions.stream()
.filter(r -> r.getFieldName().equals(persistentProperty.getName()))
.findFirst().get();
.filter(r -> r.getFieldName().equals(persistentProperty.getName())).findFirst().get();
String relationshipType = relationshipDescription.getType();
String targetLabel = relationshipDescription.getTarget().getPrimaryLabel();
Neo4jPersistentEntity<?> genericTargetNodeDescription =
(Neo4jPersistentEntity<?>) relationshipDescription.getTarget();
Neo4jPersistentEntity<?> genericTargetNodeDescription = (Neo4jPersistentEntity<?>) relationshipDescription
.getTarget();
List<String> allLabels = getLabels(values);
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
.deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels);
.deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels);
Neo4jPersistentEntity<?> concreteTargetNodeDescription = (Neo4jPersistentEntity<?>) nodeDescriptionAndLabels
.getNodeDescription();
.getNodeDescription();
List<Object> value = new ArrayList<>();
Map<Object, Object> dynamicValue = new HashMap<>();
@@ -409,8 +391,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
TypeInformation<?> actualType = persistentProperty.getTypeInformation().getRequiredActualType();
mappedObjectHandler = (type, mappedObject) -> {
List<Object> bucket = (List<Object>) dynamicValue.computeIfAbsent(keyTransformer.apply(type),
s -> createCollection(actualType.getType(), persistentProperty.getAssociationTargetType(),
values.size()));
s -> createCollection(actualType.getType(), persistentProperty.getAssociationTargetType(), values.size()));
bucket.add(mappedObject);
};
} else if (persistentProperty.isDynamicAssociation()) {
@@ -428,28 +409,21 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
Predicate<Value> isList = entry -> entry instanceof Value && typeSystem.LIST().isTypeOf(entry);
Predicate<Value> containsOnlyRelationships = entry -> entry.asList(Function.identity())
.stream()
.allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry));
Predicate<Value> containsOnlyRelationships = entry -> entry.asList(Function.identity()).stream()
.allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry));
Predicate<Value> containsOnlyNodes = entry -> entry.asList(Function.identity())
.stream()
.allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry));
Predicate<Value> containsOnlyNodes = entry -> entry.asList(Function.identity()).stream()
.allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry));
// find relationships in the result
List<Relationship> allMatchingTypeRelationshipsInResult = StreamSupport
.stream(values.values().spliterator(), false)
.filter(isList.and(containsOnlyRelationships))
.flatMap(entry -> entry.asList(Value::asRelationship).stream())
.filter(r -> r.type().equals(relationshipType))
.collect(toList());
.stream(values.values().spliterator(), false).filter(isList.and(containsOnlyRelationships))
.flatMap(entry -> entry.asList(Value::asRelationship).stream()).filter(r -> r.type().equals(relationshipType))
.collect(toList());
List<Node> allNodesWithMatchingLabelInResult = StreamSupport
.stream(values.values().spliterator(), false)
.filter(isList.and(containsOnlyNodes))
.flatMap(entry -> entry.asList(Value::asNode).stream())
.filter(n -> n.hasLabel(targetLabel))
.collect(toList());
List<Node> allNodesWithMatchingLabelInResult = StreamSupport.stream(values.values().spliterator(), false)
.filter(isList.and(containsOnlyNodes)).flatMap(entry -> entry.asList(Value::asNode).stream())
.filter(n -> n.hasLabel(targetLabel)).collect(toList());
if (allNodesWithMatchingLabelInResult.isEmpty() && allMatchingTypeRelationshipsInResult.isEmpty()) {
return Optional.empty();
@@ -466,8 +440,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
Class<?> propertiesClass = relationshipDescription.getRelationshipPropertiesClass();
Object relationshipProperties = map(possibleRelationship,
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass),
knownObjects);
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass), knownObjects);
relationshipsAndProperties.put(mappedObject, relationshipProperties);
} else {
mappedObjectHandler.accept(possibleRelationship.type(), mappedObject);
@@ -481,27 +454,25 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
Neo4jPersistentProperty idProperty = concreteTargetNodeDescription.getRequiredIdProperty();
// internal (generated) id or external set
String relatedEntityIdKey = idProperty.isInternalIdProperty()
? Constants.NAME_OF_INTERNAL_ID
: concreteTargetNodeDescription.getIdDescription()
.getOptionalGraphPropertyName()
.orElse(idProperty.getName());
String relatedEntityIdKey = idProperty.isInternalIdProperty() ? Constants.NAME_OF_INTERNAL_ID
: concreteTargetNodeDescription.getIdDescription().getOptionalGraphPropertyName()
.orElse(idProperty.getName());
Object idValue = relatedEntity.get(relatedEntityIdKey);
Object valueEntry = knownObjects.computeIfAbsent(idValue,
() -> map(relatedEntity, concreteTargetNodeDescription, knownObjects));
() -> map(relatedEntity, concreteTargetNodeDescription, knownObjects));
if (relationshipDescription.hasRelationshipProperties()) {
Relationship relatedEntityRelationship = relatedEntity.get(
RelationshipDescription.NAME_OF_RELATIONSHIP).asRelationship();
Relationship relatedEntityRelationship = relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP)
.asRelationship();
Class<?> propertiesClass = relationshipDescription.getRelationshipPropertiesClass();
Object relationshipProperties = map(relatedEntityRelationship,
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass),
knownObjects);
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass), knownObjects);
relationshipsAndProperties.put(valueEntry, relationshipProperties);
} else {
mappedObjectHandler.accept(relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(), valueEntry);
mappedObjectHandler.accept(relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(),
valueEntry);
}
}
}
@@ -526,9 +497,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) {
if (property.isInternalIdProperty()) {
return propertyContainer instanceof Node ?
Values.value(((Node) propertyContainer).id()) :
propertyContainer.get(Constants.NAME_OF_INTERNAL_ID);
return propertyContainer instanceof Node ? Values.value(((Node) propertyContainer).id())
: propertyContainer.get(Constants.NAME_OF_INTERNAL_ID);
} else {
String graphPropertyName = property.getPropertyName();
return propertyContainer.get(graphPropertyName);

View File

@@ -25,19 +25,22 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of a {@link IsNewStrategy} that follows our supported identifiers and generators.
* Entities will be treated as new:
* Implementation of a {@link IsNewStrategy} that follows our supported identifiers and generators. Entities will be
* treated as new:
* <ul>
* <li>when using internally generated (database) ids and the id property is {@literal null} or of a numeric primitive less than or equal {@literal 0},</li>
* <li>when using internally generated (database) ids and the id property is {@literal null} or of a numeric primitive
* less than or equal {@literal 0},</li>
* <li>when using externally generated values and the id is {@literal null},</li>
* <li>when using assigned values without a version property or with a version property that is {@literal null}.</li>
* </ul>
* <p>
* An entity will not be treated as new
* <ul>
* <li>when using internally generated (database) ids and the id property has a non-null value greater than {@literal 0},</li>
* <li>when using internally generated (database) ids and the id property has a non-null value greater than
* {@literal 0},</li>
* <li>when using externally generated values and the id property is not {@literal null},</li>
* <li>when using assigned values together with {@link org.springframework.data.annotation.Version @Version} which has already a value not equal to {@literal null} or {@literal 0}.</li>
* <li>when using assigned values together with {@link org.springframework.data.annotation.Version @Version} which has
* already a value not equal to {@literal null} or {@literal 0}.</li>
* </ul>
*
* @author Michael J. Simons
@@ -56,7 +59,7 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
if (idDescription.isExternallyGeneratedId() && valueType.isPrimitive()) {
throw new IllegalArgumentException(String.format("Cannot use %s with externally generated, primitive ids.",
DefaultNeo4jIsNewStrategy.class.getName()));
DefaultNeo4jIsNewStrategy.class.getName()));
}
Function<Object, Object> valueLookup;
@@ -64,7 +67,7 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
if (idDescription.isAssignedId()) {
if (versionProperty == null) {
log.warn(() -> "Instances of " + entityMetaData.getType()
+ " with an assigned id will always be treated as new without version property!");
+ " with an assigned id will always be treated as new without version property!");
valueType = Void.class;
valueLookup = source -> null;
} else {
@@ -85,7 +88,7 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
private @Nullable final Function<Object, Object> valueLookup;
private DefaultNeo4jIsNewStrategy(IdDescription idDescription, Class<?> valueType,
Function<Object, Object> valueLookup) {
Function<Object, Object> valueLookup) {
this.idDescription = idDescription;
this.valueType = valueType;
this.valueLookup = valueLookup;
@@ -122,8 +125,7 @@ class DefaultNeo4jIsNewStrategy implements IsNewStrategy {
}
throw new IllegalArgumentException(
String
.format("Could not determine whether %s is new! Unsupported identifier or version property!", entity));
String.format("Could not determine whether %s is new! Unsupported identifier or version property!", entity));
}
}

View File

@@ -40,14 +40,14 @@ import org.springframework.util.StringUtils;
* @since 1.0
*/
class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
implements Neo4jPersistentEntity<T> {
implements Neo4jPersistentEntity<T> {
private static final Set<Class<?>> VALID_GENERATED_ID_TYPES = Collections.unmodifiableSet(new HashSet<>(
Arrays.asList(Long.class, long.class)));
private static final Set<Class<?>> VALID_GENERATED_ID_TYPES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(Long.class, long.class)));
/**
* If an entity is annotated with {@link Node}, we consider this as an explicit entity
* that should get validated more strictly.
* If an entity is annotated with {@link Node}, we consider this as an explicit entity that should get validated more
* strictly.
*/
private final Boolean isExplicitEntity;
@@ -61,8 +61,7 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
/**
* Projections need to be also be eligible entities but don't define id fields.
*/
@Nullable
private IdDescription idDescription;
@Nullable private IdDescription idDescription;
private final Lazy<Collection<GraphPropertyDescription>> graphProperties;
@@ -79,8 +78,7 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
this.primaryLabel = computePrimaryLabel();
this.additionalLabels = Lazy.of(this::computeAdditionalLabels);
this.graphProperties = Lazy.of(this::computeGraphProperties);
this.dynamicLabelsProperty = Lazy
.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast)
this.dynamicLabelsProperty = Lazy.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast)
.filter(Neo4jPersistentProperty::isDynamicLabels).findFirst().orElse(null));
}
@@ -174,8 +172,8 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
}
});
Assert.state(duplicates.isEmpty(), () ->
String.format("Duplicate definition of propert%s %s in entity %s.", duplicates.size() == 1 ? "y" : "ies", duplicates, getUnderlyingClass()));
Assert.state(duplicates.isEmpty(), () -> String.format("Duplicate definition of propert%s %s in entity %s.",
duplicates.size() == 1 ? "y" : "ies", duplicates, getUnderlyingClass()));
}
private void verifyDynamicAssociations() {
@@ -186,16 +184,14 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
if (inverse.isDynamicAssociation()) {
Relationship relationship = inverse.findAnnotation(Relationship.class);
Assert.state(relationship == null || relationship.type().isEmpty(),
() ->
"Dynamic relationships cannot be used with a fixed type. Omit @Relationship or use @Relationship(direction = "
+ relationship.direction().name() + ") without a type in " + this.getUnderlyingClass()
+ " on field " + inverse.getFieldName() + ".");
() -> "Dynamic relationships cannot be used with a fixed type. Omit @Relationship or use @Relationship(direction = "
+ relationship.direction().name() + ") without a type in " + this.getUnderlyingClass() + " on field "
+ inverse.getFieldName() + ".");
Assert.state(!targetEntities.contains(inverse.getAssociationTargetType()),
() -> this.getUnderlyingClass() + " already contains a dynamic relationship to " + inverse
.getAssociationTargetType()
+ ". Only one dynamic relationship between to entities is permitted."
);
() -> this.getUnderlyingClass() + " already contains a dynamic relationship to "
+ inverse.getAssociationTargetType()
+ ". Only one dynamic relationship between to entities is permitted.");
targetEntities.add(inverse.getAssociationTargetType());
}
});
@@ -212,24 +208,21 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
String propertyName = persistentProperty.getPropertyName();
namesOfPropertiesWithDynamicLabels.add(propertyName);
Assert.state(persistentProperty.isCollectionLike(),
() -> String.format("Property %s on %s must extends %s.", persistentProperty.getFieldName(),
persistentProperty.getOwner().getType(), Collection.class.getName())
);
Assert.state(persistentProperty.isCollectionLike(), () -> String.format("Property %s on %s must extends %s.",
persistentProperty.getFieldName(), persistentProperty.getOwner().getType(), Collection.class.getName()));
});
Assert.state(namesOfPropertiesWithDynamicLabels.size() <= 1, () ->
String.format(
"Multiple properties in entity %s are annotated with @%s: %s.", getUnderlyingClass(),
DynamicLabels.class.getSimpleName(), namesOfPropertiesWithDynamicLabels));
Assert.state(namesOfPropertiesWithDynamicLabels.size() <= 1,
() -> String.format("Multiple properties in entity %s are annotated with @%s: %s.", getUnderlyingClass(),
DynamicLabels.class.getSimpleName(), namesOfPropertiesWithDynamicLabels));
}
/**
* The primary label will get computed and returned by following rules:<br>
* 1. If there is no {@link Node} annotation, use the class name.<br>
* 2. If there is an annotation but it has no properties set, use the class name.<br>
* 3. If only {@link Node#labels()} property is set, use the first one as the primary label
* 4. If the {@link Node#primaryLabel()} property is set, use this as the primary label
* 3. If only {@link Node#labels()} property is set, use the first one as the primary label 4. If the
* {@link Node#primaryLabel()} property is set, use this as the primary label
*
* @return computed primary label
*/
@@ -253,7 +246,7 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
private List<String> computeAdditionalLabels() {
return Stream.concat(computeOwnAdditionalLabels().stream(), computeParentLabels().stream())
.collect(Collectors.toList());
.collect(Collectors.toList());
}
/**
@@ -316,21 +309,20 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
String idGeneratorRef = generatedValueAnnotation.generatorRef();
if (idProperty.getActualType() == UUID.class && idGeneratorClass == GeneratedValue.InternalIdGenerator.class
&& !StringUtils.hasText(idGeneratorRef)) {
&& !StringUtils.hasText(idGeneratorRef)) {
idGeneratorClass = GeneratedValue.UUIDGenerator.class;
}
// Internally generated ids.
if (idGeneratorClass == GeneratedValue.InternalIdGenerator.class && idGeneratorRef.isEmpty()) {
if (idProperty.findAnnotation(Property.class) != null) {
throw new IllegalArgumentException(
"Cannot use internal id strategy with custom property " + propertyName
throw new IllegalArgumentException("Cannot use internal id strategy with custom property " + propertyName
+ " on entity class " + this.getUnderlyingClass().getName());
}
if (!VALID_GENERATED_ID_TYPES.contains(idProperty.getActualType())) {
throw new IllegalArgumentException(
"Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES);
"Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES);
}
return IdDescription.forInternallyGeneratedIds();
@@ -344,9 +336,8 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
public Collection<RelationshipDescription> getRelationships() {
final List<RelationshipDescription> relationships = new ArrayList<>();
this.doWithAssociations((Association<Neo4jPersistentProperty> association) ->
relationships.add((RelationshipDescription) association)
);
this.doWithAssociations(
(Association<Neo4jPersistentProperty> association) -> relationships.add((RelationshipDescription) association));
return Collections.unmodifiableCollection(relationships);
}
@@ -362,8 +353,8 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
@Override
public Collection<GraphPropertyDescription> getGraphPropertiesInHierarchy() {
TreeSet<GraphPropertyDescription> allPropertiesInHierarchy =
new TreeSet<>(Comparator.comparing(GraphPropertyDescription::getPropertyName));
TreeSet<GraphPropertyDescription> allPropertiesInHierarchy = new TreeSet<>(
Comparator.comparing(GraphPropertyDescription::getPropertyName));
allPropertiesInHierarchy.addAll(getGraphProperties());
for (NodeDescription<?> childNodeDescription : getChildNodeDescriptionsInHierarchy()) {

View File

@@ -37,7 +37,7 @@ import org.springframework.util.Assert;
* @since 1.0
*/
class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<Neo4jPersistentProperty>
implements Neo4jPersistentProperty {
implements Neo4jPersistentProperty {
private final Lazy<String> graphPropertyName;
private final Lazy<Boolean> isAssociation;
@@ -47,14 +47,12 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
/**
* Creates a new {@link AnnotationBasedPersistentProperty}.
*
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
* @param simpleTypeHolder type holder
*/
DefaultNeo4jPersistentProperty(Property property,
PersistentEntity<?, Neo4jPersistentProperty> owner,
Neo4jMappingContext mappingContext,
SimpleTypeHolder simpleTypeHolder) {
DefaultNeo4jPersistentProperty(Property property, PersistentEntity<?, Neo4jPersistentProperty> owner,
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder) {
super(property, owner, simpleTypeHolder);
@@ -102,16 +100,15 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
// Try to determine if there is a relationship definition that expresses logically the same relationship
// on the other end.
Optional<RelationshipDescription> obverseRelationshipDescription = obverseOwner.getRelationships().stream()
.filter(rel -> rel.getType().equals(type) && rel.getTarget().equals(this.getOwner()))
.findFirst();
.filter(rel -> rel.getType().equals(type) && rel.getTarget().equals(this.getOwner())).findFirst();
DefaultRelationshipDescription relationshipDescription = new DefaultRelationshipDescription(this,
obverseRelationshipDescription.orElse(null), type, dynamicAssociation, (NodeDescription<?>) getOwner(),
this.getName(), obverseOwner, direction, relationshipPropertiesClass);
obverseRelationshipDescription.orElse(null), type, dynamicAssociation, (NodeDescription<?>) getOwner(),
this.getName(), obverseOwner, direction, relationshipPropertiesClass);
// Update the previous found, if any, relationship with the newly created one as its counterpart.
obverseRelationshipDescription
.ifPresent(relationship -> relationship.setRelationshipObverse(relationshipDescription));
.ifPresent(relationship -> relationship.setRelationshipObverse(relationshipDescription));
return relationshipDescription;
}
@@ -153,12 +150,12 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
return null;
}
org.springframework.data.neo4j.core.schema.Property propertyAnnotation =
this.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class);
org.springframework.data.neo4j.core.schema.Property propertyAnnotation = this
.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class);
String targetName = this.getName();
if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty()
&& propertyAnnotation.name().trim().length() != 0) {
&& propertyAnnotation.name().trim().length() != 0) {
targetName = propertyAnnotation.name().trim();
}
@@ -193,7 +190,6 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
return isAssociation();
}
static String deriveRelationshipType(String name) {
Assert.hasText(name, "The name to derive the type from is required.");

View File

@@ -46,10 +46,9 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
private RelationshipDescription relationshipObverse;
DefaultRelationshipDescription(Neo4jPersistentProperty inverse,
@Nullable RelationshipDescription relationshipObverse,
String type, boolean dynamic, NodeDescription<?> source, String fieldName, NodeDescription<?> target,
Relationship.Direction direction, @Nullable Class<?> relationshipPropertiesClass) {
DefaultRelationshipDescription(Neo4jPersistentProperty inverse, @Nullable RelationshipDescription relationshipObverse,
String type, boolean dynamic, NodeDescription<?> source, String fieldName, NodeDescription<?> target,
Relationship.Direction direction, @Nullable Class<?> relationshipPropertiesClass) {
// the immutable obverse association-wise is always null because we cannot determine them on both sides
// if we consider to support bidirectional relationships.
@@ -76,12 +75,12 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
}
@Override
public NodeDescription<?> getTarget() {
public NodeDescription<?> getTarget() {
return target;
}
@Override
public NodeDescription<?> getSource() {
public NodeDescription<?> getSource() {
return source;
}
@@ -122,12 +121,8 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
@Override
public String toString() {
return "DefaultRelationshipDescription{" +
"type='" + type + '\'' +
", source='" + source + '\'' +
", direction='" + direction + '\'' +
", target='" + target +
'}';
return "DefaultRelationshipDescription{" + "type='" + type + '\'' + ", source='" + source + '\'' + ", direction='"
+ direction + '\'' + ", target='" + target + '}';
}
@Override
@@ -140,7 +135,7 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
}
DefaultRelationshipDescription that = (DefaultRelationshipDescription) o;
return getType().equals(that.getType()) && getTarget().equals(that.getTarget())
&& getSource().equals(that.getSource()) && getDirection().equals(that.getDirection());
&& getSource().equals(that.getSource()) && getDirection().equals(that.getDirection());
}
@Override

View File

@@ -45,15 +45,15 @@ import org.springframework.lang.Nullable;
/**
* An implementation of both a {@link Schema} as well as a Neo4j version of Spring Data's
* {@link org.springframework.data.mapping.context.MappingContext}. It is recommended to provide
* the initial set of classes through {@link #setInitialEntitySet(Set)}.
* {@link org.springframework.data.mapping.context.MappingContext}. It is recommended to provide the initial set of
* classes through {@link #setInitialEntitySet(Set)}.
*
* @author Michael J. Simons
* @since 1.0
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public final class Neo4jMappingContext
extends AbstractMappingContext<Neo4jPersistentEntity<?>, Neo4jPersistentProperty> implements Schema {
public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersistentEntity<?>, Neo4jPersistentProperty>
implements Schema {
/**
* A map of fallback id generators, that have not been added to the application context
@@ -61,8 +61,8 @@ public final class Neo4jMappingContext
private final Map<Class<? extends IdGenerator<?>>, IdGenerator<?>> idGenerators = new ConcurrentHashMap<>();
/**
* The {@link NodeDescriptionStore} is basically a {@link Map} and it is used to break the dependency
* cycle between this class and the {@link DefaultNeo4jConverter}.
* The {@link NodeDescriptionStore} is basically a {@link Map} and it is used to break the dependency cycle between
* this class and the {@link DefaultNeo4jConverter}.
*/
private final NodeDescriptionStore nodeDescriptionStore = new NodeDescriptionStore();
@@ -107,28 +107,25 @@ public final class Neo4jMappingContext
if (this.nodeDescriptionStore.containsKey(primaryLabel)) {
// @formatter:off
throw new MappingException(
String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s",
primaryLabel));
throw new MappingException(String.format(Locale.ENGLISH,
"The schema already contains a node description under the primary label %s", primaryLabel));
// @formatter:on
}
if (this.nodeDescriptionStore.containsValue(newEntity)) {
Optional<String> label = this.nodeDescriptionStore.entrySet().stream()
.filter(e -> e.getValue().equals(newEntity)).map(
Map.Entry::getKey).findFirst();
Optional<String> label = this.nodeDescriptionStore.entrySet().stream().filter(e -> e.getValue().equals(newEntity))
.map(Map.Entry::getKey).findFirst();
throw new MappingException(
String.format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s",
newEntity, label.orElse("n/a")));
throw new MappingException(String.format(Locale.ENGLISH,
"The schema already contains description %s under the primary label %s", newEntity, label.orElse("n/a")));
}
NodeDescription<?> existingDescription = this.getNodeDescription(newEntity.getUnderlyingClass());
if (existingDescription != null) {
throw new MappingException(String.format(Locale.ENGLISH,
"The schema already contains description with the underlying class %s under the primary label %s",
newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel()));
"The schema already contains description with the underlying class %s under the primary label %s",
newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel()));
}
this.nodeDescriptionStore.put(primaryLabel, newEntity);
@@ -163,8 +160,8 @@ public final class Neo4jMappingContext
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(org.springframework.data.mapping.model.Property, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder)
*/
@Override
protected Neo4jPersistentProperty createPersistentProperty(Property property,
Neo4jPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
protected Neo4jPersistentProperty createPersistentProperty(Property property, Neo4jPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {
return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder);
}
@@ -196,7 +193,7 @@ public final class Neo4jMappingContext
idGenerator = BeanUtils.instantiateClass(idGeneratorType);
} else {
idGenerator = this.beanFactory.getBeanProvider(idGeneratorType)
.getIfUnique(() -> this.beanFactory.createBean(idGeneratorType));
.getIfUnique(() -> this.beanFactory.createBean(idGeneratorType));
}
this.idGenerators.put(idGeneratorType, idGenerator);
return idGenerator;

View File

@@ -24,11 +24,10 @@ import org.springframework.data.mapping.model.MutablePersistentEntity;
import org.springframework.data.neo4j.core.schema.NodeDescription;
/**
* A {@link org.springframework.data.mapping.PersistentEntity} interface with additional methods for metadata related to Neo4j.
*
* Both Spring Data methods {@link #doWithProperties(PropertyHandler)} and {@link #doWithAssociations(AssociationHandler)} are
* aware which field of a class is meant to be mapped as a property of a node or a relationship or if it is a relationship
* (in Spring Data terms: if it is an association).
* A {@link org.springframework.data.mapping.PersistentEntity} interface with additional methods for metadata related to
* Neo4j. Both Spring Data methods {@link #doWithProperties(PropertyHandler)} and
* {@link #doWithAssociations(AssociationHandler)} are aware which field of a class is meant to be mapped as a property
* of a node or a relationship or if it is a relationship (in Spring Data terms: if it is an association).
*
* @author Michael J. Simons
* @param <T> type of the underlying class
@@ -36,10 +35,11 @@ import org.springframework.data.neo4j.core.schema.NodeDescription;
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public interface Neo4jPersistentEntity<T>
extends MutablePersistentEntity<T, Neo4jPersistentProperty>, NodeDescription<T> {
extends MutablePersistentEntity<T, Neo4jPersistentProperty>, NodeDescription<T> {
/**
* @return An optional property pointing to a {@link java.util.Collection Collection&lt;String&gt;} containing dynamic "runtime managed" labels.
* @return An optional property pointing to a {@link java.util.Collection Collection&lt;String&gt;} containing dynamic
* "runtime managed" labels.
*/
Optional<Neo4jPersistentProperty> getDynamicLabelsProperty();
}

View File

@@ -22,19 +22,19 @@ import org.springframework.data.neo4j.core.schema.GraphPropertyDescription;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
/**
* A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related to Neo4j.
* A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related
* to Neo4j.
*
* @author Michael J. Simons
* @author Philipp Tölle
* @since 1.0
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public interface Neo4jPersistentProperty
extends PersistentProperty<Neo4jPersistentProperty>, GraphPropertyDescription {
public interface Neo4jPersistentProperty extends PersistentProperty<Neo4jPersistentProperty>, GraphPropertyDescription {
/**
* Dynamic associations are associations to non-simple types stored in a map
* with a key type of {@literal java.lang.String} or enum.
* Dynamic associations are associations to non-simple types stored in a map with a key type of
* {@literal java.lang.String} or enum.
*
* @return True, if this association is a dynamic association.
*/
@@ -43,8 +43,8 @@ public interface Neo4jPersistentProperty
}
/**
* Dynamic one-to-many associations are associations to non-simple types stored in a map
* with a key type of {@literal java.lang.String} and values of {@literal java.util.Collection}.
* Dynamic one-to-many associations are associations to non-simple types stored in a map with a key type of
* {@literal java.lang.String} and values of {@literal java.util.Collection}.
*
* @return True, if this association is a dynamic association with multple values per type.
* @since 1.0.1
@@ -68,9 +68,7 @@ public interface Neo4jPersistentProperty
* @return True, if this association has properties
*/
default boolean isRelationshipWithProperties() {
return isAssociation()
&& isMap()
&& getMapValueType() != null
&& getMapValueType().isAnnotationPresent(RelationshipProperties.class);
return isAssociation() && isMap() && getMapValueType() != null
&& getMapValueType().isAnnotationPresent(RelationshipProperties.class);
}
}

View File

@@ -33,8 +33,7 @@ final class NodeDescriptionAndLabels {
private final Collection<String> dynamicLabels;
NodeDescriptionAndLabels(NodeDescription<?> nodeDescription,
Collection<String> dynamicLabels) {
NodeDescriptionAndLabels(NodeDescription<?> nodeDescription, Collection<String> dynamicLabels) {
this.nodeDescription = nodeDescription;
this.dynamicLabels = dynamicLabels;
}

View File

@@ -29,8 +29,8 @@ import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.lang.Nullable;
/**
* This class is more or less just a wrapper around the node description lookup map.
* It ensures that there is no cyclic dependency between {@link Neo4jMappingContext} and {@link DefaultNeo4jConverter}.
* This class is more or less just a wrapper around the node description lookup map. It ensures that there is no cyclic
* dependency between {@link Neo4jMappingContext} and {@link DefaultNeo4jConverter}.
*
* @author Gerrit Meier
*/
@@ -77,10 +77,8 @@ class NodeDescriptionStore {
return null;
}
public NodeDescriptionAndLabels deriveConcreteNodeDescription(
Neo4jPersistentEntity<?> entityDescription,
List<String> labels
) {
public NodeDescriptionAndLabels deriveConcreteNodeDescription(Neo4jPersistentEntity<?> entityDescription,
List<String> labels) {
if (labels == null || labels.isEmpty()) {
return new NodeDescriptionAndLabels(entityDescription, Collections.emptyList());
}

View File

@@ -1,5 +1,6 @@
/**
* The main mapping framework. This package contains all the public facing annotations necessary to mark Spring Data Neo4j entities.
* The main mapping framework. This package contains all the public facing annotations necessary to mark Spring Data
* Neo4j entities.
*
* @author Michael J. Simons
*/

View File

@@ -1,6 +1,5 @@
/**
* This package contains the core infrastructure for creating a imperative or reactive client that can execute
* queries.
* This package contains the core infrastructure for creating a imperative or reactive client that can execute queries.
*/
@NonNullApi
package org.springframework.data.neo4j.core;

View File

@@ -44,6 +44,5 @@ public final class Constants {
public static final String FROM_ID_PARAMETER_NAME = "fromId";
private Constants() {
}
private Constants() {}
}

View File

@@ -26,9 +26,9 @@ import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.*;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Relationship;
import org.neo4j.cypherdsl.core.*;
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentProperty;
@@ -39,8 +39,8 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A generator based on the schema defined by node and relationship descriptions.
* Most methods return renderable Cypher statements.
* A generator based on the schema defined by node and relationship descriptions. Most methods return renderable Cypher
* statements.
*
* @author Michael J. Simons
* @author Gerrit Meier
@@ -70,20 +70,20 @@ public enum CypherGenerator {
}
/**
* This will create a match statement that fits the given node description and may contains additional conditions.
* The {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to
* the given {@code nodeDescription}.
* This will create a match statement that fits the given node description and may contains additional conditions. The
* {@code WITH} clause of this statement contains all nodes and relationships necessary to map a record to the given
* {@code nodeDescription}.
* <p>
* It is recommended to use {@link Cypher#asterisk()} to return everything from the query in the end.
* <p>
* The root node is guaranteed to have the symbolic name {@code n}.
*
* @param nodeDescription The node description for which a match clause should be generated
* @param condition Optional conditions to add
* @param condition Optional conditions to add
* @return An ongoing match
*/
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription, @Nullable
Condition condition) {
public StatementBuilder.OrderableOngoingReadingAndWith prepareMatchOf(NodeDescription<?> nodeDescription,
@Nullable Condition condition) {
String primaryLabel = nodeDescription.getPrimaryLabel();
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
@@ -96,13 +96,12 @@ public enum CypherGenerator {
if (idDescription.isInternallyGeneratedId()) {
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
}
return match(rootNode).where(conditionOrNoCondition(condition))
.with(expressions.toArray(new Expression[] {}));
return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(new Expression[] {}));
}
/**
* Creates a statement that returns all labels of a node that are not part of a list parameter named {@link Constants#NAME_OF_STATIC_LABELS_PARAM}.
* Those are the "dynamic labels" of a node as set through SDN/RX.
* Creates a statement that returns all labels of a node that are not part of a list parameter named
* {@link Constants#NAME_OF_STATIC_LABELS_PARAM}. Those are the "dynamic labels" of a node as set through SDN.
*
* @param nodeDescription The node description for which the statement should be generated
* @return A statement having one parameter.
@@ -116,18 +115,17 @@ public enum CypherGenerator {
if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) {
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty();
versionCondition = rootNode.property(versionProperty.getName()).isEqualTo(parameter(
Constants.NAME_OF_VERSION_PARAM));
versionCondition = rootNode.property(versionProperty.getName())
.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM));
} else {
versionCondition = Conditions.noCondition();
}
return match(rootNode)
.where(nodeDescription.getIdDescription().asIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
.and(versionCondition)
.unwind(rootNode.labels()).as("label")
.with(Cypher.name("label")).where(Cypher.name("label").in(parameter(Constants.NAME_OF_STATIC_LABELS_PARAM)).not())
.returning(Functions.collect(Cypher.name("label")).as(Constants.NAME_OF_LABELS)).build();
.where(nodeDescription.getIdDescription().asIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID)))
.and(versionCondition).unwind(rootNode.labels()).as("label").with(Cypher.name("label"))
.where(Cypher.name("label").in(parameter(Constants.NAME_OF_STATIC_LABELS_PARAM)).not())
.returning(Functions.collect(Cypher.name("label")).as(Constants.NAME_OF_LABELS)).build();
}
public Statement prepareDeleteOf(NodeDescription<?> nodeDescription) {
@@ -137,11 +135,12 @@ public enum CypherGenerator {
public Statement prepareDeleteOf(NodeDescription<?> nodeDescription, @Nullable Condition condition) {
Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels())
.named(Constants.NAME_OF_ROOT_NODE);
.named(Constants.NAME_OF_ROOT_NODE);
return match(rootNode).where(conditionOrNoCondition(condition)).detachDelete(rootNode).build();
}
public Statement prepareSaveOf(NodeDescription<?> nodeDescription, UnaryOperator<OngoingMatchAndUpdate> updateDecorator) {
public Statement prepareSaveOf(NodeDescription<?> nodeDescription,
UnaryOperator<OngoingMatchAndUpdate> updateDecorator) {
String primaryLabel = nodeDescription.getPrimaryLabel();
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
@@ -152,37 +151,29 @@ public enum CypherGenerator {
if (!idDescription.isInternallyGeneratedId()) {
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) {
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription)
.getRequiredVersionProperty();
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty();
String nameOfPossibleExistingNode = "hlp";
Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode);
Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter))
.with(possibleExistingNode).where(possibleExistingNode.isNull())
.create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId())
.build();
.where(possibleExistingNode.property(nameOfIdProperty).isEqualTo(idParameter)).with(possibleExistingNode)
.where(possibleExistingNode.isNull()).create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode.internalId()).build();
Statement updateIfExists = updateDecorator.apply(match(rootNode)
.where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
.and(rootNode.property(versionProperty.getName()).isEqualTo(parameter(
Constants.NAME_OF_VERSION_PARAM)))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId())
.build();
Statement updateIfExists = updateDecorator
.apply(match(rootNode).where(rootNode.property(nameOfIdProperty).isEqualTo(idParameter))
.and(rootNode.property(versionProperty.getName()).isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId()).build();
return Cypher.union(createIfNew, updateIfExists);
} else {
return updateDecorator.apply(
Cypher.merge(rootNode.withProperties(nameOfIdProperty, idParameter))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))
).returning(rootNode.internalId()).build();
return updateDecorator.apply(Cypher.merge(rootNode.withProperties(nameOfIdProperty, idParameter)).set(rootNode,
parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode.internalId()).build();
}
} else {
String nameOfPossibleExistingNode = "hlp";
@@ -193,36 +184,26 @@ public enum CypherGenerator {
if (((Neo4jPersistentEntity) nodeDescription).hasVersionProperty()) {
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription)
.getRequiredVersionProperty();
PersistentProperty versionProperty = ((Neo4jPersistentEntity) nodeDescription).getRequiredVersionProperty();
createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
.where(possibleExistingNode.internalId().isEqualTo(idParameter))
.with(possibleExistingNode).where(possibleExistingNode.isNull())
.create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId()).build();
createIfNew = updateDecorator
.apply(optionalMatch(possibleExistingNode).where(possibleExistingNode.internalId().isEqualTo(idParameter))
.with(possibleExistingNode).where(possibleExistingNode.isNull()).create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId()).build();
updateIfExists = updateDecorator.apply(match(rootNode)
.where(rootNode.internalId().isEqualTo(idParameter))
.and(rootNode.property(versionProperty.getName()).isEqualTo(parameter(
Constants.NAME_OF_VERSION_PARAM)))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId())
.build();
updateIfExists = updateDecorator.apply(match(rootNode).where(rootNode.internalId().isEqualTo(idParameter))
.and(rootNode.property(versionProperty.getName()).isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM)))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode.internalId()).build();
} else {
createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
.where(possibleExistingNode.internalId().isEqualTo(idParameter))
.with(possibleExistingNode).where(possibleExistingNode.isNull())
.create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId())
.build();
createIfNew = updateDecorator
.apply(optionalMatch(possibleExistingNode).where(possibleExistingNode.internalId().isEqualTo(idParameter))
.with(possibleExistingNode).where(possibleExistingNode.isNull()).create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId()).build();
updateIfExists = updateDecorator.apply(match(rootNode)
.where(rootNode.internalId().isEqualTo(idParameter))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode.internalId()).build();
updateIfExists = updateDecorator.apply(match(rootNode).where(rootNode.internalId().isEqualTo(idParameter))
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode.internalId()).build();
}
return Cypher.union(createIfNew, updateIfExists);
@@ -232,31 +213,28 @@ public enum CypherGenerator {
public Statement prepareSaveOfMultipleInstancesOf(NodeDescription<?> nodeDescription) {
Assert.isTrue(!nodeDescription.isUsingInternalIds(),
"Only entities that use external IDs can be saved in a batch.");
"Only entities that use external IDs can be saved in a batch.");
Node rootNode = node(nodeDescription.getPrimaryLabel(), nodeDescription.getAdditionalLabels())
.named(Constants.NAME_OF_ROOT_NODE);
.named(Constants.NAME_OF_ROOT_NODE);
IdDescription idDescription = nodeDescription.getIdDescription();
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property!"));
String row = "entity";
return Cypher
.unwind(parameter(Constants.NAME_OF_ENTITY_LIST_PARAM)).as(row)
.merge(rootNode.withProperties(nameOfIdProperty, Cypher.property(row, Constants.NAME_OF_ID)))
.set(rootNode, Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM))
.returning(Functions.collect(rootNode.property(nameOfIdProperty)).as(Constants.NAME_OF_IDS))
.build();
return Cypher.unwind(parameter(Constants.NAME_OF_ENTITY_LIST_PARAM)).as(row)
.merge(rootNode.withProperties(nameOfIdProperty, Cypher.property(row, Constants.NAME_OF_ID)))
.set(rootNode, Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM))
.returning(Functions.collect(rootNode.property(nameOfIdProperty)).as(Constants.NAME_OF_IDS)).build();
}
@NonNull
public Statement createRelationshipCreationQuery(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship, @Nullable String dynamicRelationshipType, Long relatedInternalId) {
final Node startNode = neo4jPersistentEntity.isUsingInternalIds()
? anyNode(START_NODE_NAME)
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
.named(START_NODE_NAME);
RelationshipDescription relationship, @Nullable String dynamicRelationshipType, Long relatedInternalId) {
final Node startNode = neo4jPersistentEntity.isUsingInternalIds() ? anyNode(START_NODE_NAME)
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
.named(START_NODE_NAME);
final Node endNode = anyNode(END_NODE_NAME);
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
@@ -264,26 +242,22 @@ public enum CypherGenerator {
Parameter idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME);
String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType();
return match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds()
? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.match(endNode)
.where(endNode.internalId().isEqualTo(literalOf(relatedInternalId)))
.merge(relationship.isOutgoing()
? startNode.relationshipTo(endNode, type)
: startNode.relationshipFrom(endNode, type)
)
.build();
.where(neo4jPersistentEntity.isUsingInternalIds() ? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.match(endNode).where(endNode.internalId().isEqualTo(literalOf(relatedInternalId)))
.merge(relationship.isOutgoing() ? startNode.relationshipTo(endNode, type)
: startNode.relationshipFrom(endNode, type))
.build();
}
@NonNull
public Statement createRelationshipWithPropertiesCreationQuery(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship, Long relatedInternalId) {
RelationshipDescription relationship, Long relatedInternalId) {
Assert.isTrue(relationship.hasRelationshipProperties(),
"Properties required to create a relationship with properties");
"Properties required to create a relationship with properties");
Assert.isTrue(!relationship.isDynamic(),
"Creation of relationships with properties is only supported for non-dynamic relationships");
"Creation of relationships with properties is only supported for non-dynamic relationships");
Node startNode = anyNode(START_NODE_NAME);
Node endNode = anyNode(END_NODE_NAME);
@@ -297,26 +271,19 @@ public enum CypherGenerator {
Relationship relIncoming = startNode.relationshipFrom(endNode, type).named(RELATIONSHIP_NAME);
return match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds()
? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.match(endNode)
.where(endNode.internalId().isEqualTo(literalOf(relatedInternalId)))
.merge(relationship.isOutgoing()
? relOutgoing
: relIncoming
)
.set(RELATIONSHIP_NAME, relationshipProperties)
.build();
.where(neo4jPersistentEntity.isUsingInternalIds() ? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.match(endNode).where(endNode.internalId().isEqualTo(literalOf(relatedInternalId)))
.merge(relationship.isOutgoing() ? relOutgoing : relIncoming).set(RELATIONSHIP_NAME, relationshipProperties)
.build();
}
@NonNull
public Statement createRelationshipRemoveQuery(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationshipDescription, Neo4jPersistentEntity relatedNode) {
final Node startNode = neo4jPersistentEntity.isUsingInternalIds()
? anyNode(START_NODE_NAME)
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
.named(START_NODE_NAME);
RelationshipDescription relationshipDescription, Neo4jPersistentEntity relatedNode) {
final Node startNode = neo4jPersistentEntity.isUsingInternalIds() ? anyNode(START_NODE_NAME)
: node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels())
.named(START_NODE_NAME);
final Node endNode = node(relatedNode.getPrimaryLabel(), relatedNode.getAdditionalLabels());
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
@@ -326,15 +293,14 @@ public enum CypherGenerator {
String relationshipToRemoveName = "rel";
Relationship relationship = outgoing
? startNode.relationshipTo(endNode, relationshipType).named(relationshipToRemoveName)
: startNode.relationshipFrom(endNode, relationshipType).named(relationshipToRemoveName);
? startNode.relationshipTo(endNode, relationshipType).named(relationshipToRemoveName)
: startNode.relationshipFrom(endNode, relationshipType).named(relationshipToRemoveName);
Parameter idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME);
return match(relationship)
.where(neo4jPersistentEntity.isUsingInternalIds()
? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.delete(relationship.getSymbolicName().get()).build();
.where(neo4jPersistentEntity.isUsingInternalIds() ? startNode.internalId().isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.delete(relationship.getSymbolicName().get()).build();
}
public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescription) {
@@ -343,40 +309,36 @@ public enum CypherGenerator {
/**
* @param nodeDescription Description of the root node
* @param inputProperties A list of Java properties of the domain to be included.
* Those properties are compared with the field names of graph properties respectively relationships.
* @param inputProperties A list of Java properties of the domain to be included. Those properties are compared with
* the field names of graph properties respectively relationships.
* @return An expresion to be returned by a Cypher statement
*/
public Expression createReturnStatementForMatch(NodeDescription<?> nodeDescription,
@Nullable List<String> inputProperties) {
@Nullable List<String> inputProperties) {
Predicate<String> includeField = s -> inputProperties == null || inputProperties.isEmpty()
|| inputProperties.contains(s);
|| inputProperties.contains(s);
List<RelationshipDescription> processedRelationships = new ArrayList<>();
return projectPropertiesAndRelationships(nodeDescription, Constants.NAME_OF_ROOT_NODE, includeField,
processedRelationships);
processedRelationships);
}
private MapProjection projectAllPropertiesAndRelationships(NodeDescription<?> nodeDescription,
SymbolicName nodeName,
List<RelationshipDescription> processedRelationships) {
private MapProjection projectAllPropertiesAndRelationships(NodeDescription<?> nodeDescription, SymbolicName nodeName,
List<RelationshipDescription> processedRelationships) {
Predicate<String> includeAllFields = (field) -> true;
return projectPropertiesAndRelationships(nodeDescription, nodeName, includeAllFields, processedRelationships);
}
private MapProjection projectPropertiesAndRelationships(NodeDescription<?> nodeDescription,
SymbolicName nodeName,
Predicate<String> includeProperty,
List<RelationshipDescription> processedRelationships) {
private MapProjection projectPropertiesAndRelationships(NodeDescription<?> nodeDescription, SymbolicName nodeName,
Predicate<String> includeProperty, List<RelationshipDescription> processedRelationships) {
List<Object> contentOfProjection = new ArrayList<>();
contentOfProjection.addAll(projectNodeProperties(nodeDescription, nodeName, includeProperty));
contentOfProjection.addAll(
generateListsFor(nodeDescription.getRelationships(), nodeName, includeProperty, processedRelationships)
);
generateListsFor(nodeDescription.getRelationships(), nodeName, includeProperty, processedRelationships));
return Cypher.anyNode(nodeName).project(contentOfProjection);
}
@@ -387,7 +349,7 @@ public enum CypherGenerator {
* self-reflecting fields. Example with self-reflection and explicit value: {@code n {.id, name: n.name}}.
*/
private List<Object> projectNodeProperties(NodeDescription<?> nodeDescription, SymbolicName nodeName,
Predicate<String> includeField) {
Predicate<String> includeField) {
List<Object> nodePropertiesProjection = new ArrayList<>();
Node node = anyNode(nodeName);
@@ -413,9 +375,8 @@ public enum CypherGenerator {
/**
* @see CypherGenerator#projectNodeProperties
*/
private List<Object> generateListsFor(Collection<RelationshipDescription> relationships,
SymbolicName nodeName, Predicate<String> includeField,
List<RelationshipDescription> processedRelationships) {
private List<Object> generateListsFor(Collection<RelationshipDescription> relationships, SymbolicName nodeName,
Predicate<String> includeField, List<RelationshipDescription> processedRelationships) {
List<Object> mapProjectionLists = new ArrayList<>();
@@ -429,7 +390,7 @@ public enum CypherGenerator {
// if we already processed the other way before, do not try to jump in the infinite loop
// unless it is a root node relationship
if (!nodeName.equals(Constants.NAME_OF_ROOT_NODE) && relationshipDescription.hasRelationshipObverse()
&& processedRelationships.contains(relationshipDescription.getRelationshipObverse())) {
&& processedRelationships.contains(relationshipDescription.getRelationshipObverse())) {
continue;
}
@@ -444,7 +405,7 @@ public enum CypherGenerator {
}
private void generateListFor(RelationshipDescription relationshipDescription, SymbolicName nodeName,
List<RelationshipDescription> processedRelationships, String fieldName, List<Object> mapProjectionLists) {
List<RelationshipDescription> processedRelationships, String fieldName, List<Object> mapProjectionLists) {
String relationshipType = relationshipDescription.getType();
String relationshipTargetName = relationshipDescription.generateRelatedNodesCollectionName();
@@ -459,36 +420,30 @@ public enum CypherGenerator {
processedRelationships.add(relationshipDescription);
if (relationshipDescription.isDynamic()) {
Relationship relationship = relationshipDescription
.isOutgoing()
? startNode.relationshipTo(endNode)
: startNode.relationshipFrom(endNode);
Relationship relationship = relationshipDescription.isOutgoing() ? startNode.relationshipTo(endNode)
: startNode.relationshipFrom(endNode);
relationship = relationship.named(relationshipTargetName);
addMapProjection(relationshipTargetName,
listBasedOn(relationship)
.returning(
projectAllPropertiesAndRelationships(endNodeDescription,
relationshipFieldName, new ArrayList<>(processedRelationships))
.and(NAME_OF_RELATIONSHIP_TYPE, Functions.type(relationship))),
mapProjectionLists);
listBasedOn(relationship).returning(projectAllPropertiesAndRelationships(endNodeDescription,
relationshipFieldName, new ArrayList<>(processedRelationships)).and(NAME_OF_RELATIONSHIP_TYPE,
Functions.type(relationship))),
mapProjectionLists);
} else {
Relationship relationship = relationshipDescription.isOutgoing()
? startNode.relationshipTo(endNode, relationshipType)
: startNode.relationshipFrom(endNode, relationshipType);
? startNode.relationshipTo(endNode, relationshipType)
: startNode.relationshipFrom(endNode, relationshipType);
MapProjection mapProjection = projectAllPropertiesAndRelationships(endNodeDescription,
relationshipFieldName, new ArrayList<>(processedRelationships));
MapProjection mapProjection = projectAllPropertiesAndRelationships(endNodeDescription, relationshipFieldName,
new ArrayList<>(processedRelationships));
if (relationshipDescription.hasRelationshipProperties()) {
relationship = relationship.named(RelationshipDescription.NAME_OF_RELATIONSHIP);
mapProjection = mapProjection.and(relationship);
}
addMapProjection(relationshipTargetName,
listBasedOn(relationship).returning(mapProjection),
mapProjectionLists);
addMapProjection(relationshipTargetName, listBasedOn(relationship).returning(mapProjection), mapProjectionLists);
}
}

View File

@@ -24,13 +24,13 @@ import java.lang.annotation.Target;
import org.apiguardian.api.API;
/**
* This annotation can be used on a field of type {@link java.util.Collection Collection&lt;String&gt;}. The content
* of this field will be treated as dynamic or runtime managed labels. This means: All labels that are not statically
* This annotation can be used on a field of type {@link java.util.Collection Collection&lt;String&gt;}. The content of
* this field will be treated as dynamic or runtime managed labels. This means: All labels that are not statically
* defined via the class hierarchy and the corresponding {@link Node @Node} annotation are added to this list while
* loading the entity and all values contained in the collection will be added to the nodes labels.
* <p>
* Labels not defined through the class hierarchy or the list of dynamic labels will be removed from the database
* when {@link DynamicLabels @DynamicLabels} is used.
* Labels not defined through the class hierarchy or the list of dynamic labels will be removed from the database when
* {@link DynamicLabels @DynamicLabels} is used.
*
* @author Michael J. Simons
* @soundtrack Danger Dan - Nudeln und Klopapier

View File

@@ -27,10 +27,11 @@ import org.apiguardian.api.API;
import org.springframework.core.annotation.AliasFor;
/**
* Indicates a generated id. Ids can be generated internally. by the database itself or by an external generator. This annotation
* defaults to the internally generated ids.
* Indicates a generated id. Ids can be generated internally. by the database itself or by an external generator. This
* annotation defaults to the internally generated ids.
* <p>
* An internal id has no corresponding property on a node. It can only retrieved via the built-in Cypher function {@code id()}.
* An internal id has no corresponding property on a node. It can only retrieved via the built-in Cypher function
* {@code id()}.
* <p>
* To use an external id generator, specify on the
*

View File

@@ -20,8 +20,8 @@ import org.apiguardian.api.API;
/**
* Provides minimal information how to map class attributes to the properties of a node or a relationship.
* <p>
* Spring Data's persistent properties have slightly different semantics. They have an entity centric approach of properties.
* Spring Data properties contain - if not marked otherwise - also associations.
* Spring Data's persistent properties have slightly different semantics. They have an entity centric approach of
* properties. Spring Data properties contain - if not marked otherwise - also associations.
* <p>
* Associations between different node types can be queried on the {@link Schema} itself.
*

View File

@@ -26,52 +26,47 @@ import org.apiguardian.api.API;
/**
* This annotation is included here for completeness. It marks an attribute as the primary id of a node entity. It can
* be used as an alternative to {@link org.springframework.data.annotation.Id} and it may provide additional features
* in the future.
*
* be used as an alternative to {@link org.springframework.data.annotation.Id} and it may provide additional features in
* the future.
* <p>
* To use assigned ids, annotate an arbitrary attribute of your domain class with
* {@link org.springframework.data.annotation.Id} or this annotation:
*
* To use assigned ids, annotate an arbitrary attribute of your domain class with {@link org.springframework.data.annotation.Id}
* or this annotation:
* <pre>
* &#64;Node
* public class MyEntity {
* &#64;Id
* String theId;
* &#64;Id String theId;
* }
* </pre>
* You can combine {@code @Id} with {@code @Property} with assigned ids to rename the node property in which the assigned id is stored.
*
* You can combine {@code @Id} with {@code @Property} with assigned ids to rename the node property in which the
* assigned id is stored.
* <p>
* To use internally generated ids, annotate an arbitrary attribute of type {@code java.lang.long} or
* {@code java.lang.Long} with {@code @Id} and {@link GeneratedValue @GeneratedValue}.
*
* To use internally generated ids, annotate an arbitrary attribute of type {@code java.lang.long} or {@code java.lang.Long}
* with {@code @Id} and {@link GeneratedValue @GeneratedValue}.
* <pre>
* &#64;Node
* public class MyEntity {
* &#64;Id &#64;GeneratedValue
* Long id;
* &#64;Id &#64;GeneratedValue Long id;
* }
* </pre>
*
* It does not need to be named {@code id}, but most people chose this as the attribute in the class. As the attribute
* does not correspond to a node property, it cannot be renamed via {@code @Property}.
*
* <p>
*
* To use externally generated ids, annotate an arbitrary attribute with a type that your generated returns
* with {@code @Id} and {@link GeneratedValue @GeneratedValue} and specify the generator class.
* To use externally generated ids, annotate an arbitrary attribute with a type that your generated returns with
* {@code @Id} and {@link GeneratedValue @GeneratedValue} and specify the generator class.
*
* <pre>
* &#64;Node
* public class MyEntity {
* &#64;Id &#64;GeneratedValue(UUIDStringGenerator.class)
* String theId;
* &#64;Id &#64;GeneratedValue(UUIDStringGenerator.class) String theId;
* }
* </pre>
*
* Externally generated ids are indistinguishable to assigned ids from the database perspective and thus can be arbitrarily
* named via {@code @Property}.
* Externally generated ids are indistinguishable to assigned ids from the database perspective and thus can be
* arbitrarily named via {@code @Property}.
*
* @author Michael J. Simons
* @since 1.0

View File

@@ -62,10 +62,8 @@ public final class IdDescription {
return new IdDescription(GeneratedValue.InternalIdGenerator.class, null, null);
}
public static IdDescription forExternallyGeneratedIds(
@Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
@Nullable String idGeneratorRef,
String graphPropertyName) {
public static IdDescription forExternallyGeneratedIds(@Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
@Nullable String idGeneratorRef, String graphPropertyName) {
Assert.notNull(graphPropertyName, "Graph property name is required.");
try {
@@ -75,17 +73,14 @@ public final class IdDescription {
} catch (IllegalArgumentException e) {
Assert.notNull(idGeneratorClass, "Class of id generator is required.");
Assert.isTrue(idGeneratorClass != GeneratedValue.InternalIdGenerator.class,
"Cannot use InternalIdGenerator for externally generated ids.");
"Cannot use InternalIdGenerator for externally generated ids.");
return new IdDescription(idGeneratorClass, null, graphPropertyName);
}
}
private IdDescription(
@Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
@Nullable String idGeneratorRef,
@Nullable String graphPropertyName
) {
private IdDescription(@Nullable Class<? extends IdGenerator<?>> idGeneratorClass, @Nullable String idGeneratorRef,
@Nullable String graphPropertyName) {
this.idGeneratorClass = idGeneratorClass;
this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef;
this.graphPropertyName = graphPropertyName;
@@ -95,7 +90,7 @@ public final class IdDescription {
return Functions.id(rootNode);
} else {
return this.getOptionalGraphPropertyName()
.map(propertyName -> Cypher.property(Constants.NAME_OF_ROOT_NODE, propertyName)).get();
.map(propertyName -> Cypher.property(Constants.NAME_OF_ROOT_NODE, propertyName)).get();
}
});
}
@@ -113,7 +108,8 @@ public final class IdDescription {
}
/**
* @return True, if the ID is assigned to the entity before the entity hits the database, either manually or through a generator.
* @return True, if the ID is assigned to the entity before the entity hits the database, either manually or through a
* generator.
*/
public boolean isAssignedId() {
return this.idGeneratorClass == null && this.idGeneratorRef == null;
@@ -131,13 +127,13 @@ public final class IdDescription {
*/
public boolean isExternallyGeneratedId() {
return (this.idGeneratorClass != null && this.idGeneratorClass != GeneratedValue.InternalIdGenerator.class)
|| this.idGeneratorRef != null;
|| this.idGeneratorRef != null;
}
/**
* An ID description has only a corresponding graph property name when it's bas on an external assigment.
* An internal id has no corresponding graph property and therefor this method
* will return an empty {@link Optional} in such cases.
* An ID description has only a corresponding graph property name when it's bas on an external assigment. An internal
* id has no corresponding graph property and therefor this method will return an empty {@link Optional} in such
* cases.
*
* @return The name of an optional graph property.
*/

View File

@@ -44,8 +44,9 @@ public @interface Node {
String[] value() default {};
/**
* @return The labels to identify a node with that is supposed to be mapped to the class annotated with {@link Node @Node}.
* The first label will be the primary label if not {@link #primaryLabel()} was set explicitly.
* @return The labels to identify a node with that is supposed to be mapped to the class annotated with
* {@link Node @Node}. The first label will be the primary label if not {@link #primaryLabel()} was set
* explicitly.
*/
@AliasFor("value")
String[] labels() default {};

View File

@@ -46,8 +46,8 @@ public interface NodeDescription<T> {
List<String> getAdditionalLabels();
/**
* @return The list of all static labels, that is the union of {@link #getPrimaryLabel()} + {@link #getAdditionalLabels()}.
* Order is guaranteed to be the primary first, than the others.
* @return The list of all static labels, that is the union of {@link #getPrimaryLabel()} +
* {@link #getAdditionalLabels()}. Order is guaranteed to be the primary first, than the others.
* @since 1.1
*/
default List<String> getStaticLabels() {
@@ -78,7 +78,6 @@ public interface NodeDescription<T> {
*/
Collection<GraphPropertyDescription> getGraphPropertiesInHierarchy();
/**
* Retrieves a {@link GraphPropertyDescription} by its field name.
*

View File

@@ -45,7 +45,6 @@ public @interface Property {
String value() default "";
/**
*
* @return The name of the property in the graph.
*/
@AliasFor("value")

View File

@@ -40,6 +40,7 @@ public @interface Relationship {
/**
* Enumeration of the direction a relationship can take.
*
* @since 1.0
*/
enum Direction {

View File

@@ -20,8 +20,8 @@ import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction
* is maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by
* Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction is
* maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by
* {@link #getTarget}.
*
* @author Michael J. Simons
@@ -63,24 +63,24 @@ public interface RelationshipDescription {
NodeDescription<?> getTarget();
/**
* The name of the property where the relationship was defined. This is used by the Cypher creation to name the
* return values.
* The name of the property where the relationship was defined. This is used by the Cypher creation to name the return
* values.
*
* @return The name of the field storing the relationship property
*/
String getFieldName();
/**
* The direction of the defined relationship. This is used by the Cypher creation to query for relationships
* and create them with the right directions.
* The direction of the defined relationship. This is used by the Cypher creation to query for relationships and
* create them with the right directions.
*
* @return The direction of the relationship
*/
Relationship.Direction getDirection();
/**
* If this is a relationship with properties, the properties-defining class will get returned,
* otherwise {@literal null}.
* If this is a relationship with properties, the properties-defining class will get returned, otherwise
* {@literal null}.
*
* @return The type of the relationship property class for relationship with properties, otherwise {@literal null}
*/
@@ -88,8 +88,8 @@ public interface RelationshipDescription {
Class<?> getRelationshipPropertiesClass();
/**
* Tells if this relationship is a relationship with additional properties.
* In such cases {@code getRelationshipPropertiesClass} will return the type of the properties holding class.
* Tells if this relationship is a relationship with additional properties. In such cases
* {@code getRelationshipPropertiesClass} will return the type of the properties holding class.
*
* @return {@literal true} if an additional properties are available, otherwise {@literal false}
*/
@@ -117,7 +117,6 @@ public interface RelationshipDescription {
void setRelationshipObverse(RelationshipDescription relationshipObverse);
/**
*
* @return logically same relationship definition in the target entity
*/
RelationshipDescription getRelationshipObverse();

View File

@@ -25,8 +25,8 @@ import java.lang.annotation.Target;
import org.apiguardian.api.API;
/**
* This marker interface is used on classes to mark that they represent additional relationship properties.
* A class that implements this interface must not be used as a or annotated with {@link Node}.
* This marker interface is used on classes to mark that they represent additional relationship properties. A class that
* implements this interface must not be used as a or annotated with {@link Node}.
*
* @author Gerrit Meier
*/

View File

@@ -30,7 +30,7 @@ import org.springframework.data.neo4j.core.convert.Neo4jConverter;
import org.springframework.lang.Nullable;
/**
* Contains the descriptions of all nodes, their properties and relationships known to SDN-RX.
* Contains the descriptions of all nodes, their properties and relationships known to SDN.
*
* @author Michael J. Simons
* @since 1.0
@@ -39,7 +39,7 @@ import org.springframework.lang.Nullable;
public interface Schema {
/**
* Registers the given set of classes to be available as Neo4j domain entities.
* Registers the given set of classes to be available as Neo4j domain entities.
*
* @param initialEntitySet The set of classes to register with this schema
*/
@@ -56,7 +56,8 @@ public interface Schema {
* @param primaryLabel The primary label under which the node is described
* @return The description if any, null otherwise
*/
@Nullable NodeDescription<?> getNodeDescription(String primaryLabel);
@Nullable
NodeDescription<?> getNodeDescription(String primaryLabel);
/**
* Retrieves a nodes description by its underlying class.
@@ -64,7 +65,8 @@ public interface Schema {
* @param underlyingClass The underlying class of the node description to be retrieved
* @return The description if any, null otherwise
*/
@Nullable NodeDescription<?> getNodeDescription(Class<?> underlyingClass);
@Nullable
NodeDescription<?> getNodeDescription(Class<?> underlyingClass);
default NodeDescription<?> getRequiredNodeDescription(Class<?> underlyingClass) {
NodeDescription<?> nodeDescription = getNodeDescription(underlyingClass);
@@ -78,24 +80,23 @@ public interface Schema {
NodeDescription<?> nodeDescription = getNodeDescription(primaryLabel);
if (nodeDescription == null) {
throw new MappingException(
String.format("Required node description not found with primary label '%s'", primaryLabel));
String.format("Required node description not found with primary label '%s'", primaryLabel));
}
return nodeDescription;
}
/**
* Retrieves a schema based mapping function for the {@code targetClass}. The mapping function will expect a
* record containing all the nodes and relationships necessary to fully populate an instance of the given class.
* It will not try to fetch data from any other records or queries. The mapping function is free to throw a {@link RuntimeException},
* most likely a {@code org.springframework.data.mapping.MappingException} or {@link IllegalStateException} when
* mapping is not possible.
* Retrieves a schema based mapping function for the {@code targetClass}. The mapping function will expect a record
* containing all the nodes and relationships necessary to fully populate an instance of the given class. It will not
* try to fetch data from any other records or queries. The mapping function is free to throw a
* {@link RuntimeException}, most likely a {@code org.springframework.data.mapping.MappingException} or
* {@link IllegalStateException} when mapping is not possible.
* <p>
* In case the mapping function returns a {@literal null}, the Neo4j client will throw an exception and prevent further
* processing.
* In case the mapping function returns a {@literal null}, the Neo4j client will throw an exception and prevent
* further processing.
*
* @param targetClass The target class to which to map to.
* @param <T> Type of the target class
* @param <T> Type of the target class
* @return The default, stateless and reusable mapping function for the given target class
* @throws UnknownEntityException When {@code targetClass} is not a managed class
*/
@@ -123,8 +124,8 @@ public interface Schema {
}
/**
* Creates or retrieves an instance of the given id generator class. During the lifetime of the schema,
* this method returns the same instance of reoccurring requests of the same type.
* Creates or retrieves an instance of the given id generator class. During the lifetime of the schema, this method
* returns the same instance of reoccurring requests of the same type.
*
* @param idGeneratorType The type of the ID generator to return
* @return The id generator.

View File

@@ -19,8 +19,8 @@ import org.apiguardian.api.API;
import org.springframework.dao.InvalidDataAccessApiUsageException;
/**
* Thrown when required information about a class or primary label is requested from the {@link Schema} and those information
* is not available.
* Thrown when required information about a class or primary label is requested from the {@link Schema} and those
* information is not available.
*
* @author Michael J. Simons
* @since 1.0

View File

@@ -33,23 +33,20 @@ public final class Relationships {
/**
* The value for a relationship can be a scalar object (1:1), a collection (1:n), a map (1:n, but with dynamic
* relationship types) or a map (1:n) with properties for each relationship.
* This method unifies the type into something iterable, depending on the given inverse type.
* relationship types) or a map (1:n) with properties for each relationship. This method unifies the type into
* something iterable, depending on the given inverse type.
*
* @param rawValue The raw value to unify
* @return A unified collection (Either a collection of Map.Entry for dynamic and relationships with properties
* or a list of related values)
* @return A unified collection (Either a collection of Map.Entry for dynamic and relationships with properties or a
* list of related values)
*/
@Nullable
public static Collection<?> unifyRelationshipValue(Neo4jPersistentProperty property, Object rawValue) {
Collection<?> unifiedValue;
if (property.isDynamicAssociation()) {
if (property.isDynamicOneToManyAssociation()) {
unifiedValue = ((Map<String, Collection<?>>) rawValue)
.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(v -> new SimpleEntry(e.getKey(), v)))
.collect(toList());
unifiedValue = ((Map<String, Collection<?>>) rawValue).entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> new SimpleEntry(e.getKey(), v))).collect(toList());
} else {
unifiedValue = ((Map<String, Object>) rawValue).entrySet();
}
@@ -63,6 +60,5 @@ public final class Relationships {
return unifiedValue;
}
private Relationships() {
}
private Relationships() {}
}

View File

@@ -27,8 +27,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
* @author Michael J. Simons
* @since 1.0
*/
final class Neo4jSessionSynchronization
extends ResourceHolderSynchronization<Neo4jTransactionHolder, Object> {
final class Neo4jSessionSynchronization extends ResourceHolderSynchronization<Neo4jTransactionHolder, Object> {
private final Neo4jTransactionHolder localConnectionHolder;

View File

@@ -60,7 +60,8 @@ final class Neo4jTransactionHolder extends ResourceHolderSupport {
* @param inDatabase selected database to use
* @return An optional, ongoing transaction.
*/
@Nullable Transaction getTransaction(String inDatabase) {
@Nullable
Transaction getTransaction(String inDatabase) {
return namesMapToTheSameDatabase(this.context.getDatabaseName(), inDatabase) ? transaction : null;
}

View File

@@ -71,18 +71,18 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
}
/**
* This methods provides a native Neo4j transaction to be used from within a {@link Neo4jClient}.
* In most cases this the native transaction will be controlled from the Neo4j specific
* {@link org.springframework.transaction.PlatformTransactionManager}. However, SDN-RX provides support for other
* transaction managers as well. This methods registers a session synchronization in such cases on the foreign transaction manager.
* This methods provides a native Neo4j transaction to be used from within a {@link Neo4jClient}. In most cases this
* the native transaction will be controlled from the Neo4j specific
* {@link org.springframework.transaction.PlatformTransactionManager}. However, SDN provides support for other
* transaction managers as well. This methods registers a session synchronization in such cases on the foreign
* transaction manager.
*
* @param driver The driver that has been used as a synchronization object.
* @param driver The driver that has been used as a synchronization object.
* @param targetDatabase The target database
* @return An optional managed transaction or {@literal null} if the method hasn't been called inside
* an ongoing Spring transaction
* @return An optional managed transaction or {@literal null} if the method hasn't been called inside an ongoing
* Spring transaction
*/
public static @Nullable Transaction retrieveTransaction(final Driver driver,
@Nullable final String targetDatabase) {
public static @Nullable Transaction retrieveTransaction(final Driver driver, @Nullable final String targetDatabase) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return null;
@@ -90,7 +90,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
// Check whether we have a transaction managed by a Neo4j transaction manager
Neo4jTransactionHolder connectionHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager
.getResource(driver);
.getResource(driver);
if (connectionHolder != null) {
Transaction optionalOngoingTransaction = connectionHolder.getTransaction(targetDatabase);
@@ -100,7 +100,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
}
throw new IllegalStateException(
formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase));
formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase));
}
// Otherwise we open a session and synchronize it.
@@ -110,8 +110,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
connectionHolder = new Neo4jTransactionHolder(new Neo4jTransactionContext(targetDatabase), session, transaction);
connectionHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.registerSynchronization(
new Neo4jSessionSynchronization(connectionHolder, driver));
TransactionSynchronizationManager
.registerSynchronization(new Neo4jSessionSynchronization(connectionHolder, driver));
TransactionSynchronizationManager.bindResource(driver, connectionHolder);
return connectionHolder.getTransaction(targetDatabase);
@@ -120,8 +120,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) {
Assert.isInstanceOf(Neo4jTransactionObject.class, transaction,
() -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class,
transaction.getClass()));
() -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class,
transaction.getClass()));
return (Neo4jTransactionObject) transaction;
}
@@ -135,7 +135,7 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
protected Object doGetTransaction() throws TransactionException {
Neo4jTransactionHolder resourceHolder = (Neo4jTransactionHolder) TransactionSynchronizationManager
.getResource(driver);
.getResource(driver);
return new Neo4jTransactionObject(resourceHolder);
}
@@ -152,19 +152,15 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
TransactionConfig transactionConfig = createTransactionConfigFrom(definition);
boolean readOnly = definition.isReadOnly();
TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly);
try {
// Prepare configuration data
Neo4jTransactionContext context = new Neo4jTransactionContext(
databaseSelectionProvider.getDatabaseSelection().getValue(),
bookmarkManager.getBookmarks()
);
databaseSelectionProvider.getDatabaseSelection().getValue(), bookmarkManager.getBookmarks());
// Configure and open session together with a native transaction
Session session = this.driver
.session(sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseName()));
Session session = this.driver.session(sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseName()));
Transaction nativeTransaction = session.beginTransaction(transactionConfig);
// Synchronize on that
@@ -174,7 +170,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
TransactionSynchronizationManager.bindResource(this.driver, transactionHolder);
} catch (Exception ex) {
throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()), ex);
throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()),
ex);
}
}
@@ -228,7 +225,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
TransactionSynchronizationManager.unbindResource(driver);
}
static class Neo4jTransactionObject implements SmartTransactionObject {
private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder is required but not present. o_O";
@@ -236,16 +232,15 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
// The resource holder is null when the call to TransactionSynchronizationManager.getResource
// in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder.
// If it is null, there's no existing session / transaction.
@Nullable
private Neo4jTransactionHolder resourceHolder;
@Nullable private Neo4jTransactionHolder resourceHolder;
Neo4jTransactionObject(@Nullable Neo4jTransactionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's
* no existing transaction.
* Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's no existing
* transaction.
*
* @param resourceHolder A newly created resource holder with a fresh drivers session,
*/

View File

@@ -47,10 +47,9 @@ public final class Neo4jTransactionUtils {
}
public static SessionConfig sessionConfig(boolean readOnly, Collection<Bookmark> bookmarks,
@Nullable String databaseName) {
@Nullable String databaseName) {
SessionConfig.Builder builder = SessionConfig.builder()
.withDefaultAccessMode(readOnly ? AccessMode.READ : AccessMode.WRITE)
.withBookmarks(bookmarks);
.withDefaultAccessMode(readOnly ? AccessMode.READ : AccessMode.WRITE).withBookmarks(bookmarks);
if (databaseName != null) {
builder.withDatabase(databaseName);
@@ -60,8 +59,8 @@ public final class Neo4jTransactionUtils {
}
/**
* Maps a Spring {@link TransactionDefinition transaction definition} to a native Neo4j driver transaction.
* Only the default isolation leven ({@link TransactionDefinition#ISOLATION_DEFAULT}) and
* Maps a Spring {@link TransactionDefinition transaction definition} to a native Neo4j driver transaction. Only the
* default isolation leven ({@link TransactionDefinition#ISOLATION_DEFAULT}) and
* {@link TransactionDefinition#PROPAGATION_REQUIRED propagation required} behaviour are supported.
*
* @param definition The transaction definition passed to a Neo4j transaction manager
@@ -71,12 +70,14 @@ public final class Neo4jTransactionUtils {
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
throw new InvalidIsolationLevelException(
"Neo4jTransactionManager is not allowed to support custom isolation levels.");
"Neo4jTransactionManager is not allowed to support custom isolation levels.");
}
int propagationBehavior = definition.getPropagationBehavior();
if (!(propagationBehavior == TransactionDefinition.PROPAGATION_REQUIRED || propagationBehavior == TransactionDefinition.PROPAGATION_REQUIRES_NEW)) {
throw new IllegalTransactionStateException("Neo4jTransactionManager only supports 'required' or 'requires new' propagation.");
if (!(propagationBehavior == TransactionDefinition.PROPAGATION_REQUIRED
|| propagationBehavior == TransactionDefinition.PROPAGATION_REQUIRES_NEW)) {
throw new IllegalTransactionStateException(
"Neo4jTransactionManager only supports 'required' or 'requires new' propagation.");
}
TransactionConfig.Builder builder = TransactionConfig.builder();
@@ -97,10 +98,9 @@ public final class Neo4jTransactionUtils {
String _requestedDb = requestedDb == null ? defaultDatabase : String.format("'%s'", requestedDb);
return String.format("There is already an ongoing Spring transaction for %s, but you request %s", _currentDb,
_requestedDb);
_requestedDb);
}
private Neo4jTransactionUtils() {
}
private Neo4jTransactionUtils() {}
}

View File

@@ -27,12 +27,13 @@ import org.springframework.transaction.reactive.TransactionSynchronizationManage
* @author Michael J. Simons
* @since 1.0
*/
final class ReactiveNeo4jSessionSynchronization extends ReactiveResourceSynchronization<ReactiveNeo4jTransactionHolder, Object> {
final class ReactiveNeo4jSessionSynchronization
extends ReactiveResourceSynchronization<ReactiveNeo4jTransactionHolder, Object> {
private final ReactiveNeo4jTransactionHolder transactionHolder;
ReactiveNeo4jSessionSynchronization(TransactionSynchronizationManager transactionSynchronizationManager,
ReactiveNeo4jTransactionHolder transactionHolder, Driver driver) {
ReactiveNeo4jTransactionHolder transactionHolder, Driver driver) {
super(transactionHolder, driver, transactionSynchronizationManager);

View File

@@ -47,9 +47,11 @@ final class ReactiveNeo4jTransactionHolder extends ResourceHolderSupport {
return session;
}
@Nullable RxTransaction getTransaction(String inDatabase) {
@Nullable
RxTransaction getTransaction(String inDatabase) {
return Neo4jTransactionUtils.namesMapToTheSameDatabase(this.context.getDatabaseName(), inDatabase) ? transaction : null;
return Neo4jTransactionUtils.namesMapToTheSameDatabase(this.context.getDatabaseName(), inDatabase) ? transaction
: null;
}
Mono<Bookmark> commit() {

View File

@@ -70,46 +70,41 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction
public static Mono<RxTransaction> retrieveReactiveTransaction(final Driver driver, final String targetDatabase) {
return TransactionSynchronizationManager.forCurrentTransaction() // Do we have a Transaction context?
// Bail out early if synchronization between transaction managers is not active
.filter(TransactionSynchronizationManager::isSynchronizationActive)
.flatMap(tsm -> {
// Get an existing holder
ReactiveNeo4jTransactionHolder existingTxHolder = (ReactiveNeo4jTransactionHolder) tsm
.getResource(driver);
// Bail out early if synchronization between transaction managers is not active
.filter(TransactionSynchronizationManager::isSynchronizationActive).flatMap(tsm -> {
// Get an existing holder
ReactiveNeo4jTransactionHolder existingTxHolder = (ReactiveNeo4jTransactionHolder) tsm.getResource(driver);
// And use it if there is any
if (existingTxHolder != null) {
return Mono.just(existingTxHolder);
}
// And use it if there is any
if (existingTxHolder != null) {
return Mono.just(existingTxHolder);
}
// Otherwise open up a new native transaction
return Mono.defer(() -> {
RxSession session = driver.rxSession(Neo4jTransactionUtils.defaultSessionConfig(targetDatabase));
return Mono.from(session.beginTransaction(TransactionConfig.empty())).map(tx -> {
// Otherwise open up a new native transaction
return Mono.defer(() -> {
RxSession session = driver.rxSession(Neo4jTransactionUtils.defaultSessionConfig(targetDatabase));
return Mono.from(session.beginTransaction(TransactionConfig.empty())).map(tx -> {
ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder(
new Neo4jTransactionContext(targetDatabase), session, tx);
newConnectionHolder.setSynchronizedWithTransaction(true);
ReactiveNeo4jTransactionHolder newConnectionHolder = new ReactiveNeo4jTransactionHolder(
new Neo4jTransactionContext(targetDatabase), session, tx);
newConnectionHolder.setSynchronizedWithTransaction(true);
tsm.registerSynchronization(
new ReactiveNeo4jSessionSynchronization(tsm, newConnectionHolder, driver));
tsm.registerSynchronization(new ReactiveNeo4jSessionSynchronization(tsm, newConnectionHolder, driver));
tsm.bindResource(driver, newConnectionHolder);
return newConnectionHolder;
tsm.bindResource(driver, newConnectionHolder);
return newConnectionHolder;
});
});
});
})
.map(connectionHolder -> {
}).map(connectionHolder -> {
RxTransaction transaction = connectionHolder.getTransaction(targetDatabase);
if (transaction == null) {
throw new IllegalStateException(
Neo4jTransactionUtils.formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase));
throw new IllegalStateException(Neo4jTransactionUtils
.formatOngoingTxInAnotherDbErrorMessage(connectionHolder.getDatabaseName(), targetDatabase));
}
return transaction;
}
)
// If not, than just don't open a transaction
.onErrorResume(NoTransactionException.class, nte -> Mono.empty());
})
// If not, than just don't open a transaction
.onErrorResume(NoTransactionException.class, nte -> Mono.empty());
}
private static ReactiveNeo4jTransactionObject extractNeo4jTransaction(Object transaction) {
@@ -156,37 +151,36 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction
transactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly);
return databaseSelectionProvider.getDatabaseSelection()
.switchIfEmpty(Mono.just(DatabaseSelection.undecided()))
.map(databaseName -> new Neo4jTransactionContext(databaseName.getValue(), bookmarkManager.getBookmarks()))
.map(context -> Tuples.of(context, this.driver.rxSession(
Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseName()))))
.flatMap(contextAndSession -> Mono
.from(contextAndSession.getT2().beginTransaction(transactionConfig))
.map(nativeTransaction -> new ReactiveNeo4jTransactionHolder(contextAndSession.getT1(), contextAndSession.getT2(), nativeTransaction))
)
.doOnNext(transactionHolder -> {
transactionHolder.setSynchronizedWithTransaction(true);
transactionObject.setResourceHolder(transactionHolder);
transactionSynchronizationManager.bindResource(this.driver, transactionHolder);
});
return databaseSelectionProvider.getDatabaseSelection().switchIfEmpty(Mono.just(DatabaseSelection.undecided()))
.map(
databaseName -> new Neo4jTransactionContext(databaseName.getValue(), bookmarkManager.getBookmarks()))
.map(
context -> Tuples
.of(context,
this.driver.rxSession(Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(),
context.getDatabaseName()))))
.flatMap(contextAndSession -> Mono.from(contextAndSession.getT2().beginTransaction(transactionConfig))
.map(nativeTransaction -> new ReactiveNeo4jTransactionHolder(contextAndSession.getT1(),
contextAndSession.getT2(), nativeTransaction)))
.doOnNext(transactionHolder -> {
transactionHolder.setSynchronizedWithTransaction(true);
transactionObject.setResourceHolder(transactionHolder);
transactionSynchronizationManager.bindResource(this.driver, transactionHolder);
});
}).then();
}
@Override
protected Mono<Void> doCleanupAfterCompletion(TransactionSynchronizationManager transactionSynchronizationManager,
Object transaction) {
Object transaction) {
return Mono
.just(extractNeo4jTransaction(transaction))
.map(r -> {
ReactiveNeo4jTransactionHolder holder = r.getRequiredResourceHolder();
r.setResourceHolder(null);
return holder;
})
.flatMap(ReactiveNeo4jTransactionHolder::close)
.then(Mono.fromRunnable(() -> transactionSynchronizationManager.unbindResource(driver)));
return Mono.just(extractNeo4jTransaction(transaction)).map(r -> {
ReactiveNeo4jTransactionHolder holder = r.getRequiredResourceHolder();
r.setResourceHolder(null);
return holder;
}).flatMap(ReactiveNeo4jTransactionHolder::close)
.then(Mono.fromRunnable(() -> transactionSynchronizationManager.unbindResource(driver)));
}
@Override
@@ -194,10 +188,9 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction)
.getRequiredResourceHolder();
return holder.commit()
.doOnNext(bookmark -> bookmarkManager.updateBookmarks(holder.getBookmarks(), bookmark))
.then();
.getRequiredResourceHolder();
return holder.commit().doOnNext(bookmark -> bookmarkManager.updateBookmarks(holder.getBookmarks(), bookmark))
.then();
}
@Override
@@ -205,26 +198,25 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction)
.getRequiredResourceHolder();
.getRequiredResourceHolder();
return holder.rollback();
}
@Override
protected Mono<Object> doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) throws TransactionException {
protected Mono<Object> doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction)
throws TransactionException {
return Mono
.just(extractNeo4jTransaction(transaction))
.doOnNext(r -> r.setResourceHolder(null))
.then(Mono.fromSupplier(() -> synchronizationManager.unbindResource(driver)));
return Mono.just(extractNeo4jTransaction(transaction)).doOnNext(r -> r.setResourceHolder(null))
.then(Mono.fromSupplier(() -> synchronizationManager.unbindResource(driver)));
}
@Override
protected Mono<Void> doResume(TransactionSynchronizationManager synchronizationManager, Object transaction, Object suspendedResources) throws TransactionException {
protected Mono<Void> doResume(TransactionSynchronizationManager synchronizationManager, Object transaction,
Object suspendedResources) throws TransactionException {
return Mono
.just(extractNeo4jTransaction(transaction))
.doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources))
.then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources)));
return Mono.just(extractNeo4jTransaction(transaction))
.doOnNext(r -> r.setResourceHolder((ReactiveNeo4jTransactionHolder) suspendedResources))
.then(Mono.fromRunnable(() -> synchronizationManager.bindResource(driver, suspendedResources)));
}
/*
@@ -233,7 +225,7 @@ public class ReactiveNeo4jTransactionManager extends AbstractReactiveTransaction
*/
@Override
protected Mono<Void> doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager,
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
GenericReactiveTransaction genericReactiveTransaction) throws TransactionException {
return Mono.fromRunnable(() -> {
ReactiveNeo4jTransactionObject transactionObject = extractNeo4jTransaction(genericReactiveTransaction);

View File

@@ -28,7 +28,7 @@ import org.springframework.data.repository.query.QueryByExampleExecutor;
*
* @author Michael J. Simons
* @author Ján Šúr
* @param <T> type of the domain class to map
* @param <T> type of the domain class to map
* @param <ID> identifier type in the domain class
* @since 1.0
*/
@@ -39,7 +39,8 @@ public interface Neo4jRepository<T, ID> extends PagingAndSortingRepository<T, ID
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#saveAll(java.lang.Iterable)
*/
@Override <S extends T> List<S> saveAll(Iterable<S> entities);
@Override
<S extends T> List<S> saveAll(Iterable<S> entities);
/*
* (non-Javadoc)
@@ -66,11 +67,13 @@ public interface Neo4jRepository<T, ID> extends PagingAndSortingRepository<T, ID
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
*/
@Override <S extends T> List<S> findAll(Example<S> example);
@Override
<S extends T> List<S> findAll(Example<S> example);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
*/
@Override <S extends T> List<S> findAll(Example<S> example, Sort sort);
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}

View File

@@ -29,5 +29,4 @@ import org.springframework.data.repository.reactive.ReactiveSortingRepository;
*/
@NoRepositoryBean
public interface ReactiveNeo4jRepository<T, ID>
extends ReactiveSortingRepository<T, ID>, ReactiveQueryByExampleExecutor<T> {
}
extends ReactiveSortingRepository<T, ID>, ReactiveQueryByExampleExecutor<T> {}

View File

@@ -21,8 +21,8 @@ import org.springframework.data.repository.config.RepositoryBeanDefinitionRegist
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
/**
* {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableNeo4jRepositories} annotation.
* The {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's
* {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableNeo4jRepositories} annotation. The
* {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's
* {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, a dedicated SPI to register beans
* during processing of configuration classes.
*

View File

@@ -105,10 +105,10 @@ public final class Neo4jRepositoryConfigurationExtension extends RepositoryConfi
public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {
builder.addPropertyValue("transactionManager",
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyReference("neo4jOperations",
source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME));
source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME));
builder.addPropertyReference("neo4jMappingContext",
source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
}
}

View File

@@ -21,8 +21,8 @@ import org.springframework.data.repository.config.RepositoryBeanDefinitionRegist
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
/**
* {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableReactiveNeo4jRepositories} annotation.
* The {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's
* {@link RepositoryBeanDefinitionRegistrarSupport} to enable {@link EnableReactiveNeo4jRepositories} annotation. The
* {@link RepositoryBeanDefinitionRegistrarSupport} is a dedicated implementation of Spring's
* {@code org.springframework.context.annotation.ImportBeanDefinitionRegistrar}, a dedicated SPI to register beans
* during processing of configuration classes.
*

Some files were not shown because too many files have changed in this diff Show More