Kotlinで複数データベース接続

maven Maven
kotlin Kotlin
intellij-idea IntelliJ
jpa jpa
spring-boot spring-boot

はじめに

最近Kotlinアツいです!
どこでもKotlinが定期的に開催されています!私も参加しています!
Kotlinに興味のある方は是非!

業務でKolinを使ったREST APIを開発した際に複数DB接続をするのに結構ハマったので備忘録です!!
この記事が今後みんなの役に立ったらうれしい!!!

参考にさせていただいた記事
Spring BootでJPAを使用した複数データベース接続

環境

Spring boot
Kotlin
JPA
MySQL
DB2
Maven
Docker
IntelliJ IDEA
macOS Sierra

環境構築

手順1 SPRING INITIALIZRでプロジェクトを作成

参考URL
SPRING INITIALIZR

Kobito.CKGZxb.png

まずは GroupArtifact を任意の値に変更して Generate Project

※1 Dependenciesを設定することも可能なんですが、万能ではないのでここでは設定していません。

※2 IntelliJを利用している人はプロジェクトを新規作成するときにSpring Initializrを選択できるようになっています。

参考 Spring InitializrをIntelliJで使う

手順2 ダウンロードされたプロジェクトを解凍

Kobito.I8LmSU.png

zipファイルが/ダウンロードに保存されているので任意のディレクトリで解凍します。

手順3 IntelliJ IDEAでプロジェクトをOpen

IntelliJ IDEAを使っていない人はお好みのエディタでOpen

手順4 pom.xmlを修正

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.ibm.db2.jcc</groupId>
    <artifactId>db2jcc4</artifactId>
    <version>10.1</version>
</dependency>

dependencies に上記の dependecy を追加します。
※ db2jcc4はjarをダウンロードする必要があります。

手順5 application.ymlを作成

私は プロジェクト/src/main/resources/configapplication.yml を配置しました。

内容は以下

application.yml
spring:
  datasource:
    primary:
      url: jdbc:mysql://localhost/データベース名?useSSL=false
      username: root
      password:
      driverClassName: com.mysql.jdbc.Driver
    secondary:
      url: jdbc:db2://192.168.99.100:50000/データベース名
      username: db2inst1
      password: db2inst1
      driverClassName: com.ibm.db2.jcc.DB2Driver
  jpa:
      hibernate:
        ddl-auto: none

※ データベース名はご自身の環境に置き換えてください。

手順6 データベースを作成する

MySQLは割愛します。
参考 【MySQL, SQL】データベースを扱う基本SQL一覧

DB2はDockerに環境を構築します!

参考 Dockerのインストール・起動手順

実装

ディレクトリ構成

Kobito.Ox3ooC.png

Entity

まずはEntityです。
ポイントはデータベースごとにパッケージを分けることです。
パッケージに分けることで、Configurationの設定をシンプルにできます。

User.kt
package com.example.multipledatabasesdemo.domain.model.primary

import javax.persistence.*

@Entity
class User (
  @Id
  @GeneratedValue(strategy= GenerationType.IDENTITY)
  @Column
  var id: Int = 0,
  @Column
  var name: String = ""
)
ExternalUserInfo.kt
package com.example.multipledatabasesdemo.domain.model.secondary

import javax.persistence.*

@Entity(name = "external_user_info")
class ExternalUserInfo (
  @Id
  @GeneratedValue(strategy= GenerationType.IDENTITY)
  @Column
  var id: Int = 0,
  @Column
  var status: String = ""
)

Repository

Repositoryのポイントも同じくデータベースごとにパッケージを分けることです。

UserRepository.kt
package com.example.multipledatabasesdemo.domain.repository.primary

import com.example.multipledatabasesdemo.domain.model.primary.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface UserRepository: JpaRepository<User, Int>
ExternalUserInfoRepository.kt
package com.example.multipledatabasesdemo.domain.repository.secondary

