Welcome SDN⚡️RX

Gerrit Meier
Neo4j Developer Blog
7 min readAug 28, 2019

Please note that Spring Data Neo4j RX replaces Spring Data Neo4j in the future. More information can be found here.

If you have followed the development of Neo4j you may have noticed that there is a milestone release of the upcoming 4.0 version. Besides all the other great features, the post mentions SDN/RX, the experimental new version of Spring Data Neo4j.

SDN/RX will be completely based on the Spring Framework and utilizing the support of the Spring Data commons project. It will not have an intermediate mapping layer like Neo4j-OGM between Spring Data and the database.

Introducing SDN⚡️RX

When we started to think about a new implementation of Spring Data Neo4j, we wanted to use the approach that Spring Data JDBC took as well:

Create a simple, understandable library and be opinionated in the way it should get used.

Knowing that reactive capability was coming to Neo4j, we started to base our approach around it. Adding full immutable object support to our must-have list gave us the constraints to get a picture in our heads how the library should work. Although we talk about reactive and the project’s name has a RX suffix, SDN/️RX supports everything it does for reactive also for an imperative synchronous programming model.

Photo by Ezekiel Elin on Unsplash

Getting started

To try out all the feature SDN/RX has right now, the best choice is to start a Neo4j 4.0 milestone release instance. If you have not yet downloaded it, you can get it from the Neo4j download page.

The current version 1.0.0-beta01 is available in Maven central under following coordinates:

<dependency>
<groupId>org.neo4j.springframework.data</groupId>
<artifactId>spring-data-neo4j-rx</artifactId>
<version>1.0.0-beta01</version>
</dependency>

The easiest way to get started is to create a Spring Boot application with the Spring Boot Starter. Choose the latest available milestone release of the 2.2.0 series as the base of the application. From our point of view it is the most common way to get started. The following parts assume that you created a blank Spring Boot 2.2.0 application.

Spring Boot starter

If you set up your application with Spring Boot, you can also benefit from a second project we created for SDN/RX: the SDN/RX Spring Boot starter.

Instead of defining a direct dependency to SDN/RX, you can define the dependency to the starter which also brings you support for the application configuration.

<dependency>
<groupId>org.neo4j.springframework.data</groupId>
<artifactId>spring-data-neo4j-rx-spring-boot-starter
</artifactId>
<version>1.0.0-beta01</version>
</dependency>

The starter will also pull in the dependencies for the official Spring Boot auto-configuration for the Neo4j Java Driver that lets you define the connection parameters etc. for the driver.

You can also use this starter as a stand-alone, driver-only solution in combination with your Spring Boot projects where an object mapping library is not needed.

Having the starter dependency in your application, you can now add the mandatory properties to the existing application.properties in your resources folder.

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

As you can see the namespaces for the properties are driver centric and not related to SDN/RX or Spring Data in general. The setup steps are now done and we can focus on modeling our domain.

Data modeling

The example project we will create is based on the Neo4j’s favorite domain: Movies. We are so into movies that you can even create a data set to play with right through the Neo4j Browser interface. Simply type :play movies and follow along the guide to the second step where you will get provided with the statements to create the graph.

The domain we are looking at is pretty minimal: We have a Movie and a Person entity. Let’s start with the Person entity.

@Node // I
public class Person {
@Id // II
private String name;
private int born; public Person(String name, int born) {
this.name = name;
this.born = born;
}
// skipping getter
}

As you can see the definition of the entity class is pretty straight forward:
I: We define it as a for mapping eligible class by annotating it with @Node.
II: Additionally we provide the information which is the property that represents the identifier with @Id and if it should get generated, in this case we define the name of the person (actor) as our business identifier. If we would decide to use the internal id mechanism of Neo4j, we could define@GeneratedValue on a Long typed property.
Of course it is possible to define an own IdGenerator when using the @GeneratedValue annotation.

Next up is the Movie entity.

@Node
public class Movie {
@Id
private final String title;
@Property("tagline") // I
private final String description;
@Relationship(type = "ACTED_IN", direction = INCOMING) // II
private Set<Person> actors;

@Relationship(type = "DIRECTED", direction = INCOMING)
private Set<Person> directors;
public Movie(String title, String description) {
this.title = title;
this.description = description;
}
// skipping getter
}

This looks pretty much the same as the other entity but has additional declarations of the relationship to Person entities.
I: If a property in our entity class does not represent the same property in the graph, we can tell SDN/RX to respect this during the mapping phases by using @Property.
II: All relationships need to be defined by using the @Relationship annotation. They can either point to an entity class or a List or Set type of other entity classes. As you can see, it is possible to declare different relationship types for the same entity types within one class.

