Spring Data Neo4j - Neo4jデータの追加と参照

Spring Data Neo4jは、グラフデータベースの一つであるNeo4jを扱うためのフレームワークである。

概要

Neo4jはグラフデータベースの一つであり、Spring DataはNeo4jを扱うためのインターフェースを提供するフレームワークを用意している。

Kotlin言語を使って、Spring Data Neo4jを活用する簡単なプロジェクトを作成してみる。

Neo4jサーバーの準備

Neo4jはオープンソースサーバーなので、無料でインストールしたり、Dockerで実行したりできる。

Neo4jをmacOS環境にインストールする方法は3つある。

自分の好みの方法でインストールすればよい。

Neo4jプロジェクトの作成

次のようにcurlコマンドを使って、Spring Bootの初期プロジェクトを作成する。

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 -

このコマンドを実行すると、Java 17、Spring Boot 3.0.6のプロジェクトが作成される。

ビルドスクリプト

Neo4jを動作させるためのライブラリを、ビルドスクリプトに次のように追加する。

/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")
}

依存ライブラリにSpring Data Neo4jライブラリ(spring-boot-starter-data-neo4j)が含まれていることを確認できる。

Entityの定義

Neo4jではエンティティとエンティティの関係が接続され、どちらの方向も同じように重要である。各人のレコードを保存するシステムをモデル化すると考えてみよう。ここでは、特定の人の同僚も追跡したい。たとえば、下のエンティティでは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()))
    }
}

クエリRepositoryの作成

Spring Data Neo4jは、Neo4jへデータを保存することに重点を置いている。ただし、検索クエリ派生機能を含め、Spring Data Commonsプロジェクトの機能も継承している。基本的にはNeo4jのクエリ言語を学ぶ必要はない。代わりに、いくつかのメソッドを作成すれば、クエリが自動で作成されるようにできる。

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>
}

PersonRepositoryインターフェースはNeo4jRepositoryを拡張し、操作したい型(Person)を結び付ける。このインターフェースには、標準CRUD(作成、読み取り、更新、削除)操作を含む多くの操作が用意されている。

Neo4jアクセス権限

Neo4j Community Editionへアクセスするには、資格情報の設定が必要である。 Springの基本設定ファイルであるapplication.propertiesapplication.ymlに変更し、次のように設定を追加する。

/src/main/resources/application.yml

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

ログ設定

ログを表示するために、依存関係としてkotlin-loggingライブラリを追加する。

dependencies {
	// .. 省略 ..

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

そして、Spring設定ファイルにログレベルを設定する。

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

この設定がないと、Cypher関連のWARNが発生する。(どうやらSpring Data Neo4jの更新が必要そうだ。)

アプリケーションクラスの作成

Spring Initializrは、アプリケーション用の簡単なクラスを生成してくれる。

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)
}

生成されたアプリケーションクラスを次のように変更する。

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
    • このアノテーションによって、Neo4j設定が有効になる。
  • 最初にすべてのデータを削除し、「Greg」、「Roy」、「Craing」を追加し、TEAMMATEworkWith(..)関数で追加していることを確認できる。
  • 結果として、各人のteammatesを表示し、逆に「Craig」をteammateとして指定している人も表示している。

アプリケーションの実行

実行してみると、結果がログに表示される。

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

Neo4j Browser(http://localhost:7474/browser/)に接続すると、Node Listを確認できる。

Accessing Data Neo4j

参考

上記のサンプルコードはGitHubで確認できる。