import com.example.multipledatabasesdemo.domain.model.secondary.ExternalUserInfo
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface ExternalUserInfoRepository: JpaRepository<ExternalUserInfo, Int>

Service

正直今回の処理ではいらないんですが、お作法に従います。

UserService.kt
package com.example.multipledatabasesdemo.domain.service.primary

import com.example.multipledatabasesdemo.domain.model.primary.User
import com.example.multipledatabasesdemo.domain.repository.primary.UserRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

@Service
class UserService @Autowired constructor(private val userRepository: UserRepository) {

  fun findById(id: Int): User {
    return userRepository.findOne(id)
  }
}
ExternalUserInfoService.kt
package com.example.multipledatabasesdemo.domain.service.secondary

import com.example.multipledatabasesdemo.domain.model.secondary.ExternalUserInfo
import com.example.multipledatabasesdemo.domain.repository.secondary.ExternalUserInfoRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

@Service
class ExternalUserInfoService @Autowired constructor(private val externalUserInfoRepository: ExternalUserInfoRepository) {

  fun findById(id: Int): ExternalUserInfo {
    return externalUserInfoRepository.findOne(id)
  }
}

Configuration

PrimaryDataSourceConfiguration.kt
package com.example.multipledatabasesdemo.domain.configuration

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.orm.jpa.JpaTransactionManager
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import javax.persistence.EntityManagerFactory
import javax.sql.DataSource

@Configuration
@EnableJpaRepositories(
  basePackages = arrayOf("com.example.multipledatabasesdemo.domain.repository.primary"),
  entityManagerFactoryRef = "primaryEntityManager",
  transactionManagerRef = "primaryTransactionManager")
class PrimaryDataSourceConfiguration {

  @Bean
  @Primary
  @ConfigurationProperties(prefix = "spring.datasource.primary")
  fun primaryProperties(): DataSourceProperties {
    return DataSourceProperties()
  }

  @Bean
  @Primary
  @Autowired
  fun primaryDataSource(@Qualifier("primaryProperties") properties: DataSourceProperties): DataSource {
    return properties.initializeDataSourceBuilder().build()
  }

  @Bean
  @Primary
  @Autowired
  fun primaryEntityManager(builder: EntityManagerFactoryBuilder, @Qualifier("primaryDataSource") dataSource: DataSource): LocalContainerEntityManagerFactoryBean {
    return builder.dataSource(dataSource)
      .packages("com.example.multipledatabasesdemo.domain.model.primary")
      .persistenceUnit("primary")
      .build()
  }

  @Bean
  @Primary
  @Autowired
  fun primaryTransactionManager(@Qualifier("primaryEntityManager") primaryEntityManager: EntityManagerFactory): JpaTransactionManager {
    return JpaTransactionManager(primaryEntityManager)
  }
}
SecondaryDataSourceConfiguration.kt
package com.example.multipledatabasesdemo.domain.configuration

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.orm.jpa.JpaTransactionManager
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import javax.persistence.EntityManagerFactory
import javax.sql.DataSource

@Configuration
@EnableJpaRepositories(
  basePackages = arrayOf("com.example.multipledatabasesdemo.domain.repository.secondary"),
  entityManagerFactoryRef = "secondaryEntityManager",
  transactionManagerRef = "secondaryTransactionManager")
class SecondaryDataSourceConfiguration {

  @Bean
  @ConfigurationProperties(prefix = "spring.datasource.secondary")
  fun secondaryProperties(): DataSourceProperties {
    return DataSourceProperties()
  }

  @Bean
  @Autowired
  fun secondaryDataSource(@Qualifier("secondaryProperties") properties: DataSourceProperties): DataSource {
    return properties.initializeDataSourceBuilder().build()
  }

  @Bean
  @Autowired
  fun secondaryEntityManager(builder: EntityManagerFactoryBuilder, @Qualifier("secondaryDataSource") dataSource: DataSource): LocalContainerEntityManagerFactoryBean {
    return builder.dataSource(dataSource)
      .packages("com.example.multipledatabasesdemo.domain.model.secondary")
      .persistenceUnit("secondary")
      .build()
  }

