技術選定とversion

Spring Bootで複数のDataSourceを扱う方法は、使用するORMによってだけでなく、Springのconfigurationをapplication.properties/application.ymlで行うかJavaConfigで行うかによっても変わってくる。
ここではMyBatisとapplication.propertiesでの方法を書く。
ちなみにMyBatisを選んだのは直接SQLを書きたいからで、application.propertiesを選んだのは(ymlでもいいが)JavaConfigより設定が一か所にまとまり、さらに.javaファイルよりも「このファイルは設定である」と一目でわかるのが簡潔に思うから。

検証version
・Spring Boot 1.3.3
・MyBatis 3.3.1
・MyBatis-Spring 1.2.4

今回の設定における前提

  • DBは二つ接続したい。

    一つは自システム専用のDBで、もう一つは他システムのDB。
    それぞれownとotherというDB名とする。

  • ownにはCustomerテーブル、otherにはOrderテーブルがある。

pom.xml

Spring BootにMyBatisを組み込むためのライブラリの設定。
pom.xml

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.0.1</version>
</dependency>

mybatis-spring-boot-starterが提供されているので、これだけでMyBatis本体とMyBatis-Springが入ってくれる。

ディレクトリ構造

DB接続部分のディレクトリ構造の抜粋

src/main/java/jp/co/sample
├ domain
│   ├ Customer.java
│   └ Order.java
├ mapper
│   ├ handler
│   ├ other
│   │    └ OrderMapper.java
│   └ own
│        └ CustomerMapper.java
├ service
│   ├ CustomerService.java
│   └ OrderService.java
├ system/config/db
│        ├ DataSource.java
│        ├ OtherDataSourceConfiguration.java
│        ├ OtherDBProperties.java
│        ├ OwnDataSourceConfiguration.java
│        └ OwnDBProperties.java
├ web
└ Application.java

src/main/resources
├ jp/co/sample
│        ├ other
│        │    └ OrderMapper.xml
│        └ own
│             └ CustomerMapper.xml
├ application.properties
├ data_other.sql
├ data_own.sql
├ mybatis-config.xml
├ schema_other.sql
└ schema_own.sql

ディレクトリごとの役割解説

ディレクトリ 役割
domain 検索結果に相当するJavaBeansをおく。
mapper 直下にDBごとのディレクトリを作成し、SQLを記載したxmlを呼び出すためのinterfaceをおく。
ディレクトリを分けるということ以外、Javaソース上でDBの違いは意識しなくて済む。
service mapper interfaceを呼び出したり、ビジネスロジックを書く。DBの違いはここでは意識しなくて済む。
system/config/db DBに関係する設定。パラメータ設定をapplication.propertiesで行えるようにするためのコードが書かれている。

ソースコード解説

ディレクトリごとの役割解説で記載した通り、DBごとの違いが現れるのはsystem/config/dbのソースだけであり、あとは単一DBで使用するときと変わらない。
※ただし、Application.javaにはAnnotationの変更が入る。
まずはsystem/config/dbの各クラスについて全文掲載していく。

DataSource.java

import lombok.Getter;
import lombok.Setter;

public class DataSource extends org.apache.tomcat.jdbc.pool.DataSource {

    /**
     * Schema (DDL) script resource reference.
     */
    @Getter @Setter
    private String schema;

    /**
     * Data (DML) script resource reference.
     */
    @Getter @Setter
    private String data;

}

Tomcatのorg.apache.tomcat.jdbc.pool.DataSourceを独自拡張している。
Spring Bootでは/src/main/resourcesにschema.sqlとdata.sqlを配置することで、アプリの起動時に自動でSQLを実行してくれる機能がある。また以下のようにファイル名を指定することで、デフォルトのファイル名から変更することもできる。

spring.datasource.schema=classpath:schema_for_development.sql
spring.datasource.data=classpath:data_for_development.sql

しかしorg.apache.tomcat.jdbc.pool.DataSourceには同パラメータがないため、schemaとdataというフィールドを持つように拡張している。

OwnDBProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

import lombok.Getter;
import lombok.Setter;

@ConfigurationProperties("spring.own")
public class OwnDBProperties {

    @Getter @Setter
    @NestedConfigurationProperty
    private DataSource datasource;
}

ownDB用にapplication.properiesでspring.ownをkeyに各種設定をできるようにしている。
@ConfigurationPropertiesをクラスに付与すると、@ConfigurationPropertiesの引数で指定した値がapplication.properiesでkeyとして使える。
DataSourceクラスで定義されているものを、各種keyとして使用したいので、@NestedConfigurationPropertyをDataSourceに付与することで、spring.own.datasourceというprefixで各種項目にアクセスできるようになる。

OtherDBProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

import lombok.Getter;
import lombok.Setter;

@ConfigurationProperties("spring.other")
public class OtherProperties {

    @Getter @Setter
    @NestedConfigurationProperty
    private DataSource datasource;
}

spring.otherをapplication.propertiesで使用できるようにする。基本はOwnDBPropertiesと同じ。

OwnDataSourceConfiguration.java

import java.util.Optional;

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;

@Configuration
@MapperScan(basePackages = "jp.co.sample.mapper.own", sqlSessionTemplateRef = "ownSqlSessionTemplate")
public class OwnDataSourceConfiguration {

    @Autowired
    private OwnDBProperties ownDBProperties;

    @Autowired
    private ApplicationContext context;

    @Bean(name = { "dataSource", "ownDataSource" })
    @ConfigurationProperties(prefix = "spring.own.datasource")
    @Primary
    public DataSource dataSource() {
        return ownDBProperties.getDatasource();
    }

    @Bean(name = "ownSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("ownDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // MyBatis のコンフィグレーションファイル
        bean.setConfigLocation(context.getResource(context.getEnvironment().getProperty("mybatis.config")));
        return new SqlSessionTemplate(bean.getObject());
    }

    @Bean(name = "ownDataSourceInitializer")
    public DataSourceInitializer dataSourceInitializer(@Qualifier("ownDataSource") DataSource dataSource) {
        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(dataSource);
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
// schema,dataをpropertiesファイルで指定しない場合、Location must not be nullが出るため、nullの場合は処理を行わないように修正(2016/06/28)
//        databasePopulator.addScripts(
//                context.getResource(ownDBProperties.getDatasource().getSchema()),
//                context.getResource(ownDBProperties.getDatasource().getData()));
        Optional.ofNullable(ownDBProperties.getDatasource().getSchema())
            .map(schema -> context.getResource(schema))
            .ifPresent(resource -> databasePopulator.addScript(resource));
        Optional.ofNullable(ownDBProperties.getDatasource().getData())
            .map(data -> context.getResource(data))
            .ifPresent(resource -> databasePopulator.addScript(resource));

        dataSourceInitializer.setDatabasePopulator(databasePopulator);
        dataSourceInitializer.setEnabled(true);

        return dataSourceInitializer;
    }

}

ownDB用に以下のクラスを構築するJavaConfig。

  • DataSource
  • MyBatisが使用するSqlSessionTemplate
  • Spring Boot起動時のSQLファイル自動実行用のDataSourceInitializer

JavaConfigなので、@Configurationをクラスに付与する。
また@MapperScanで、ownDBにSQLアクセスするmapper interface群が格納されているpackageの指定と、SqlSessionTemplateがどのインスタンスを参照すればいいかを指定する。

OtherDataSourceConfiguration.java

import java.util.Optional;

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;

@Configuration
@MapperScan(basePackages = "jp.co.sample.mapper.other", sqlSessionTemplateRef = "otherSqlSessionTemplate")
public class OtherDataSourceConfiguration {

    @Autowired
    private OtherProperties otherProperties;

    @Autowired
    private ApplicationContext context;

    @Bean(name = "otherDataSource")
    @ConfigurationProperties(prefix = "spring.other.datasource")
    public DataSource dataSource() {
        return otherProperties.getDatasource();
    }

    @Bean(name = "otherSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("otherDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // MyBatis のコンフィグレーションファイル
        bean.setConfigLocation(context.getResource(context.getEnvironment().getProperty("mybatis.config")));
        return new SqlSessionTemplate(bean.getObject());
    }

    @Bean(name = "otherDataSourceInitializer")
    public DataSourceInitializer dataSourceInitializer(@Qualifier("otherDataSource") DataSource dataSource) {
        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(dataSource);
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
// schema,dataをpropertiesファイルで指定しない場合、Location must not be nullが出るため、nullの場合は処理を行わないように修正(2016/06/28)
//        databasePopulator.addScripts(
//                context.getResource(otherProperties.getDatasource().getSchema()),
//                context.getResource(otherProperties.getDatasource().getData()));
        Optional.ofNullable(otherProperties.getDatasource().getSchema())
            .map(schema -> context.getResource(schema))
            .ifPresent(resource -> databasePopulator.addScript(resource));
        Optional.ofNullable(otherProperties.getDatasource().getData())
            .map(data -> context.getResource(data))
            .ifPresent(resource -> databasePopulator.addScript(resource));

        dataSourceInitializer.setDatabasePopulator(databasePopulator);
        dataSourceInitializer.setEnabled(true);

        return dataSourceInitializer;
    }
}

