Codementor Events

Introduction to Spring Data Neo4j

Published Sep 10, 2020Last updated Oct 22, 2021

This introductory article talks about the integration of Spring Data with Neo4j database. We will discuss and learn how to develop a Spring Boot project to work with Neo4j Database. We will build an application that stores data in and retrieves data out from Neo4j.

Prerequisites

  • Familiarity with graph database
  • Familiarity with Spring framework
  • Installed Neo4j
  • A java based IDE
  • JDK 1.8 or later
  • Gradle 4+ or Maven 3.2+
  • and about 15 minutes of time

Quick Intro

Neo4j is an open-source NoSQL graph database. It offers a rich set of library and possibilities for Spring developers. If you are a Java developer, then modeling using Neo4j is seamless and straight forward. It has the flattest and fastest performance for read and write over any other solutions. Spring data Neo4j built on Spring framework and offers Object-Graph Mapping (OGM).
It enables full-fledged object mapping through POJO based classes and uses fundamental Spring concepts such as a template classes for core API usage and provides an annotation based object-mapping support.

Getting Started

Like other Spring-based projects, you can start from scratch by creating a maven or Gradle based project from your favorite IDE. Follow below step by step process or you can bypass the basic setup steps that are already familiar with.

Starting with Spring Initializr

To get started with Spring Data Neo4j,all we need is the Spring Data Neo4j dependency. You can create your project as Spring Boot or with Spring Initializr then your final list of dependencies wil look like this:
SpringInitializrDependencies.png
Note- The code sample and exapmles used in this article has been created through Spring Initializr.

Final POM and Dependencies

The below one is the final pom.xml file created when we choose Maven:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>io.codementor.neo4j</groupId>
  <artifactId>spring-data-neo4j</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>spring-data-neo4j</name>
  <description>Spring Data Neo4j project using Spring Boot</description>

  <properties>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-neo4j</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

Creating an Entity

Great! We can now start by creating our entity. Entities represent the objects that will be stored in our Neo4J database as Nodes. So, for an entity class, we use the annotation @NodeEntity. This makes them represent database objects, i.e., the nodes in the graph. Every entity has an identifier property. The identifier is annotated with @Id annotation and a @GeneratedValue annotation. The @GeneratedValue annotation signifies that the database will automatically generate the value of the identifier when the node is added to it. In addition to the identifier, the entity class can contain any number of other properties.

Let's create two simple entities to get started. We can create a Book entity and an Author Entity. For the book entity, we can have properties like title and language. On the other hand, for the Author Entity, we can have a name. We will also have a list of Books that every author as authored. These entities will be connected by an AUTHORED relationship. This is denoted by the @Relationship annotation. We also have another property called direction. The direction property is worth mentioning here. In Neo4j, all relationships are directional. According to directions, all relationships can be of any of these types - INCOMING, OUTCOMING, BOTH, NODE.

All relationships are unless otherwise specified are OUTGOING by default. This means that the current node is the source and the referred node is the target. Similarly, INCOMING means the referred node is the source and the current node is the target. BOTH means a bidirectional relationship and NODE means a unique relationship between two nodes where there is no importance of direction.

Author.java

package io.codementor.neo4j.entities;

import java.util.List;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity
public class Author {

  @Id
  @GeneratedValue
  private Long id;
  private String name;
  
  @Relationship(type = "AUTHORED", direction = Relationship.INCOMING)
  private List books;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public List getBooks() {
    return books;
  }

  public void setBooks(List books) {
    this.books = books;
  }
}

Book.java

package io.codementor.neo4j.entities;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;

@NodeEntity
public class Book {
 
  @Id
  @GeneratedValue
  private Long id;
  private String title;
  private String language;
  
  
  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getLanguage() {
    return language;
  }
  public void setLanguage(String language) {
    this.language = language;
  }
}

Setting up the Database and Datasource

We will now add Neo4j configurations to our application. We will set the DB URL, username, and password to our application.properties file. These properties will be used to connect to our database instance.

Adding Neo4j configurations

spring.data.neo4j.uri=bolt://localhost:11002/neo4j
spring.data.neo4j.username=neo4j
spring.data.neo4j.password=admin

Adding data to our database

Now, we will add some data to our Neo4j database. We can use Neo4j Desktop to write the queries in CYPHER.

CREATE (Invisible_Man:Book {title: 'Invisible Man', language: 'English'})
CREATE (Moby_Dick:Book {title: 'Moby Dick', language: 'English'})
CREATE (Hamlet:Book {title: 'Hamlet', language: 'English'})
CREATE (Ellison:Author {name: 'Ralph Ellison'})
CREATE (Shakespeare:Author {name: 'William Shakespeare'})
CREATE (Melville:Author {name: 'Herman Melville'})

CREATE
(Invisible_Man)-[:AUTHORED]->(Ellison),
(Moby_Dick)-[:AUTHORED]->(Melville),
(Hamlet)-[:AUTHORED]->(Shakespeare)
;

