Help us understand the problem. What is going on with this article?

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-drivermysql: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

接続設定用のクラスをつくる

まずはソースコードから。

@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 パッケージに作成します。

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だと「:」表記になるのでわかりづらいですが)

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の操作や、メソッド名から直接クエリを導出する機能なども使えちゃいます。便利。

    @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です。かんたん!

@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

日付型(日時)の属性の扱い

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

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

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

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

    override fun toEntityAttribute(value: LocalDate): LocalDate {
        // 特に変換はしない
        return value
    }
}
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の設定回りだったのはひみつだよ!)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away