1. Overview
Neo4j is a popular graph database management system designed to store, manage, and query data using a graph-based model. We’ll learn how to configure the project and use the various components of Spring Data Neo4j to interact with a Neo4j database.
In this tutorial, we’ll look at how to use Spring Data Neo4j.
2. Project Setup
Let’s start by setting up our project. We’ll create a Spring Boot application that contains entities and repositories that interact with the database. We’ll then look at configuring the project to connect to the database and test the repositories.
2.1. Dependencies
First, let’s add the required dependencies to our project. We’ll add the Spring Boot Starter Data Neo4j dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
<version>2.7.14</version>
</dependency>
2.2. Neo4j Harness
For testing purposes, we’ll use Neo4j Harness:
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Neo4j Harness is a library that allows us to run a Neo4j database in an embedded mode. This is useful for in-memory testing without installing a Neo4j database.
2.3. Configuring Neo4J Connection
To connect to the database, we need to configure the URI, username, and password. Let’s configure these properties in the application.properties file:
spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=password
The above properties help connect to a local Neo4j database with the username neo4j and password password.
Next, let’s configure the dialect for the database. To do this, we need to create a bean of type org.neo4j.ogm.config.Configuration:
@Configuration
public class Neo4jConfig {
@Bean
Configuration cypherDslConfiguration() {
return Configuration.newConfig()
.withDialect(Dialect.NEO4J_5).build();
}
}
Here, we have set the dialect to NEO4J_5. This directs Spring Data Neo4J to generate types and queries using Neo4J 5 specifications.
3. Code Example
Now that we have our project set up, let’s see how to use Spring Data Neo4j to interact with the database. We’ll look at the various components of Spring Data Neo4j and how to use them.
3.1. Neo4J Entities
Let’s start by creating entities that will be used in our repositories.
First, we’ll create a Book entity:
@Node("Book")
public class Book {
@Id
private String isbn;
@Property("name")
private String title;
private Integer year;
@Relationship(type = "WRITTEN_BY", direction = Relationship.Direction.OUTGOING)
private Author author;
// Constructor, getters and setters
}
Let’s look at the annotations used in the above entity:
- @Node marks the class as a node in the database. We have passed the label of the node as a value to the annotation.
- We need to mark the primary key of the node with the @Id annotation.
- If the name of the field in the entity is different from the name of the property in the database, we can use the @Property annotation to specify the name of the property in the database.
- The @Relationship annotation is used to specify the relationship between two nodes. We need to specify the type of relationship and the direction of the relationship. In this case, we have specified that the relationship is WRITTEN_BY, and the direction is OUTGOING.
Similarly, let’s create the Author entity:
@Node("Author")
public class Author {
@Id
private Long id;
private String name;
@Relationship(type = "WRITTEN_BY", direction = Relationship.Direction.INCOMING)
private List<Book> books;
// Constructor, getters and setters
}
As we can see, the Author entity is similar to the Book entity. The only difference is that the Author entity has a list of Book entities, and the direction of the relationship is INCOMING.
3.2. Neo4J Repositories
Next, we’ll create repositories that interact with the database, starting with the BookRepository:
@Repository
public interface BookRepository extends Neo4jRepository<Book, String> {
Book findOneByTitle(String title);
List<Book> findAllByYear(Integer year);
}
These methods are similar to the methods in a Spring Data JPA repository. The findOneByTitle() method will find a book by its title, and findAllByYear() will find all books published in a given year.
3.3. Custom Queries
We can also use the @Query annotation to write custom queries.
Let’s create a custom query in our AuthorRepository to find all books of an author written after a given year:
@Repository
public interface AuthorRepository extends Neo4jRepository<Author, Long> {
@Query("MATCH (b:Book)-[:WRITTEN_BY]->(a:Author) WHERE a.name = $name AND b.year > $year RETURN b")
List<Book> findBooksAfterYear(@Param("name") String name, @Param("year") Integer year);
}
To create the query, we use the MATCH keyword to match the nodes in the database. We specify the labels of the nodes and the relationship between them. Finally, we use the WHERE keyword to specify the conditions for the query.
To pass parameters to the query, we can use the $ symbol. We can use the @Param annotation to specify the parameter’s name in case the name in the query differs from the name of the parameter in the method.
4. Testing
Now that we have our entities and repositories, let’s test them by writing some integration tests for the repositories. We’ll use the Neo4j Harness dependency to run a Neo4j database in embedded mode for testing purposes.
4.1. Configuring Neo4j Harness
Configuring Neo4j Harness can be divided into three steps:
- Starting an embedded database
- Setting up the configuration to use the embedded database
- Cleaning up the database after the tests are run
Let’s set up the embedded server in a test class:
class BookAndAuthorRepositoryIntegrationTest {
<span class="pl-k">private</span> <span class="pl-k">static</span> <span class="pl-smi"><span class="pl-token">Neo4j</span></span> <span class="pl-s1"><span class="pl-token">newServer</span></span>;
@BeforeAll
static void initializeNeo4j() {
newServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer()
.withFixture("CREATE (b:Book {isbn: '978-0547928210', name: 'The Fellowship of the Ring', year: 1954})" +
"-[:WRITTEN_BY]->(a:Author {id: 1, name: 'J. R. R. Tolkien'})" +
"CREATE (b2:Book {isbn: '978-0547928203', name: 'The Two Towers', year: 1956})-[:WRITTEN_BY]->(a)")
.build();
}
@AfterAll
static void stopNeo4j() {
newServer.close();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("spring.neo4j.uri", newServer::boltURI);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", () -> "null");
}
}
In the above class, we have created a static @BeforeAll method to start the embedded database. We have also created a static @AfterAll method to stop the database after the tests are run.
Let’s take a look at server initialization in depth:
- We use the newInProcessBuilder() method to obtain a builder instance.
- The withDisabledServer() property creates a server without HTTP access, as we don’t need to enable outside traffic on our server.
- Optionally, we can use withFixture() to run an initial Cypher query when the server is created.
- Here, we provide the query to create two books and an author when the server starts. This helps in the initial data setup if required.
Next, we create a @DynamicPropertySource method to set the properties for the embedded database. We have set the URI, username, and password for the database. This overrides the properties provided in the application.properties file.
4.2. Testing the Repositories
Next, we can add the test methods to test the repositories:
@DataNeo4jTest
class BookAndAuthorRepositoryIntegrationTest {
// harness setup code
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
@Test
void givenBookExists_whenFindOneByTitle_thenBookIsReturned() {
Book book = bookRepository.findOneByTitle("The Fellowship of the Ring");
assertEquals("978-0547928210", book.getIsbn());
}
@Test
void givenOneBookExistsForYear_whenFindAllByYear_thenOneBookIsReturned() {
List<Book> books = bookRepository.findAllByYear(1954);
assertEquals(1, books.size());
}
@Test
void givenOneBookExistsAfterYear_whenFindBooksAfterYear_thenOneBookIsReturned() {
List<Book> books = authorRepository.findBooksAfterYear("J. R. R. Tolkien", 1955);
assertEquals(1, books.size());
}
}
The annotation @DataNeo4jTest is used to configure the test class for Spring Data Neo4j. It loads the Spring configuration and creates a test slice for Spring Data Neo4j beans.
Next, we have three test methods to test the findOneByTitle(), findAllByYear(), and findBooksAfterYear() methods.
5. Conclusion
In this article, we learned how to use Spring Data Neo4j to interact with a Neo4j database. We saw how to create entities and repositories and how to write custom queries. Finally, we wrote some integration tests for the repositories.
As always, the code for the examples can be found over on GitHub.