Java
Kotlin
neo4j

Java Object-Graph Mappingライブラリを通してNeo4jを扱う

OGM込みでNeo4jを使っていきます
Kotlinを書きたかったのでときどきKotlinが混じっていますが大人しくJavaを使ったほうがいいと思います

Neo4jとは

Meet Neo4j: The graph database platform powering today's mission-critical enterprise applications, including artificial intelligence, fraud detection and recommendations.

driverライブラリだけで使うこともできる。
Maven Repository: org.neo4j.driver » neo4j-java-driver

今回はOGMとBoltを使う。
Maven Repository: org.neo4j » neo4j-ogm
Maven Repository: org.neo4j » neo4j-ogm-bolt-driver

DockerにNeo4j環境の用意がある。
library/neo4j - Docker Hub

docker run \
    --publish=7474:7474 --publish=7687:7687 \
    --volume=$HOME/neo4j/data:/data \
    --volume=$HOME/neo4j/logs:/logs \
    neo4j

上記ポートの場合、
ブラウザで http://localhost:7474/ を見ると色々と遊べる。
boltは bolt://localhost:7687/ から。
パスワードはブラウザでの初回ログイン時に決定するか、 --env NEO4J_AUTH=neo4j/<password> をつけて設定する。

とりあえず試す

Movie.java
@NodeEntity
public class Movie {

    @Id Long id;

    Movie(Long id) {
        this.id = id;
    }
}
Main.java
public static void main(String[] args) {
    Configuration configuration = new Configuration.Builder()
                                                   .uri("bolt://適当なuri")
                                                   .credentials(ユーザネーム, パスワード)
                                                   .build();

    SessionFactory sessionFactory = new SessionFactory(configuration, パッケージ);

    sessionFactory.openSession().save(new Movie(0L));
}

パッケージにはMovie.javaがあるところのパッケージ名を書く。

本格的に作り込む

オブジェクトのマッピングとデータベースへの接続ができるように用意しなければならない。

Entityを作る

ノードに相当するNodeEntityを作成する。エッジに個別の情報を持たせるにはRelationshipEntityを作成する。作ったEntityのあるパッケージを SessionFactory に渡してセッションを作ることで、OGMがEntityを認識する。
SessionFactory にはパッケージを可変長引数で受け取るコンストラクタがあるので、複数のパッケージに分けて作ってしまっても大丈夫。

作り間違えていると実行時にエラーが出る。

NodeEntityを作る

abstract class Entity {

    @Id @GeneratedValue
    private Long id;

    public Long getId() {
        return id;
    }
}

@NodeEntity
class Course extends Entity {
    String name;

    @Relationship(type= "SUBJECT_TAUGHT")
    Subject subject;

    @Relationship(type= "TEACHES_CLASS", direction=Relationship.INCOMING)
    Teacher teacher;

    @Relationship(type= "ENROLLED", direction=Relationship.INCOMING)
    Set<Enrollment> enrollments = new HashSet<>();
}

NodeEntityとするクラスには、

  • @NodeEntity をつける。
  • primary idが必要。primary idとするフィールドには @Id をつける。
    • 例のようにabstract classでつけてもOK。
    • @GeneratedValue をつけるとcreate時に勝手に値を決めてくれる。
  • エッジを張りたいときは相手のNodeEntityの型を持つフィールドを用意して @Relationship をつける。
    • 対多数の関係は Set<> にする。
    • type は相手ノードのフィールドと一致させる。
    • direction = Relationship.INCOMING でエッジの向きをこのノードに向けることができる。初期値では Relationship.OUTGOING になっている。

RelationshipEntityを作る

エッジに何か詳しく情報を持たせたい場合はクラスを作成し、その型のフィールドをノードに持たせる。

@RelationshipEntity(type = "ENROLLED")
class Enrollment {

    @StartNode
    Student student;

    @EndNode
    Course course;

