Welcome SDN⚡️RX
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.
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 ReactiveNeo4jRepository
and 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:
- Use derived finder methods
- 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)
orOptional<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, roughlyMATCH (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.