In this example the relationship are defined as INCOMING to match the graph model. The default direction of the annotation would be OUTGOING.

Of course we now want to work with the domain we just created. In this post we focus on using of Spring Data’s repositories. There is another way to interact with the database in SDN/RX but this will get covered in one of our next blog posts about SDN/RX.

If you are familiar with Spring Data in general or Spring Data Neo4j in particular, the repository definition will look the same as expected. To create a repository for our Person entity, we just need to declare an interface named PersonRepository and extend the ReactiveNeo4jRepository with the entity-type and primary-key type as generic parameters.

public interface PersonRepository 
extends ReactiveNeo4jRepository<Person, String> {
}

This repository offers a lot of built-in CRUD methods that cover all of the basic functionality you need to interact with the database.

In case you want to use imperative types (in which the repository returns List and Optional instead of Flux and Mono, you must extend from Neo4jRepository instead of ReactiveNeo4jRepository.

public interface PersonRepository 
extends Neo4jRepository<Person, String> {
}

Now we are mostly done with the example but still need to interact with the repository. To keep things simple here, we create a test class annotated with @SpringBootTest to benefit from Spring Boot’s test support that will take care of the bean dependency management beside other things.

@SpringBootTest
class DemoApplicationIT {
@Autowired
private PersonRepository repository;
@Test
void loadAllPeopleFromGraph() {
int expectedPersonCount = 133;

StepVerifier.create(repository.findAll())
.expectNextCount(expectedPersonCount)
.verifyComplete();
}
}

Here we use one of the many data access methods findAll that the repository infrastructure provides out of the box. The test uses Project Reactor’s test dependency. Because of the nature of the reactive programming model we need a subscriber, in this case this is realized by the StepVerifier that waits for the signals (data) and can act/verify on arrival.

A test on the imperative version of that repository would likely use AssertJ assertThat(repository.findAll()).hasSize(expectedPersonCount) or similar.

While both ReactiveNeo4jRepositoryand Neo4jRepository offer the complete CRUD functionality for a domain type, we might need sometimes more data access options than those methods offer. To achieve this you have two choices:

  1. Use derived finder methods
  2. Define your own custom query

Derived finder methods

You can easily define you own methods within the repository interface by following a structured syntax. e.g. defining a method like
Mono<Person> findOneByName(String name) or
Optional<Person> findOneByName(String name) depending whether you used the reactive or the imperative version. This will create the query you need under the hood and it will exactly do what you expect: search and return the Person node from the graph with the given name, roughly
MATCH (p:Person) WHERE p.name = $name RETURN p

A test can prove this functionality and also shows the right mapping of the previously defined relationships. This would be the test for the imperative version:

@Test
void findPersonByName() {
Optional<Person> person = repository.findOneByName("Tom Hanks");
assertThat(person)
.map(PersonEntity::getBorn)
.isPresent().hasValue(1956);
}

For the reactive part this test is a little bit more complex:

@Test
void findPersonByName() {
StepVerifier.create(repository.findByName("Tom Hanks"))
.assertNext(personEntity -> {
assertThat(personEntity.getBorn()).isEqualTo(1956);
})
.verifyComplete();
}

These are of course just simple examples of what you can do with derived finder methods. There are plenty of features we support like find in ranges, concatenated conditions, etc. Those will all be listed in the upcoming documentation.

Custom queries

When derived finder methods cannot express what you want to achieve, it is a good idea to make use of custom queries. To do this you just need to define a method and annotate it with @Query.

@Query(
"MATCH (p:Person) WHERE (p)-[:ACTED_IN]->() AND (p)-[:DIRECTED]->() RETURN p")
Flux<Person> getPeopleWhoActedAndDirected();

Also this functionality can be proven working in a test case.

@Test
void findsPeopleWhoActedAndDirected() {
int expectedActorAndDirectorCount = 5;
StepVerifier.create(repository.getPersonsWhoActAndDirect())
.expectNextCount(expectedActorAndDirectorCount)
.verifyComplete();
}

Please keep in mind if we do not return the related entities in the custom queries, the relationship-fields in the entities won’t get populated.

Continue exploring

If you want to peek in the sources of SDN/RX, you can do this on Github. There are also complete examples of the topics touched in this post.

The reactive example can be used with VSCode remote containers. To learn more about this, read and watch the introduction from Michael Simons’ blog post.

(near) Future plans

A lot of work besides code will be the documentation and — this is very important for us — a migration path document.

Of course we would love your feedback on SDN/RX, so please head over to our Neo4j community site if you have questions or create issues on the repository for things you ran into.

Although we will keep supporting Spring Data Neo4j and Neo4j-OGM for a long time along with SDN/RX, we would be happy to bring as many of you over to our (brave) new world.

--

--