Developer Guides Getting Started Getting Started What is a Graph Database? Intro to Graph DBs Video Series Concepts: RDBMS to Graph Concepts: NoSQL to Graph Getting Started Resources Neo4j Graph Platform Graph Platform Overview Neo4j Desktop Intro Neo4j Browser Intro… Read more →

Developer Guides

Want to Speak? Get $ back.

Spring Data Neo4j RX

1. Intro

Please download the public milestone release of Neo4j 4.0 to use the reactive features of SDN/RX. You’ll find it in the Download Center under “Pre-Releases” as Neo4j 4.0.0-alpha09mr02. Reactive functionality is used by org.neo4j.springframework.data.core.ReactiveNeo4jClient and org.neo4j.springframework.data.repository.ReactiveNeo4jRepository` You can of course use SDN/RX against Neo4j 3.5 but only with org.neo4j.springframework.data.core.Neo4jClient and the imperative repositories (org.neo4j.springframework.data.repository.Neo4jRepository, org.springframework.data.repository.CrudRepository and related).

1.1. What’s in the box?

Spring Data Neo4j⚡️RX or in short “SDN/RX” is a next-generation Spring Data module, created and maintained by Neo4j, Inc. in close collaboration with Pivotals Spring Data Team.

SDN/RX relies completely on the 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

  • Full support for immutable entities and thus full support for Kotlin’s data classes right from the start
  • 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 Spring Data Neo4j in parallel and will replace it eventually when they are on feature parity in regards of repository support and mapping.

1.2. Why should I use SDN/RX in favor of SDN+OGM

SDN/RX has several features not present in SDN+OGM, notably

  • Full support for Springs reactive story, including reactive transaction
  • Full support for Query By Example
  • Full support for fully immutable entities
  • Support for all modifiers and variations of derived finder methods, including spatial queries

1.3. Do I need both SDN/RX and Spring Data Neo4j?

No.

They are mutually exclusive and you cannot mix them in one project.

1.4. How does SDN/RX relate to Neo4j-OGM?

Neo4j-OGM is an Object Graph Mapping library, which is mainly used by Spring Data Neo4j as it’s 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.

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.

1.5. Does SDN/RX support connections over HTTP to Neo4j?

No.

1.6. Does SDN/RX support embedded Neo4j?

Embedded Neo4j has multiple facets to it:

1.6.1. Does SDN/RX provide an embedded instance for your application?

No.

1.6.2. Does SDN/RX 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.

1.6.3. What about a Neo4j template class?

Please see Neo4j client.

2. Getting started

We provide a Spring Boot starter for SDN/RX. As with any other Spring Boot starter, the only thing you have to do is to include the starter module via your dependency management. If you don’t configure anything, than the starter assumes bolt://localhost:7687 as Neo4j URI and a server that 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 reference, look the dedicated manual, too.

SDN/RX supports

  • The well known and understood imperative aka blocking programming model (much like Spring Data JDBC or JPA)
  • Reactive programming based on Reactive Streams, including full support for reactive transactions.

Those are all included in the same binary. The reactive programming model requires a 4.0 Neo4j server on the database side and reactive Spring on the other hand. Have a look at the examples directory for all examples.

2.1. Preparing the database

For this example, we stay within the movie graph, as it comes for free with every Neo4j instance.

If you don’t have a running database but Docker installed, please run:

Listing 1. Start a local Neo4j instance inside Docker.
docker run --publish=7474:7474 --publish=7687:7687 neo4j:4.0.0-alpha09

You know can access http://localhost:7474. At first visit, you have to change your password. We chose secret in the examples. Note the command ready to run in the prompt. Execute it to fill your database with some test data.

2.2. Create a new Spring Boot project

The easiest way to setup a Spring Boot project is start.spring.io (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.

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.

2.2.1. Maven

You can issue a CURL request against the Spring Initializer to create a basic Maven project:

Listing 2. Create a basic Maven project with the Spring Initializr
curl https://start.spring.io/starter.tgz \
  -d dependencies=webflux,actuator \
  -d bootVersion=2.2.0.M5 \
  -d baseDir=Neo4jSpringBootExample \
  -d name=Neo4j%20SpringBoot%20Example | tar -xzvf -

This will create a new folder Neo4jSpringBootExample. As this starter is not yet on the initializer, you’ll have to add the following dependency manually to your pom.xml:

Listing 3. Inclusion of the spring-data-neo4j-rx-spring-boot-starter in a Maven project
<dependency>
	<groupId>org.neo4j.springframework.data</groupId>
	<artifactId>spring-data-neo4j-rx-spring-boot-starter</artifactId>
	<version>1.0.0-beta01</version>
</dependency>

You would also add the dependency manually in case of an existing project.

2.2.2. Gradle

The idea is the same, just generate a Gradle project:

Listing 4. Create a basic Gradle project with the Spring Initializr
curl https://start.spring.io/starter.tgz \
  -d dependencies=webflux,actuator \
  -d type=gradle-project \
  -d bootVersion=2.2.0.M5 \
  -d baseDir=Neo4jSpringBootExampleGradle \
  -d name=Neo4j%20SpringBoot%20Example | tar -xzvf -

The dependency for Gradle looks like this and must be added to build.gradle:

Listing 5. Inclusion of the spring-data-neo4j-rx-spring-boot-starter in a Gradle project
dependencies {
    compile 'org.neo4j.springframework.data:spring-data-neo4j-rx-spring-boot-starter:1.0.0-beta01'
}

You would also add the dependency manually in case of an existing project.

2.2.3. Configuration

Now open any of those projects in your favorite IDE. Find application.properties and configure your Neo4j credentials:

org.neo4j.driver.uri=bolt://localhost:7687
org.neo4j.driver.authentication.username=neo4j
org.neo4j.driver.authentication.password=secret

This is the bare minimum of what you need to connect to a Neo4j instance.

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.

2.3. Creating your domain

Our domain layer should accomplish two things:

  • Map your Graph to objects
  • Provide access to those

2.3.1. Example Node-Entity

SDN/RX fully supports unmodifiable entities, for both Java and data classes in Kotlin. Therefor we will focus on immutable entities here, Listing 6 shows a such an entity.

SDN/RX supports all data types the Neo4j Java Driver supports, see Map Neo4j types to native language types inside the chapter “The Cypher type system”. Future versions will support additional converters.
Listing 6. 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.annotation.PersistenceConstructor;

@Node("Movie") (1)
public class MovieEntity {

	@Id @GeneratedValue (2)
	private Long id;

	private final String title;

	@Property("tagline") (3)
	private final String description;

	public MovieEntity(String title, String description) { (4)
		this.id = null;
		this.title = title;
		this.description = description;
	}

	public Long getId() {
		return id;
	}

	public String getTitle() {
		return title;
	}

	public String getDescription() {
		return description;
	}

	public MovieEntity withId(Long id) { (5)
		if (this.id == null) {
			return this;
		} else {
			MovieEntity newObject = new MovieEntity(this.title, this.description);
			newObject.id = this.id;
			return newObject;
		}
	}
}
1 @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
2 Each entity has to have an id. The combination of @Id and @GeneratedValue configures SDN/RX to use Neo4j’s internal id.
3 This shows @Property as a way to use a different name for the field than for the Graphs property.
4 This is the constructor to be used by your application code. It set’s the id to null, as the field containing the internal id should never be manipulated.
5 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.

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.

The same entity using Project Lombok annotations for creating value objects is shown in Listing 7

Listing 7. MovieEntity.java
import lombok.Value;

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;

@Node("Movie")
@Value(staticConstructor = "of")
public class MovieEntity {

	@Id @GeneratedValue
	private Long id;

	private String title;

	@Property("tagline")
	private String description;
}

And finally, Listing 8 shows the corresponding entity as a Kotlin Data Class.

Listing 8. MovieEntity.kt
@Node("Movie")
data class MovieEntity (

    @Id
    @GeneratedValue
    val id: Long? = null,

    val title: String,

    @Property("tagline")
    val description: String
)

2.3.2. 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

  • org.springframework.data.repository.Repository
  • org.springframework.data.repository.CrudRepository
  • org.springframework.data.repository.reactive.ReactiveCrudRepository
  • org.springframework.data.repository.reactive.ReactiveSortingRepository

Chose imperative and reactive accordingly.

While technically not prohibited, it is not recommended to mix imperative and reactive database acccess in the same application. We won’t support 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. Most of the time it’s harder to take something away, than add. Furthermore, using store specifics leaks your store into your domain. From a performance point of view, there is no penalty.

We like the store agnostic way of doing things and chose the first approach in the example. A repository fitting to any of the movie entites above looks like this:

Listing 9. MovieRepository.java
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.data.domain.Example;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface MovieRepository extends ReactiveCrudRepository<MovieEntity, Long> {

	Mono<MovieEntity> findOneByTitle(String title); (1)

	Flux<MovieEntity> findAll(Example<MovieEntity> example);
}
1 The declaration of these two methods is purely optional, if not needed, don’t add them. We use them in Listing 25 and Listing 10.

This repository can be used in any Spring component like this:

Listing 10. MovieController.java
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.neo4j.springframework.data.examples.spring_boot.domain.MovieEntity;
import org.neo4j.springframework.data.examples.spring_boot.domain.MovieRepository;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/movies")
public class MovieController {

	private final MovieRepository movieRepository;

	public MovieController(MovieRepository movieRepository) {
		this.movieRepository = movieRepository;
	}

	@PutMapping
	Mono<MovieEntity> createOrUpdateMovie(@RequestBody MovieEntity newMovie) {
		return movieRepository.save(newMovie);
	}

	@GetMapping(value = { "", "/" }, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	Flux<MovieEntity> getMovies() {
		return movieRepository
			.findAll();
	}

	@GetMapping("/by-title")
	Mono<MovieEntity> byTitle(@RequestParam String title) {
		return movieRepository.findOneByTitle(title);
	}

	@DeleteMapping("/{id}")
	Mono<Void> delete(@PathVariable Long id) {
		return movieRepository.deleteById(id);
	}
}
Testing reactive code is done with a reactor.test.StepVerifier. Have a look at the corresponding documentation of Project Reactor.

3. Conversions

3.1. Build-in conversion

We support a broad range of conversions out of the box. Find the list of supported cypher types in the official drivers manual: Working with Cypher values.

Primitive types of wrapper types are equally supported.

Domain type Cypher type Maps directly to native type

java.lang.Boolean

Boolean

boolean[]

List of Boolean

java.lang.Long

Integer

long[]

List of Integer

java.lang.Double

Float

java.lang.String

String

java.lang.String[]

List of String

byte[]

ByteArray

java.lang.Byte

ByteArray with length 1

java.lang.Character

String with length 1

char[]

List of String with length 1

java.util.Date

String formatted as ISO 8601 Date (yyyy-MM-dd’T’HH:mm:ss.SSSXXX

double[]

List of Float

java.lang.Float

String

float[]

List of String

java.lang.Integer

Integer

int[]

List of Integer

java.util.Locale

String formatted as BCP 47 language tag

java.lang.Short

Integer

short[]

List of Integer

java.math.BigDecimal

String

java.math.BigInteger

String

java.time.LocalDate

Date

java.time.OffsetTime

Time

java.time.LocalTime

LocalTime

java.time.ZonedDateTime

DateTime

java.time.LocalDateTime

LocalDateTime

java.time.Period

Duration

java.time.Duration

Duration

org.neo4j.driver.types.IsoDuration

Duration

org.neo4j.driver.types.Point

Point

org.neo4j.springframework.data.types.CartesianPoint2d

Point with CRS 4326

org.neo4j.springframework.data.types.CartesianPoint3d

Point with CRS 4979

org.neo4j.springframework.data.types.GeographicPoint2d

Point with CRS 7203

org.neo4j.springframework.data.types.GeographicPoint3d

Point with CRS 9157

org.springframework.data.geo.Point

Point with CRS 4326 and x/y corresponding to lat/long

4. Neo4jClient

Spring Data Neo4j⚡️RX comes with a Neo4j Client, providing a human readable layer on top of Neo4js Java driver. It has the following main goals

  1. Integrate into Springs transaction management, for both imperative and reactive scenarios
  2. Participate in JTA-Transactions if necessary
  3. Provide a consistent API for both imperative and reactive scenarios
  4. Don’t add any mapping overhead

SDN/RX relies on all those features and uses them to fulfill it’s entity mapping features.

The plain java driver is a very versatile tool and provides an asynchronous API in addition to the imperative and reactive versions. SDN/RX uses it as directly as possible while being as user friendly and idiomatic as possible, too.

The Neo4j Client comes in two flavors:

  • org.neo4j.springframework.data.core.Neo4jClient
  • org.neo4j.springframework.data.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.

4.1. Imperative or reactive?

Interactions with a Neo4j client usually ends with a call to

  • fetch().one()
  • fetch().first()
  • 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 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. The publisher can only be subscribed once.

4.2. Getting an instance of the client

As with most things in SDN/RX, both clients depend on a configured driver instance.

Listing 11. Creating an instance of the imperative Neo4j client
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;

import org.neo4j.springframework.data.core.Neo4jClient;

public class Demo {

    public static void main(String...args) {

        Driver driver = GraphDatabase
            .driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "secret"));

        Neo4jClient client = Neo4jClient.create(driver);
    }
}

The driver can only open a reactive session against a 4.0 database and will fail with an exception on any lower version.

Listing 12. Creating an instance of the reactive Neo4j client
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;

import org.neo4j.springframework.data.core.ReactiveNeo4jClient;

public class Demo {

    public static void main(String...args) {

        Driver driver = GraphDatabase
            .driver("neo4j://localhost:7687", AuthTokens.basic("neo4j", "secret"));

        ReactiveNeo4jClient client = ReactiveNeo4jClient.create(driver);
    }
}
Make sure you use the same driver instance for the client as you used for providing a Neo4jTransactionManager or ReactiveNeo4jTransactionManager 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 provides ready to use beans of the Neo4j client that fit the environment (imperative or reactive) and you usually don’t have to configure your own instance.

4.3. Usage

4.3.1. 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 fluent API of the client allows to specify the target database exactly once, after the declaration of the query to execute. Listing 13 demonstrates it with the reactive client:

Listing 13. Selecting the target database
Flux<Map<String, Object>> allActors = client
	.query("MATCH (p:Person) RETURN p")
	.in("neo4j") (1)
	.fetch()
	.all();
1 Select the target database in which the query is to be executed

4.3.2. Specifying queries

The interaction with the clients starts with a query. A query can be defined by a plain String or a Supplier<String>. The supplier will be evaluated as late as possible and can be provided by any query builder.

Listing 14. Specifying a query
Mono<Map<String, Object>> firstActor = client
	.query(() -> "MATCH (p:Person) RETURN p")
	.fetch()
	.first();

4.3.3. Retrieving results

As the previous listings shows, the interaction with the client always ends with a call to fetch and how many results shall be received. Both reactive and imperative client offer

one()

Expect exactly one result from the query

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 when subscribed to.

If you don’t expect any results from your query, than use run() after specificity the query.

Listing 15. Retrieving result summaries in a reactive way
Mono<ResultSummary> summary = reactiveClient
    .query("MATCH (m:Movie) where m.title = 'Aeon Flux' DETACH DELETE m")
    .run();

summary
    .map(ResultSummary::counters)
    .subscribe(counters ->
        System.out.println(counters.nodesDeleted() + " nodes have been deleted")
    ); (1)
1 The actual query is triggered here by subscribing to the publisher

Please take a moment to compare both listings and understand the difference when the actual query is triggered.

Listing 16. Retrieving result summaries in a imperative way
ResultSummary resultSummary = imperativeClient
	.query("MATCH (m:Movie) where m.title = 'Aeon Flux' DETACH DELETE m")
	.run(); (1)

SummaryCounters counters = resultSummary.counters();
System.out.println(counters.nodesDeleted() + " nodes have been deleted")
1 Here the query is triggered immediate

4.3.4. Mapping parameters

Queries can contain named parameters ($someName). The Neo4j client allows comfortable binding to those.

The client doesn’t check whether all parameters are bound or whether there are to many values. That is left to the driver. However the client prevents you from using a parameter name twice.

You can either map simple types that the Java driver understands or complex classes. Please have a look at the drivers manual, to see which simple types are understood.

Listing 17. Mapping simple types
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", "Li.*");

Flux<Map<String, Object>> directorAndMovies = client
	.query(
		"MATCH (p:Person) - [:DIRECTED] -> (m:Movie {title: $title}), (p) - [:WROTE] -> (om:Movie) " +
			"WHERE p.name =~ $name " +
			"  AND p.born < $someDate.year " +
			"RETURN p, om"
	)
	.bind("The Matrix").to("title") (1)
	.bind(LocalDate.of(1979, 9, 21)).to("someDate")
	.bindAll(parameters) (2)
	.fetch()
	.all();
1 There’s a fluent API for binding simple types
2 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.

You can provide a Function<T, Map<String, Object>> for any given domain object like an owner of bicycles in Listing 18 to the Neo4j client to map those domain objects to parameters the driver can understand.

Listing 18. Example of a domain type
public class Director {

    private final String name;

    private final List<Movie> movies;

    Director(String name, List<Movie> movies) {
        this.name = name;
        this.movies = new ArrayList<>(movies);
    }

    public String getName() {
        return name;
    }

    public List<Movie> getMovies() {
        return Collections.unmodifiableList(movies);
    }
}

public class Movie {

    private final String title;

    public Movie(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

The mapping function has to fill in all named parameters that might occur in the query like Listing 19 shows:

Listing 19. Using a mapping function for binding domain objects
Director joseph = new Director("Joseph Kosinski",
        Arrays.asList(new Movie("Tron Legacy"), new Movie("Top Gun: Maverick")));

Mono<ResultSummary> summary = client
    .query(""
        + "MERGE (p:Person {name: $name}) "
        + "WITH p UNWIND $movies as movie "
        + "MERGE (m:Movie {title: movie}) "
        + "MERGE (p) - [o:DIRECTED] -> (m) "
    )
    .bind(joseph).with(director -> { (1)
        Map<String, Object> mappedValues = new HashMap<>();
        List<String> movies = director.getMovies().stream()
            .map(Movie::getTitle).collect(Collectors.toList());
        mappedValues.put("name", director.getName());
        mappedValues.put("movies", movies);
        return mappedValues;
    })
    .run();
1 The with method allows for specifying the binder function

4.3.5. Working with result objects

Both clients return collections or publishers of maps (Map<String, Object>). Those maps corresponds exactly with the records that a query might have produced.

In addition, you can plugin your own BiFunction<TypeSystem, Record, T> through fetchAs to reproduce your domain object.

Listing 20. Using a mapping function for reading domain objects
Mono<Director> lily = client
    .query(""
        + " MATCH (p:Person {name: $name}) - [:DIRECTED] -> (m:Movie)"
        + "RETURN p, collect(m) as movies")
    .bind("Lilly Wachowski").to("name")
    .fetchAs(Director.class).mappedBy((TypeSystem t, Record record) -> {
        List<Movie> movies = record.get("movies")
            .asList(v -> new Movie((v.get("title").asString())));
        return new Director(record.get("name").asString(), movies);
    })
    .one();

TypeSystem gives access to the types the underlying Java driver used to fill the record.

4.3.6. 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. 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. Returning an empty optional is ok.

Listing 21. Delegate database interaction to an imperative StatementRunner
Optional<Long> result = client
    .delegateTo((StatementRunner runner) -> {
        // Do as many interactions as you want
        long numberOfNodes = runner.run("MATCH (n) RETURN count(n) as cnt")
            .single().get("cnt").asLong();
        return Optional.of(numberOfNodes);
    })
    // .in("aDatabase") (1)
    .run();
1 The database selection as described in Section 4.3.1 is optional

The reactive version receives a RxStatementRunner.

Listing 22. Delegate database interaction to a reactive RxStatementRunner
Mono<Integer> result = client
    .delegateTo((RxStatementRunner runner) ->
        Mono.from(runner.run("MATCH (n:Unused) DELETE n").summary())
            .map(ResultSummary::counters)
            .map(SummaryCounters::nodesDeleted))
    // .in("aDatabase") (1)
    .run();
1 Optional selection of the target database

Note that in both Listing 21 and Listing 22 the types of the runner have only been stated to provide more clarity to reader of this manual.

5. Q&A

5.1. Do I need to use Neo4j specific annotations?

No. You are free to use the following, equivalent Spring Data annotations:

SDN/RX Neo4j specific annotation Spring Data common annotation Purpose Difference

org.neo4j.springframework.data.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.annotation.Persistent

Marks the class as persistent entity.

@Node allows customizing the labels

5.2. Howto use assigned ids?

Just @Id without @GeneratedValue and fill your id attribute via a constructor parameter or a setter or wither. See this blog post for some general remarks about finding good ids.

5.3. Howto use externally generated ids?

We provide the interface org.neo4j.springframework.data.core.schema.IdGenerator. Implement it anyway you want and configure your implementation like this:

Listing 23. ThingWithGeneratedId.java
@Node
public class ThingWithGeneratedId {

	@Id @GeneratedValue(TestSequenceGenerator.class)
	private String theId;
}

If you pass in the name of a class to @GeneratedValue, this class must have a no-args default constructor. You can however use a string as well:

Listing 24. ThingWithIdGeneratedByBean.java
@Node
public class ThingWithIdGeneratedByBean {

	@Id @GeneratedValue(generatorRef = "idGeneratingBean")
	private String theId;
}

With that, idGeneratingBean refers to a bean in the Spring context. This might be useful for sequence generating.

Setters are not required on non-final fields for the id.

5.4. Howto audit entities?

All Spring Data annotations are supported. Those are

  • org.springframework.data.annotation.CreatedBy
  • org.springframework.data.annotation.CreatedDate
  • org.springframework.data.annotation.LastModifiedBy
  • org.springframework.data.annotation.LastModifiedDate

5.5. Howto use Find by example?

Find by example is a new feature in SDN/RX. You instantiate an entity or use one already persisted and from it, you create an org.springframework.data.domain.Example. If you repository extends org.neo4j.springframework.data.repository.Neo4jRepository or org.neo4j.springframework.data.repository.ReactiveNeo4jRepository, you can immedialty use the available findBy methods taking in an example, like shown in Listing 25

Listing 25. findByExample in Action
Example<MovieEntity> movieExample = Example.of(new MovieEntity("The Matrix", null));
Flux<MovieEntity> movies = this.movieRepository.findAll(movieExample);

movieExample = Example.of(
	new MovieEntity("Matrix", null),
	ExampleMatcher
	    .matchingAny()
        .withMatcher(
        	"title",
        	ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING)
        )
);
movies = this.movieRepository.findAll(movieExample);

5.6. Can I use SDN/RX 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.

6. Features to be added

  • Versioning support
  • Some events
  • Custom datatype converters

7. Migrating from SDN+OGM to SDN/RX

As the relationship mapping of SDN/RX is not yet fully complete, those topics are not adressed here. Also, SDN/RX is still in alpha, so things might change along the way. Please See this migrating guide currently as an ongoing effort, that helps you considering your current state and where you might want to go.

7.1. Known issues with past SDN+OGM migrations

SDN+OGM has had quite a history over the years and we understand that migrating big application systems is neither fun nor something that provides immediate profit. The main issues we observed when migrating from older versions of Spring Data Neo4j to newer ones are roughly in order the following:

Having skipped more than one major upgrade

While Neo4j-OGM can be used stand alone, Spring Data Neo4j cannot. It depends to large extend on the Spring Data and therefore, on the Spring Framework itself, which eventually affects large parts of your application. Depending on how the application has been structured, that is, how much the any of the framework part leaked into your business code, the more you have to adapt your application. It get’s worse when you have more than one Spring Data module in your application, if you accessed a relational database in the same service layer as your graph database. Updating two object mapping frameworks is not fun.

Relying on a embedded database configured through Spring Data itself

The embedded database in a SDN+OGM project is configured by Neo4j-OGM. Say you want to upgrade from Neo4j 3.0 to 3.5, you can’t without upgrading your whole application. Why is that? As you chose to embed a database into your application, you tied yourself into the modules that configure this embedded database. To have another, embedded database version, you have to upgrade the module that configured it, because the old one does not support the new database. As there is always a Spring Data version corresponding to Neo4j-OGM, you would have to upgrade that as well. Spring Data however depends on Spring Framework and than the arguments from the first bullet apply.

Being unsure about which building blocks to include

It’s not easy to get the terms right. We wrote the building blocks of an SDN+OGM setting here. It may be so that all of them have been added by coincidence and you’re dealing with a lof of conflicting dependencies.

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. 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.

7.2. Prepare the migration from SDN+OGM Lovelace or SDN+OGM Moore to SDN/RX

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.

First, you must make sure that your application runs against Neo4j in server mode over the Bolt protocol, which means work in two of three cases:

7.2.1. You’re on embedded

You have added org.neo4j:neo4j-ogm-embedded-driver and org.neo4j:neo4j to you project and starting the database via OGM facilities. This is no longer supported and you have to setup a standard Neo4j server (both standalone and cluster are supported).

The above dependencies have to be removed.

Migrating from the embedded solution is probably the thoughtest 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.

7.2.2. 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:// protocol, which takes care of routing, too.

7.2.3. You’re already using Bolt indirectly

A default SDN+OGM project uses org.neo4j:neo4j-ogm-bolt-driver and thus indirectly, the pure Java Driver. You can keep your existing URL.

7.3. Migrating

Once you have made sure, that your SDN+OGM application works over Bolt as expected, you can start migrating to SDN/RX.

  • 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 our new starter. Any of those properties can be configured through standard Spring Boot means. You’ll especially have to adapt the properties for the url and authentication, see Listing 26
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.
Listing 26. Old and new properties compared
# Old
spring.data.neo4j.embedded.enabled=false # No longer support
spring.data.neo4j.uri=bolt://localhost:7687
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
Those new properties might be changed in the future again when SDN/RX 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:

Old New

org.neo4j.ogm.annotation.NodeEntity

org.neo4j.springframework.data.core.schema.Node

org.neo4j.ogm.annotation.GeneratedValue

org.neo4j.springframework.data.core.schema.GeneratedValue

org.neo4j.ogm.annotation.Id

org.neo4j.springframework.data.core.schema.GeneratedValue

org.neo4j.ogm.annotation.Property

org.neo4j.springframework.data.core.schema.Property

org.neo4j.ogm.annotation.Relationship

org.neo4j.springframework.data.core.schema.Relationship

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.