    Date enrolledDate;

}
  • type はNodeEntityのフィールドで指定するものと合わせる。
  • 向きに合わせて @StartNode にあたるNodeEntityの型のフィールドと @EndNode にあたるノードのフィールドを作る。

Entityの使用例

// NodeEntityのインスタンスを作成
val course = Course(/*適当に作成*/)
val student = Student(/*適当に作成*/)

// RelationshipEntityのインスタンスを作成
val enrollment = Enrollment(student: student, course: course, /*適当に作成*/)

// NodeEntityにRelationshipの情報を与える
course.enrollments = hashSetOf(enrollment)
student.enrollments = hashSetOf(enrollment)

session.save(course)

どう頑張ってもRelationshipのフィールドはNodeEntityインスタンス作成時に埋まらないので、一旦nullかなにかで埋めておくしかなさそう。Kotlinにはちょっとつらい。

ついでにServiceも作ってみる

Session に用意されているメソッドを直接呼ぶだけでも十分使うことができるが、公式が Service を作っていたのでKotlin版に書き換えてみた。

Entity.kt
interface Entity {
    Long getId();
}

implements Entity して @Id Long id; としておけばOK。

Service.kt
interface Service<T> {
    fun findAll(): Iterable<T>
    fun find(id: Long): T?
    fun find(vararg filters: Filter): Iterable<T>
    fun delete(id: Long)
    fun createOrUpdate(`object`: T): T?
}
GenericService.kt
abstract class GenericService<T: Entity>(
    private val session: Session
): Service<T> {

    companion object {
        private const val DEPTH_LIST = 0
        private const val DEPTH_ENTITY = 1
    }

    override fun find(id: Long): T? {
        return session.load(getEntityType(), id, DEPTH_ENTITY)
    }

    override fun find(vararg filters: Filter): Iterable<T> {
        return session.loadAll(getEntityType(), Filters(filters.toList()))
    }

    override fun findAll(): Iterable<T> {
        return session.loadAll(getEntityType(), DEPTH_LIST)
    }

    override fun createOrUpdate(`object`: T): T? {
        session.save(`object`, DEPTH_ENTITY)
        return find(`object`.id)
    }

    override fun delete(id: Long) {
        session.delete(session.load(getEntityType(), id))
    }

    abstract fun getEntityType(): Class<T>
}
HogeService.kt
class HogeService(session: Session): GenericService<Hoge>(session) {

    override fun getEntityType(): Class<Hoge> {
        return Hoge::class.java
    }
}

テストする

Maven Repository: org.neo4j » neo4j-ogm-test
にある Neo4jRule が便利。

HogeTest.kt
class HogeTest {

    @Rule @JvmField
    val neo4jRule = Neo4jRule()

    private lateinit var session: Session

    @Before
    fun setUp() {
        val configuration = Configuration.Builder()
                                         .uri(neo4jRule.boltURI().toString())
                                         .build()

        val sessionFactory = SessionFactory(configuration, Movie::class.java.`package`.name)
        session = sessionFactory.openSession()
        session.purgeDatabase()
    }

    @Test
    fun hogeTest() {
        // ご自由に
    }
}

その他補足

参考文献

Using Neo4j from Java - Neo4j Graph Database Platform
Neo4j OGM - An Object Graph Mapping Library for Neo4j v3.1

Neo4j with Docker - Neo4j Graph Database Platform
2.5. Docker - Chapter 2. Installation

neo4j-examples/neo4j-ogm-university: Example Project for Neo4j OGM
neo4j-sdn-ogm-issue-report-template/ogm-3.0 at master · neo4j-examples/neo4j-sdn-ogm-issue-report-template

最初はKotlinだけで全部書こうとしたのだが、EntityのフィールドがnullになりがちであることとLombokの使用を優先したので、

  • EntityだけJavaで書き、
  • Session周りはKotlinで書き、
  • 別モジュールからKotlinでばりばり使う

になってしまった。悲しかった。