  @Bean
  @Autowired
  fun secondaryTransactionManager(@Qualifier("secondaryEntityManager") secondaryEntityManager: EntityManagerFactory): JpaTransactionManager {
    return JpaTransactionManager(secondaryEntityManager)
  }
}

ちょこっとソース解説

はじめにで参考にした記事を元に記述しています。

DataSourcePropertiesの設定

DataSourcePropertiesはデフォルトでBean登録されており、デフォルトのDataSourcePropertiesを使用しないようにするために、必ずprimaryDB側に @primary を付与する必要があります。
@primary がないと、複数のBeanが登録されているためにエラーとなってしまいます。

@ConfigurationProperties は application.yml の以下の部分の設定を読み取ります。

application.yml
spring:
  datasource:
    *:
        以下の設定

DataSourceの設定

それぞれのDBに対するDataSourceを定義します。

同じくprimaryDB側に @primary をつけないとエラーになります。
@Autowired をつけて、引数に渡したDataSourcePropertiesをインジェクション(注入)しています。
対象クラスのBeanが複数あるので @Qualifier で明示しています。

LocalContainerEntityManagerFactoryBeanの設定

EntityManagerを生成するためのクラスになります。

EntityManagerFactoryBuilderはSpring Bootが提供しているBeanをそのままインジェクションします。
.packages(...)で対象となるEntityが格納されたパッケージを指定することで複数データベースに対応しています。この引数はクラスの配列も受け取れるので、パッケージを分けずに、Entityのクラスを直接指定することもできます。
.persistenceUnit(...)は永続性ユニットの名前で、複数データベースを定義するときは永続性ユニットを区別するために必須になります。

JpaTransactionManagerの設定

EntityManagerが2つあるため、それぞれのTransactionManagerを定義します。

JpaTransactionManagerを生成して、EntityManagerFactoryを設定すればOKです。

クラスの設定

対象のJpaRepositoryパッケージ、EntityManagerFactory,TransactionManagerがわかるように、@EnableJpaRepositories で指定します。

Controller

MultipleDatabasesDemoController.kt
package com.example.multipledatabasesdemo.controller

import com.example.multipledatabasesdemo.domain.model.primary.User
import com.example.multipledatabasesdemo.domain.model.secondary.ExternalUserInfo
import com.example.multipledatabasesdemo.domain.service.primary.UserService
import com.example.multipledatabasesdemo.domain.service.secondary.ExternalUserInfoService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/")
class MultipleDatabasesDemoController @Autowired constructor(private val userService: UserService, private val externalUserInfoService: ExternalUserInfoService) {

  @GetMapping("/user")
  fun getUser(@RequestParam(value = "id") id: Int): User = userService.findById(id)

  @GetMapping("/external-user-info")
  fun getExternalUserInfo(@RequestParam(value = "id") id: Int): ExternalUserInfo = externalUserInfoService.findById(id)
}

ちょこっとソース解説

@RestController はJsonやXML等を返すWebAPI用のコントローラで使用する。

@RequestMapping は、クラスとメソッドの両方に使用可能です。
クラスに使用した場合は、親パスに一致させることができます。

@Autowired を指定することによって、com.example.multipledatabasesdemo.domain.service に作成したコンポーネントをインジェクション(注入)します。

@RequestMapping のGETリクエスト用のアノテーションが@GetMappingです。

検証

プロジェクトのディレクトリに移動して mvn spring-boot:run を実行します!

サーバーが起動したらREST APIテストツールをなどを利用してテストします!

MySQLにアクセス

Kobito.KnNi1u.png

DB2にアクセス

Kobito.QfLGiE.png

ちゃんと接続できています!めでたし!

サンプルコード

GitHubに公開しておりますので是非参考に!
https://github.com/manzen/multiple-databases-demo