The above query creates the book and the author nodes and then adds relationships between them. As we can see, our DB objects are created so as to reflect our entities. We have created three Book objects - Invisible Man, Moby Dick, and Hamlet. They can be referred to by the variables Invisible_Man, Moby_Dick, and Hamlet respectively. Then we have added our authors. The authors are similarly referred to by the respective variables.

The third part of the query is where we create our relationships between the Book and the Author objects. As we can see, we have the "AUTHORED" relationship. It connects a book to an author. We can similarly connect many books to an author.
neo4jData.png

Creating Repository

Now that we have created the entities and set up the database, we will create our repository. We will create an Author Repository. It will be an interface that extends the Neo4jRepository. Neo4jRepository itself extends Spring Data's CrudRepository. As a result, all CRUD operations provided by Spring Data are inherently available. We can also add our data access methods along with CYPHER queries with the @Query annotation. As we will see, we have added a custom query method to get all Authors in our Neo4j DB along with the books they have published.

AuthorRepository.java

package io.codementor.neo4j.repositories;

import java.util.List;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;

import io.codementor.neo4j.entities.Author;

@Repository
public interface AuthorRepository extends Neo4jRepository<Author, Long> {

  @Query("MATCH (au:Author)<-[a:AUTHORED]-(b:Book) RETURN au,a,b")
  List<Author> getAllAuthors();
}

The query method returns all the authors and the list of their published books.

BookRepository.java

package io.codementor.neo4j.repositories;

import java.util.List;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;

import io.codementor.neo4j.entities.Book;

@Repository
public interface BookRepository extends Neo4jRepository<Book, Long> {

  Book findByTitle(String title);

  Book findByLanguage(String language);

        @Query("MATCH (b:Book) RETURN b")        
        List<Book> getAllBooks();

  @Query("MATCH (b:Book) WHERE b.title =~ ('(?i).*'+$str+'.*') RETURN b")
  List<Book> findByTitleContaining(String str);
}

In addition to the default CRUD methods provided by the Neo4jRepository interface, we have added four more methods. These methods can help us fetch a book by its title or language. We have a method to get all the books in the DB. Also, we have another method that uses a query to get all books containing a given string as a part of their title.

Creating Service

Now, we create our Author service class. The service class contains the getAllAuthors() which uses the getAllAuthors() method from our AuthorRepository to return a list of the authors.

AuthorService.java

package io.codementor.neo4j.services;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import io.codementor.neo4j.entities.Author;
import io.codementor.neo4j.repositories.AuthorRepository;

@Service
public class AuthorService {
  
  @Autowired
  private AuthorRepository authorRepository;

  public List getAll() {
    return authorRepository.getAllAuthors();
  }

  public Author saveAuthor(Author author) {
    return authorRepository.save(author);
  }

  public Optional<Author> getAuthorById(Long id) {
    return authorRepository.findById(id);
  }

  public void deleteAuthor(Long id) {
    authorRepository.deleteById(id);
  }
  public void deleteAllAuthors() {
    authorRepository.deleteAll();
  }

        public Long getCountOfAuthors() {
    return authorRepository.count();
  }

}

We have seen how we can use a custom query annotated method. Not only this we can also use the methods which are already provided by Spring Data’s Crudrepository. So we added the methods to save an author, retrieve an author. We also added methods for deleting an author and deleting all of them. At the end we also added a method for returning the count of authors in the database.

BookService.java

We shall similarly create a BookService to use the methods of the BookRepository interface. We shall have methods to add a book, delete a book, delete all books, find a book by its title, or language. Moreover, we have a method to retrieve all books and another to retrieve the number of books as well.

package io.codementor.neo4j.services;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import io.codementor.neo4j.entities.Book;
import io.codementor.neo4j.repositories.BookRepository;

@Service
public class BookService {

  @Autowired
  private BookRepository bookRepository;
  
  public Book saveBook(Book book) {
    return bookRepository.save(book);
  }
  
  public Book getBookByTitle(String title) {
    return bookRepository.findByTitle(title);
  }

        public List<Book> getBookByTitleContaining(String str) {
    return bookRepository.findByTitleContaining(str);
  }
  
  public Book getBooksByLanguage(String language) {
    return bookRepository.findByLanguage(language);
  }
  
  public List<Book> getAllBooks(){
    return bookRepository.getAllBooks();
  }
  
  public void deleteBook(Long id) {
    bookRepository.deleteById(id);
  }
  
  public void deleteAllBooks() {
    bookRepository.deleteAll();
  }
  
  public Long getCountOfBooks() {
    return bookRepository.count();
  }
}

Creating the controller

Let's add the AuthorController now. We will have a single endpoint that we will be using to fetch all the authors along with the list of their published books using the service method we created earlier. We will also have all the endpoints that we will use to perform all the CRUD Operations on our Neo4j DB.

AuthorController.java

package io.codementor.neo4j.controllers;

import java.util.Collection;
import java.util.NoSuchElementException;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
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.PostMapping;
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.RestController;

import io.codementor.neo4j.entities.Author;
import io.codementor.neo4j.services.AuthorService;

@RestController
@RequestMapping("/api/authors")
public class AuthorController {

