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


## はじめに
最近Kotlinアツいです！
[どこでもKotlin](https://m3-engineer.connpass.com/event/63057/)が定期的に開催されています！私も参加しています！
Kotlinに興味のある方は是非！

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

参考にさせていただいた記事
[Spring BootでJPAを使用した複数データベース接続](https://qiita.com/AHA_oretama/items/69db13e49030be2a742e)


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

## 環境構築

### 手順1 SPRING INITIALIZRでプロジェクトを作成
参考URL
[SPRING INITIALIZR](https://start.spring.io/)

![Kobito.CKGZxb.png](https://qiita-image-store.s3.amazonaws.com/0/136533/5011ec2d-cb0b-f799-1091-6eeef440ea57.png "Kobito.CKGZxb.png")


まずは `Group` と `Artifact` を任意の値に変更して `Generate Project`

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

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

参考 [Spring InitializrをIntelliJで使う](https://qiita.com/gosshys/items/fdf614d7301d01022a47)


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

![Kobito.I8LmSU.png](https://qiita-image-store.s3.amazonaws.com/0/136533/2959d638-8433-cf94-98f7-4caa4ab6ee08.png "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/config` に `application.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一覧](https://qiita.com/knife0125/items/bb095a85d1a5d3c8f706)

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

参考 [Dockerのインストール・起動手順](https://github.com/manzen/multiple-databases-demo/wiki)


## 実装

### ディレクトリ構成
![Kobito.Ox3ooC.png](https://qiita-image-store.s3.amazonaws.com/0/136533/06a7fe4e-efdc-849f-8575-2a74d36336ce.png "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が２つあるため、それぞれの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](https://qiita-image-store.s3.amazonaws.com/0/136533/88e7261b-c8a0-a774-864f-e6e242ffae86.png "Kobito.KnNi1u.png")

DB2にアクセス

![Kobito.QfLGiE.png](https://qiita-image-store.s3.amazonaws.com/0/136533/ab139b05-5091-9c31-2b09-43af12293436.png "Kobito.QfLGiE.png")



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


## サンプルコード
GitHubに公開しておりますので是非参考に！
https://github.com/manzen/multiple-databases-demo