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

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

# Neo4jとは

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

driverライブラリだけで使うこともできる。
[Maven Repository: org.neo4j.driver » neo4j-java-driver](https://mvnrepository.com/artifact/org.neo4j.driver/neo4j-java-driver)

今回はOGMとBoltを使う。
[Maven Repository: org.neo4j » neo4j-ogm](https://mvnrepository.com/artifact/org.neo4j/neo4j-ogm)
[Maven Repository: org.neo4j » neo4j-ogm-bolt-driver](https://mvnrepository.com/artifact/org.neo4j/neo4j-ogm-bolt-driver)

DockerにNeo4j環境の用意がある。
[library/neo4j - Docker Hub](https://hub.docker.com/_/neo4j/)

```sh
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>` をつけて設定する。

# とりあえず試す

```java:Movie.java
@NodeEntity
public class Movie {

    @Id Long id;

    Movie(Long id) {
        this.id = id;
    }
}
```

```java: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を作る

```java
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を作る

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

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

    @StartNode
    Student student;

    @EndNode
    Course course;

    Date enrolledDate;

}
```

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

## Entityの使用例

```kotlin
// 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版に書き換えてみた。

```kotlin:Entity.kt
interface Entity {
    Long getId();
}
```

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

```kotlin: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?
}
```

```kotlin: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>
}
```

```kotlin: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](https://mvnrepository.com/artifact/org.neo4j/neo4j-ogm-test)
にある `Neo4jRule` が便利。

```kotlin: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() {
        // ご自由に
    }
}
```

# その他補足

* OGMの中のSLF4Jが「Failed to load class "org.slf4j.impl.StaticLoggerBinder".」と言ってくるので、お好みでライブラリを追加する。
  * 例えば [Maven Repository: ch.qos.logback » logback-classic](https://mvnrepository.com/artifact/ch.qos.logback/logback-classic)
* 実行時にようやくEntityに関するエラーが出るので、Entity専用のパッケージを用意してちょっとずつ作っていくとまだマシ。
* LombokでEntityに `@Builder` `@Builder.Default` `@Getter` `@Setter` を与えておくと楽かもしれない。
* 上手にOGMを使いたい人向け資料: [Chapter&nbsp;3.&nbsp;Reference - Neo4j OGM - An Object Graph Mapping Library for Neo4j v3.1](https://neo4j.com/docs/ogm-manual/current/reference/)

# 参考文献

[Using Neo4j from Java - Neo4j Graph Database Platform](https://neo4j.com/developer/java/)
[Neo4j OGM - An Object Graph Mapping Library for Neo4j v3.1](https://neo4j.com/docs/ogm-manual/current/)

[Neo4j with Docker - Neo4j Graph Database Platform](https://neo4j.com/developer/docker/)
[2.5.&nbsp;Docker - Chapter&nbsp;2.&nbsp;Installation](https://neo4j.com/docs/operations-manual/current/installation/docker/)

[neo4j-examples/neo4j-ogm-university: Example Project for Neo4j OGM](https://github.com/neo4j-examples/neo4j-ogm-university)
[neo4j-sdn-ogm-issue-report-template/ogm-3.0 at master · neo4j-examples/neo4j-sdn-ogm-issue-report-template](https://github.com/neo4j-examples/neo4j-sdn-ogm-issue-report-template/tree/master/ogm-3.0)

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

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

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