  @Autowired
  private AuthorService authorService;

  @PostMapping("/add")
  public Author addAuthor(@RequestBody Author author) {
    return authorService.saveAuthor(author);
  }

  @GetMapping("/{id}")
  public Author getAuthorById(@PathVariable String id) {
    Optional authorOpt = authorService.getAuthorById(Long.parseLong(id));
    if (authorOpt.isPresent()) {
      return authorOpt.get();
    }
    throw new NoSuchElementException("No author found with given id.");
  }

  @GetMapping
  public Collection getAllAuthors() {
    return authorService.getAll();
  }

  @GetMapping("/count")
  public Long getCountofAuthors() {
    return authorService.getCountOfAuthors();
  }

  @DeleteMapping("/{id}")
  public String deleteAuthorById(@PathVariable String id) {
    authorService.deleteAuthor(Long.parseLong(id));
    return "Author deleted successfully";
  }

  @DeleteMapping
  public String deleteAllAuthors() {
    authorService.deleteAllAuthors();
    return "All Authors deleted successfully";
  }

}

As we can see, we have endpoints to add and remove authors. We also have endpoints to get an author by id or all of them. Moreover, we have an endpoint for getting the count of authors.

BookController.java

Similar to AuthorController, we will create our BookController. We will add the endpoints here to perform basic CRUD operations. In addition to that, we shall have endpoints to get a book by its title and also by a string which may be a part of its title.

package io.codementor.neo4j.controllers;

import java.util.Collection;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
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.PostMapping;
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.RestController;

import io.codementor.neo4j.entities.Book;
import io.codementor.neo4j.services.BookService;

@RestController
@RequestMapping("/api/books")
public class BookController {

  @Autowired
  private BookService bookService;

  @PostMapping("/add")
  public Book addBook(@RequestBody Book book) {
    return bookService.saveBook(book);
  }

  @GetMapping("/{title}")
  public Book getBookByTitle(@PathVariable String title) {
    return bookService.getBookByTitle(title);
  }
  
  @GetMapping("/title/{str}")
  public List getBookByTitleContaining(@PathVariable String str) {
    return bookService.getBookByTitleContaining(str);
  }

  @GetMapping
  public Collection getAllBooks() {
    return bookService.getAllBooks();
  }

  @GetMapping("/count")
  public Long getCountofBooks() {
    return bookService.getCountOfBooks();
  }

  @DeleteMapping("/{id}")
  public String deleteBookById(@PathVariable String id) {
    bookService.deleteBook(Long.parseLong(id));
    return "Book deleted successfully";
  }

  @DeleteMapping
  public String deleteAllBooks() {
    bookService.deleteAllBooks();
    return "All Books deleted successfully";
  }

}

Running the application

We have set up our application now. Let's run it as a Spring Boot Application. We can use Postman to test our endpoint. When we make a call to our "/authors" endpoint we can see that we get the expected list.
CRUDPostman.png
The result of the request should be similar to that given below.

[
    {
        "id": 15,
        "name": "Ralph Ellison",
        "books": [
            {
                "id": 12,
                "title": "Invisible Man",
                "language": "English"
            }
        ]
    },
    {
        "id": 17,
        "name": "Herman Melville",
        "books": [
            {
                "id": 13,
                "title": "Moby Dick",
                "language": "English"
            }
        ]
    },
    {
        "id": 16,
        "name": "William Shakespeare",
        "books": [
            {
                "id": 14,
                "title": "Hamlet",
                "language": "English"
            }
        ]
    }
]

Similarly other endpoint can be invoked,

Create an Author and a Book

Let us now add an author along with his book. The request will look as given below.

{
    "name": "Leo Tolstoy",
    "books": [
        {
            "title": "War and Peace",
            "language": "English"
        }
    ]
}

When we make the request using Postman if the request is successful, it will give us the newly created node in a JSON format as given below.

{
    "id": 8,
    "name": "Leo Tolstoy",
    "books": [
        {
            "id": 7,
            "title": "War and Peace",
            "language": "English"
        }
    ]
}

We can see that Ids have also been assigned to our objects by the Database.

AddAuthorAndBook.png

Fetching Data using Attributes

1. Fetching Book using the title: For this, we hit the “/books/{title}” endpoint that we had created earlier. Let’s use the title “Hamlet” and see what we get.
fetchingBookUsingTitle.png
As we can see, the API returns the details of the book with the title “Hamlet”.
2. Fetching a book using a part of its title: For this, we hit the “/books/title/{str}” endpoint. Let’s use the string “Peace” and see what we get.
fetchingBookUsingPartOfTitle.png
We see that we get the details of the book War and Peace.

Delete Data

Let’s delete an author and a book.
deleteAnAuthoraAndABook.png

Return the count of authors

We can get the count of authors from our DB using endpoints.
CountOfAuthors.png

Conclusion

In this article, we have seen how we can use Spring Data Neo4j to perform operations on a Neo4j database. Likewise, we can perform all CRUD operations and also execute our own queries as well. We have also explored a bit of the Cypher query language and how we can use it.

Discover and read more posts from Asad Ali
get started