otherDB用。各種名称、参照がownからotherになっている以外はOwnDataSourceConfiguration.javaと同じ。

以上で、application.propertiesで複数DataSourceの設定が行えるようになった。
次はApplication.javaでのAnnotationの要変更箇所を見ていく。

Application.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

import jp.co.sample.system.config.db.OwnDBProperties;
import jp.co.sample.system.config.db.OtherDBProperties;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({DataSourceProperties.class, OwnDBProperties.class, OtherDBProperties.class})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Spring Bootアプリとして実行するために、Application.javaで@SpringBootApplicationを付与しているが、@SpringBootApplicationが付与されていると自動的にDataSource関連の設定を行ってくれる。
しかし、その設定を行ってくれるクラスDataSourceAutoConfiguration.classは単一DBの設定を前提としているため、@SpringBootApplicationのexcludeに設定することで、DataSourceAutoConfiguration.classが動かないようにしている。

@EnableConfigurationPropertiesは、@ConfigurationPropertiesを付与されたbeansが働くようにするために必要な設定である。
今回独自に追加したOwnDBProperties.class, OtherDBProperties.classを設定するのに加え、本来DataSourceAutoConfiguration.classで設定されていたデフォルトのDataSourceProperties.classも設定する。

Javaのコード部分は以上で終わりとなる。
最後に/src/main/resourcesを見ていく。

CustomerMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="jp.co.sample.mapper.own.CustomerMapper">

    <select id="selectAll" resultMap="Customer">
        SELECT *
        FROM customer
    </select>

</mapper>

xmlファイルで注意する点はが指定するmapper interfaceがどのディレクトリにあるかということだけ。
jp.co.sample.mapper.own.CustomerMapperが正解で、jp.co.sample.mapper.CustomerMapperやjp.co.sample.mapper.other.CustomerMapperというように格納されているディレクトリ名をコピペで処理して間違えないようにしなければいけない。

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>
    <typeAliases>
        <package name="jp.co.sample.domain" />
    </typeAliases>
    <typeHandlers>
        <typeHandler handler="jp.co.sample.mapper.handler.LocalDateTypeHandler" />
        <typeHandler handler="jp.co.sample.mapper.handler.LocalDateTimeTypeHandler" />
    </typeHandlers>

</configuration>

mybatis-config.xmlは複数DBになったことによる影響はない。理由は今回についてはdomainディレクトリ以下をownとotherで分けなかったため、typeAliasesのpackage nameに影響がなかったから。
domain以下もディレクトリを分けるのであれば、mybatis-config-own.xmlとmybatis-config-other.xmlの二つに分けるのが良いと思う。そしてmybatis-configのファイルパス指定をapplication.propertiesで行うのではなく、OwnDataSourceConfiguration、OtherDataSourceConfigurationで直指定すればいい。

application.properties


#Mybatis設定

mybatis.config=classpath:mybatis-config.xml

#接続情報

spring.own.datasource.url=jdbc:h2:mem:own;
#初期設定

spring.own.datasource.schema=classpath:schema_own.sql
spring.own.datasource.data=classpath:data_own.sql

#コネクションプール設定

spring.own.datasource.max-active=100
spring.own.datasource.max-idle=8
spring.own.datasource.min-idle=8
spring.own.datasource.initial-size=8
#コネクションを利用する際に検証を行う。DBが再起動していてもこの処理を挟むことでtomcatを再起動しなくても済む

spring.own.datasource.test-on-borrow=true
spring.own.datasource.validation-query=SELECT 1
#コミットされずに残ったコネクションは60秒後に破棄される。

spring.own.datasource.remove-abandoned=true
spring.own.datasource.remove-abandoned-timeout=60
#auto commit

spring.own.datasource.default-auto-commit=true


#接続情報

spring.other.datasource.url=jdbc:h2:mem:other;
#初期設定

spring.other.datasource.schema=classpath:schema_other.sql
spring.other.datasource.data=classpath:data_other.sql

#コネクションプール設定

spring.other.datasource.max-active=100
spring.other.datasource.max-idle=8
spring.other.datasource.min-idle=8
spring.other.datasource.initial-size=8
#コネクションを利用する際に検証を行う。DBが再起動していてもこの処理を挟むことでtomcatを再起動しなくても済む

spring.other.datasource.test-on-borrow=true
spring.other.datasource.validation-query=SELECT 1
#コミットされずに残ったコネクションは60秒後に破棄される。

spring.other.datasource.remove-abandoned=true
spring.other.datasource.remove-abandoned-timeout=60
#auto commit

spring.other.datasource.default-auto-commit=true