Spring Data Neo4jとSpring Data JPAを1つのWebアプリケーションで使う＋おまけ



## はじめに
半年ほど前にNeo4jを使うアプリケーションをSpringBoot（Kotlin）でつくりました。
javaでググっても（当時は）あまり事例を見かけなかったので、だれかの参考になればいいなと思って記事にしてみます。


## やりたい（やりたかった）こと

- 1つのWebアプリケーションから、MySQLとNeo4j両方にアクセスしたい
- （時間がなかったので）できれば使い慣れた言語とかフレームワークだとうれしい

ということでちらりとググってみたところ `Spring Data Neo4j` とかいうプロジェクトがあるじゃないですかー！ということで、今回はこちらを使ってみました。
現時点（2019年11月現在）だと、最新版は 5.2.2 になるようです。ドキュメントはこちら。
https://docs.spring.io/spring-data/neo4j/docs/5.2.2.RELEASE/reference/html/#reference

### 注意
Neo4jのセットアップ方法や、Spring Bootについての突っ込んだ話はしていません。ある程度Spring Bootを触ったことのある方向けの記事になっています。

## 実装したときのバージョン
- Spring Boot 2.1.4
    - Spring Data JPAとNeo4jは上記に対応するバージョン

## プロジェクトの作成
1. https://start.spring.io/ にて`Dependencies`で下記を追加し`Generate`をぽち
    - `Spring Data JPA`（MySQL用）
    - `Spring Data Neo4j`

2. ダウンロード後、DBのドライバがそれぞれ必要なので、依存関係に `org.neo4j:neo4j-ogm-embedded-driver` と `mysql:mysql-connector-java` を追加


## DBに接続する
色々試行錯誤した結果、以下のようなやり方で接続することができました。
作り終えてから気が付いたのですが、なんとなく `Spring Data JPA` を使って複数のデータソースに接続するときと考え方は似ているんじゃないかと思います。

### repositoryとentityのパッケージを決める
それぞれのデータソースごとにrepositoryとentityを格納するパッケージを決めます。
お好みでお好きな感じにどうぞ。今回はこんな感じでわけてみました。

    // Neo4j用
    - xxx.gateway.neo4j.repository
    - xxx.gateway.neo4j.entity
    // MySQL用
    - xxx.geteway.mysql.repository
    - xxx.geteway.mysql.entity

### 接続設定用のクラスをつくる
まずはソースコードから。

```kotlin

@Configuration
@EnableJpaRepositories(basePackages = [ "xxx.gateway.mysql.repository" ])
@EnableNeo4jRepositories(basePackages = ["xxx.gateway.neo4j.repository"], transactionManagerRef = "neo4jTransactionManager")
@EnableTransactionManagement
class DataSourceConfiguration {

    @Value("\${spring.data.neo4j.uri}")
    private lateinit var neo4jUri: String

    @Value("\${spring.data.neo4j.username}")
    private lateinit var neo4jUserName: String

    @Value("\${spring.data.neo4j.password}")
    private lateinit var neo4jPassword: String

    @Value("\${spring.data.neo4j.connection.pool.size:#{null}}")
    private lateinit var connectionPoolSize: Optional<Int>

    @Bean
    fun sessionFactory(): SessionFactory {
        return SessionFactory(configuration(), "xxx.gateway.neo4j.entity")
    }

    @Bean
    fun configuration(): org.neo4j.ogm.config.Configuration {
        return org.neo4j.ogm.config.Configuration.Builder()
                .uri(neo4jUri)
                .credentials(neo4jUserName, neo4jPassword).build()
    }

    @Bean("neo4jTransactionManager")
    fun neo4jTransactionManager(): Neo4jTransactionManager {
        return Neo4jTransactionManager(sessionFactory())
    }

    @Bean("mysqlTransactionManager")
    fun mysqlTransactionManager(entityManagerFactory: LocalContainerEntityManagerFactoryBean): JpaTransactionManager {
        return JpaTransactionManager(entityManagerFactory.`object`!!)
    }

    @Primary
    @Bean("transactionManager")
    fun transactionManager(neo4jTransactionManager: Neo4jTransactionManager,
                           mysqlTransactionManager: JpaTransactionManager): PlatformTransactionManager {
        return ChainedTransactionManager(
                mysqlTransactionManager,
                neo4jTransactionManager
        )
    }
}
```

構成としては、JPA（MySQL）用の`TransactionManager`とNeo4j用の`TransactionManager`を2つを用意し、Springのコンテナから最優先で参照される（`@Primary`）ときに`ChainedTransactionManager` として2つとも返す　という作りになっています。
で、repositoryで`TransactionManager` をバインドするときにどちらのmanagerが採用されるかを決めるのが、上記で決めたパッケージ、という構成になるようです。

この状態で、それぞれのrepository、entityパッケージの配下にNeo4j用、JPA（MySQL）用のrepositoryクラスとentityクラスを実装します。（JPA用の実装については先達たちがたくさん記事を書いてくださっているので割愛）

### Neo4j用のEntityをつくる
とはいえ、Neo4j側もJPAとそんなに違いはありません。
まずはEntityから。最初に決めた `xxx.gateway.neo4j.entity` パッケージに作成します。

```Kotlin
package xxx.gateway.neo4j.entity

import org.neo4j.ogm.annotation.*

@NodeEntity
data class Person (

        @Id
        @GeneratedValue
        var id: Long? = null,

        var name: String? = null,

        @Relationship(type = "parent")
        var parents: MutableList<Person> = mutableListOf()
)
```

