Spring Data Neo4j - Add and Query Neo4j Data

Spring Data Neo4j is a framework for working with Neo4j, one of the graph databases.

Overview

Neo4j is one of the graph databases, and Spring Data provides a framework that offers an interface for working with Neo4j.

This article creates a simple project that uses Spring Data Neo4j with Kotlin.

Preparing the Neo4j Server

Neo4j is an open source server, so you can install it for free or run it with Docker.

There are three ways to install Neo4j in a macOS environment.

Use whichever method you prefer.

Creating the Neo4j Project

Create the initial Spring Boot project with the following curl command.

curl https://start.spring.io/starter.tgz \
-d bootVersion=3.0.6 \
-d dependencies=data-neo4j \
-d baseDir=spring-data-neo4j \
-d groupId=com.devkuma \
-d artifactId=spring-data-neo4j \
-d packageName=com.devkuma.neo4j \
-d applicationName=Neo4jApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=17 \
-d type=gradle-project-kotlin | tar -xzvf -

Running this command creates a project with Java 17 and Spring Boot 3.0.6.

Build Script

Add the libraries required to run Neo4j to the build script as follows.

/build.gradle.kts

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-data-neo4j")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

You can see that the Spring Data Neo4j library (spring-boot-starter-data-neo4j) is included in the dependencies.

Defining the Entity

Neo4j connects entities and relationships between entities, and both directions are equally important. Suppose you are modeling a system that stores records for each person. In this case, you also want to track a specific person’s colleagues. In the entity below, that corresponds to teammates.

src/main/java/com/devkuma/neo4j/entity/Person.java

package com.devkuma.neo4j.entity

import org.springframework.data.neo4j.core.schema.GeneratedValue
import org.springframework.data.neo4j.core.schema.Id
import org.springframework.data.neo4j.core.schema.Node
import org.springframework.data.neo4j.core.schema.Relationship
import java.util.*
import java.util.stream.Collectors

@Node
class Person {

    @Id
    @GeneratedValue
    var id: Long = 0
    var name: String = ""

    @Relationship(type = "TEAMMATE")
    var teammates: MutableSet<Person>? = null

    fun worksWith(person: Person) {
        if (teammates == null) {
            teammates = HashSet()
        }
        teammates!!.add(person)
    }

    override fun toString(): String {
        return ("$name's teammates => " +
                Optional
                    .ofNullable(teammates)
                    .orElse(mutableSetOf()).stream()
                    .map { obj: Person -> obj.name }
                    .collect(Collectors.toList()))
    }
}

Creating the Query Repository

Spring Data Neo4j focuses on storing data in Neo4j. However, it also inherits features from the Spring Data Commons project, including derived query methods. Basically, you do not need to learn Neo4j’s query language. Instead, you can write a few methods so that queries are generated automatically.

src/main/java/com/devkuma/neo4j/repository/PersonRepository.java

package com.devkuma.neo4j.repository

import com.devkuma.neo4j.entity.Person

import org.springframework.data.neo4j.repository.Neo4jRepository

interface PersonRepository : Neo4jRepository<Person, Long> {
    fun findByName(name: String): Person?
    fun findByTeammatesName(name: String): List<Person>
}

The PersonRepository interface extends Neo4jRepository and connects the type you want to work with, Person. This interface comes with many operations, including standard CRUD operations: create, read, update, and delete.

Neo4j Access Credentials

To access Neo4j Community Edition, credential settings are required. Change Spring’s default configuration file from application.properties to application.yml and add the settings below.

/src/main/resources/application.yml

spring:
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: secret123

Logging Configuration

Add the kotlin-logging library as a dependency to display logs.

dependencies {
	// .. omitted ..

	implementation("io.github.microutils:kotlin-logging:3.0.5")
}

Then set the log level in the Spring configuration file.

logging:
  level:
    org.springframework.data.neo4j.cypher: ERROR

Without this setting, warnings related to Cypher occur. (It seems Spring Data Neo4j may need an update.)

Creating the Application Class

Spring Initializr creates a simple class for the application.

src/main/java/com/devkuma/neo4j/entity/Person.java

package com.devkuma.neo4j

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Neo4Ajpplication

fun main(args: Array<String>) {
	runApplication<Neo4Ajpplication>(*args)
}

Change the generated application class as follows.

package com.devkuma.neo4j

import com.devkuma.neo4j.entity.Person
import com.devkuma.neo4j.repository.PersonRepository
import mu.KotlinLogging
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories
import kotlin.system.exitProcess

private val log = KotlinLogging.logger {}

@SpringBootApplication
@EnableNeo4jRepositories
class Neo4jApplication {

    @Bean
    fun demo(@Autowired personRepository: PersonRepository): CommandLineRunner {
        return CommandLineRunner {

            personRepository.deleteAll()

            var greg = Person()
            greg.name = "Greg"
            var roy = Person()
            roy.name = "Roy"
            val craig = Person()
            craig.name = "Craig"
            val team: List<Person> = listOf(greg, roy, craig)

            log.info("Before linking up with Neo4j...")
            team.stream().forEach { person: Person ->
                log.info("\t${person}")
            }

            personRepository.save(greg)
            personRepository.save(roy)
            personRepository.save(craig)

            greg = personRepository.findByName(greg.name)!!
            greg.worksWith(roy)
            greg.worksWith(craig)
            personRepository.save(greg)

            roy = personRepository.findByName(roy.name)!!
            roy.worksWith(craig)
            personRepository.save(roy)

            log.info("Lookup each person by name...")
            team.stream().forEach { person: Person ->
                log.info("\t${personRepository.findByName(person.name)}")
            }
            val teammates = personRepository.findByTeammatesName(craig.name)
            log.info("The following have ${craig.name} as a teammate...")
            teammates.stream()
                .forEach { person: Person -> log.info("\t${person.name}") }
        }
    }
}

fun main(args: Array<String>) {
    runApplication<Neo4jApplication>(*args)
    exitProcess(0)
}
  • @EnableNeo4jRepositories
    • This annotation enables the Neo4j configuration.
  • At the beginning, all data is deleted. Then “Greg”, “Roy”, and “Craing” are inserted, and TEAMMATE relationships are added with the workWith(..) function.
  • The result displays each person’s teammates and, conversely, the people who have “Craig” as a teammate.

Running the Application

Run the application, and you can see the result in the logs.

2023-05-13T01:53:34.640+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : Before linking up with Neo4j...
2023-05-13T01:53:34.640+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Greg's teammates => []
2023-05-13T01:53:34.640+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Roy's teammates => []
2023-05-13T01:53:34.640+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Craig's teammates => []
2023-05-13T01:53:34.852+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : Lookup each person by name...
2023-05-13T01:53:34.865+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Greg's teammates => [Craig, Roy]
2023-05-13T01:53:34.874+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Roy's teammates => [Craig]
2023-05-13T01:53:34.881+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Craig's teammates => []
2023-05-13T01:53:34.893+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : The following have Craig as a teammate...
2023-05-13T01:53:34.894+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Greg
2023-05-13T01:53:34.894+09:00  INFO 36427 --- [           main] com.devkuma.neo4j.Neo4jApplication       : 	Roy

Open Neo4j Browser (http://localhost:7474/browser/) to check the node list.

Accessing Data Neo4j

References

The example code above is available on GitHub.