`@Entity` ではなく、 `@NodeEntity` となっているところがポイント。
`@Id` などもNeo4jで利用できますが、クラス名が同じでも`Spring Data JPA`側の実装だったりするので気を付けましょう。
特にIDEで自動補完したりしていると間違えやすいです。`Neo4j` の方は`org.neo4j.ogm.annotation` パッケージの方を参照するのだと頭の片隅に置いておくとよいかもです。

### Neo4j用のrepository（interface）をつくる

こちらも最初に決めたパッケージの `xxx.gateway.neo4j.repository` 以下に作成します。
基本的に、`Neo4jRepository` をextendsするだけです。（Kotlinだと「:」表記になるのでわかりづらいですが）

```Kotlin
package xxx.gateway.neo4j.repository

import xxx.gateway.neo4j.entity.Person
import org.springframework.data.neo4j.repository.Neo4jRepository

interface PersonRepository: Neo4jRepository<Person, Long> {
    // name属性をキーにしてNodeを探す
    fun findByName(name: String): List<Person>
}
```

### つかってみる

これで、`Spring Data Neo4j`でも、`Spring Data JPA`と同じように`save()`などの基本的なEntityの操作や、メソッド名から直接クエリを導出する機能なども使えちゃいます。便利。

```Kotlin
    @Autowired
    private lateinit var personRepository: PersonRepository

    fun findPerson(name: String): List<PersonInfo> {
        // nameで検索したあと、PersonInfoというクラスのインスタンスのリストに変換して返す
        return personRepository.findByName(name).map{ PersonInfo(it) }
    }

```

## おまけ
以下は `Spring Data Neo4j`を使うときに困ったことと解決方法のおまけです。ご参考まで。

### OGMを直接参照する

NodeやRelationshipを直接いじいじしたり、1種類のNodeだけをリストで取ってくるという場合は上記の例で十分ですが、業務的にはCypherをある程度自分で書くことになることが多いんじゃないかと思います（少なくともわたしは書きたかった）。
その場合は `org.neo4j.ogm.session.Session` をDIしてあげればOKです。かんたん！

```Kotlin
@Repository
class SomeRepositoryImpl: SomeRepository {
    @Autowired
    private lateinit var session: Session

    override fun getSomeCustomData(params: SomeParams) {
        val cypher = 'MATCH (n: SomethingNode)...'
        val paramsMap = emptyMap<String, Any>()
        paramsMap.add('aaa', 'bbb');
        val result = session.query(cypher, paramsMap, true)

        val rows = result.queryResults()
    }
}
```

ただし、この実行結果である `rows` はうまくEntityとして取得できるパターンと、そうでないパターンがあるようです。
`queryForObject`メソッドだと指定したクラスのインスタンスにマッピングした状態で返してくれるのかな？と思っていたのですが、試したCypherでは1階層目だけEntityになり、2階層目以降はMapで返るという不思議パターンに…。

最終的に、Mapを地道に自前でマッピングしてオブジェクトに突っ込む処理をごりごり書きました。この辺がもうちょっと便利になっているといいなーと思うのですが…。一応最新版の関連しそうなドキュメントのURLも貼っておきます。
https://docs.spring.io/spring-data/neo4j/docs/5.2.2.RELEASE/reference/html/#reference:session:loading-entities:cypher-queries

#### 日付型（日時）の属性の扱い

日付型の属性についても記述しておきます。

##### 試行錯誤
- 単純にEntityの属性を `java.util.Date` にしてみたら、登録はできたけど取得時に例外が発生した
- `LocalDateTime` でやっても似たようなもの
- ドキュメントでは対応しているようなことを書いてあったのだが、どうやら（当時は）まだ未対応だった模様
    - https://community.neo4j.com/t/how-to-use-neo4j-native-types-with-sdn-and-ogm/4442/3

##### 解決方法
1. 「なにも変換をしないConverter」を定義し、Entityの対象の属性にコンバータとして指定
2. データの取得時は（SpringDataNeo4jで）Jacksonを使っているため、そちらにも同じように「なにも変換をしないConverter」を指定してあげる（SpringのRestサービスでよくやるJacksonの日付のフォーマット設定と同じ）

2は色んなところでサンプルがあるので省略しますが、1の内容だけ書くとこんな感じです。

```kotlin:何も変換をしないConverter
class NoOpLocalDateConversion : AttributeConverter<LocalDate, LocalDate> {
    override fun toGraphProperty(value: LocalDate): LocalDate {
        // 特に変換はしない
        return value
    }

    override fun toEntityAttribute(value: LocalDate): LocalDate {
        // 特に変換はしない
        return value
    }
}
```


```kotlin:Entity
        @Convert(NoOpLocalDateTimeConversion::class)  // ←これ
        @get:JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        var someDateTime: LocalDateTime = LocalDateTime.now()
```

ちなみにkotlinで `@JsonFormat(...)` は @get:JsonFormat(...) と指定してあげる必要があります（ハマった）。

これで日時の属性も正常に編集・取得が行えるようになりました。

## まとめ
実際に開発していたときは接続まわりがうまく動くようになるまでにだいぶ時間がかかりました。
ただしいちど動いてしまえばそのあとの機能の作りこみがぐっと楽になるので、苦労しているどこかの誰かに届くといいな。
~~（でも一番苦労したのは同アプリにつっこんだ`Spring Boot Sequrity`の設定回りだったのはひみつだよ！）~~