Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

Spring Bootでマルチデータソースのやり方

スポンサードリンク

f:id:fa11enprince:20200613105140j:plain 以前このブログで紹介したMaven + Eclipseでマルチモジュールプロジェクトを作成するのなかでサンプルコードで示したものの特にマルチデータソースのやり方についての解説です。
gup.monster
この記事に書いてある通り、PostgreSQLとMongoDBを組み合わせる場合を例として書いてあります。

完成版のソースコードはこちらです。
github.com

ミドルウェア バージョン
Spring Boot 2
Java 1.8
Apache Maven 3

細かいバージョンはGitHubを参照してください。

Spring-Data-JPAとSpring-Data-MongoDBを利用した場合のやり方を解説します。

構成の概要

プロジェクト/モジュール/パッケージは次のようになっています。
2モジュールありmulti-module-batchはサンプルを動かす利用側コード
multi-module-commonはデータアクセス系を提供する共通モジュールの位置づけです。
f:id:fa11enprince:20200613104405p:plain

設定ファイル

PostgreSQLとMongoDBを利用する場合を考えてみます。
まずは設定ファイルの構成を考えます。
application.yml

spring:
  datasource:
    postgres:
      url: jdbc:postgresql://localhost:5433/postgres
      username: postgres
      password: postgres
    mongo:
      host: localhost
      port: 27018
      database: mongo
  profiles:
    include: common

重要なところのみ抜粋です。
このように利用する側の設定で、ユーザ名、パスワード、データベースを指定できれば設定ファイルとしては十分かと思います。
また、このプロジェクトではマルチモジュールを採用していてデータベースアクセス部分はcommonのプロジェクトが別にいます。
例えばコネクションプール系の設定はだいたい一緒でいいだろうというのがあると思いますので、
commonの設定に切り出したいとします。
それがinclude: commonの部分です。この場合Springがapplication-common.ymlという設定ファイルを追加で読み込みます。

共通モジュール側のプロジェクトのapplication-common.yaml

packages:
  common:
    component: com.example.multiModule
    postgres:
      entity: com.example.multiModule.common.spring.postgres.entities
      repository: com.example.multiModule.common.spring.postgres.repositories
    mongo:
      entity: com.example.multiModule.common.spring.mongo.entities
      repository: com.example.multiModule.common.spring.mongo.repositories

spring:
  datasource:
    postgres:
      driver-class-name: org.postgresql.Driver
    hikari:
      minimum-idle: 2 # Default: Same as maximum-pool-size
      maximum-pool-size: 4 # Default: 10
      idle-timeout: 60000 # Default: 60seconds
      connection-timeout: 30000 # Default: 30000
      leak-detection-threshold: 5000 #5sec Default: 0 -> disable
      max-lifetime: 600000 # 10min, Default: 1800000 (30 minutes)

使用するドライバとコネクションプールの設定です。
Mongoは今回特にこれに相当する設定をしないので、省略しています。
packagesの設定は@EnableJpaRepositories, @EnableMongoRepositoriesで利用します。

マルチデータソースの場合、Configを個別に用意してあげる必要があります。

マルチデータソースの場合、Configを個別に用意してあげる必要があります。
こちらのプロジェクトではデータアクセスにかかわる部分、RepositoryとEntittyも実装します。
また、Serviceも今回こちらに実装します。
基本的に定型の設定をコードベースで書くことになります。ある意味覚えゲーです。

PostgreSQL設定

ポイントとなるのはPostgresConfig.javaMongoConfig.javaです。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = {"${packages.common.postgres.repository}"},
    entityManagerFactoryRef = "postgresEntityManagerFactory",
    transactionManagerRef = "postgresTransactionManager"
)
@EntityScan(basePackages = { "${packages.common.postgres.entity}" })
public class PostgresConfig {

    @Autowired
    PackageConfig packageConfig;

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.postgres")
    public DataSourceProperties postgresDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.jpa.postgres")
    public JpaProperties postgresJpaProperties() {
        return new JpaProperties();
    }

    @Bean(name = "postgresDataSource")
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean(name = "postgresEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory(
            final EntityManagerFactoryBuilder builder,
            @Qualifier("postgresDataSource") final DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages(packageConfig.getCommon().getPostgres().getEntity())
                .persistenceUnit("postgres")
                .properties(postgresJpaProperties().getProperties())
                .build();
    }

    @Bean(name = "postgresTransactionManager")
    @Primary
    public PlatformTransactionManager postgresTransactionManager(
            @Qualifier("postgresEntityManagerFactory") final EntityManagerFactory entityManagerFactory) {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}

カスタムで設定を書くので@EnableTransactionManagement, @EnableJpaRepositoriesの指定は必須となります。
後者の@EnableJpaRepositoryですが@Repositoryをついたクラスを検索してBeanとして登録するためのものです。
また、マルチモジュールにしている関係でEntityの場所がどこ?となってしまうので@EntityScanを指定してやる必要があります。

各メソッドの説明

設定値を読む用のメソッドもあったりして厄介ですが、このクラスのメソッドで絶対に書かなければならないのが、3つあります。

    @Bean(name = "postgresDataSource")
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

これが1つ目です。HikariCPを使うようにしています。
さらにDataSourceProperties経由でjdbcドライバ等の設定を読むようにしています。
名前が被るのでBean名をアノテーションで変更してあげます。ここではpostgresDataSourceとしています。
細かい内容はこの辺りの内容が参考になると思います。
https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-data-access

2つ目が

    @Bean(name = "postgresEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory(
            final EntityManagerFactoryBuilder builder,
            @Qualifier("postgresDataSource") final DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages(packageConfig.getCommon().getPostgres().getEntity())
                .persistenceUnit("postgres")
                .properties(postgresJpaProperties().getProperties())
                .build();
    }

です。なんとも長ったらしいメソッドですが、
LocalContainerEntityManagerFactoryBeanを返すメソッドを作ってやらなければなりません。
エンティティ登録管理のファクトリを教えてくれってことですね。Springムズカシイ。
補足ですがアノテーションの@QualifierはどのDataSourceよってSpringがわかんないので、
さっき定義したpostgresDataSouceっすよってことで教えてあげます。
しばらく触ってないと忘れがちですが@Beanによってクラスを返すメソッドを定義してSpring Beanにすると @Autowiredで注入できるのですよね。
何を言っているのかわからねー、という人はここがわかりやすいかなと思います。
dev.classmethod.jp
紛らわしいのが単にgetter/setterの塊のJava Beans。これとは違う用語なので混同しないでくださいね。

3つ目、

    @Bean(name = "postgresTransactionManager")
    @Primary
    public PlatformTransactionManager postgresTransactionManager(
            @Qualifier("postgresEntityManagerFactory") final EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

これも先ほど同様にな感じで先ほど定義したBeanを注入して設定する、な感じです。

MongoDBの設定

Spring-Data-MongoDBを利用する前提です。
こちらはずいぶんシンプルです。設定の読み込みをのぞけば2メソッド書けば終わりです。
また、@EnableMongoRepositories@EntityScanを書く必要があります。後者はマルチモジュールにしているためですね。

@Configuration
@EnableMongoRepositories(basePackages = { "${packages.common.mongo.repository}" })
@EntityScan(basePackages = { "${packages.common.mongo.entity}" })
public class MongoConfig {
    
    @Autowired
    MongoMappingContext mongoMappingContext;
    
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.mongo")
    public MongoProperties mongoProperties() {
        return new MongoProperties();
    }

    @Bean
    @Primary
    public MongoClient mongo() {
        return new MongoClient(mongoProperties().getHost(), mongoProperties().getPort());
    }

    @Bean
    @Primary
    public MongoTemplate mongoTemplate() throws Exception {
        MongoDbFactory factory = new SimpleMongoDbFactory(mongo(), mongoProperties().getDatabase());
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));  // remove _class
        
        return new MongoTemplate(factory, converter);
    }
}

各メソッドの説明

    @Bean
    @Primary
    public MongoClient mongo() {
        return new MongoClient(mongoProperties().getHost(), mongoProperties().getPort());
    }

解説不要そうなこちらと

    @Bean
    @Primary
    public MongoTemplate mongoTemplate() throws Exception {
        MongoDbFactory factory = new SimpleMongoDbFactory(mongo(), mongoProperties().getDatabase());
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));  // remove _class
        
        return new MongoTemplate(factory, converter);
    }

こちらが必要です。実はこちらについては

public MongoTemplate mongoTemplate() throws Exception {
    return new MongoTemplate(
              new SimpleMongoDbFactory(new MongoClient(mongo.getHost(), mongo.getPort()), mongo.getDatabase()));
}

と書くだけでよいのですが、デフォルトのまま使うとすべてのコレクションのドキュメントに_classと勝手にフィールドがつくのでそれが嫌だったので消す設定を追加しています。
以上で終わりです。

どう使えばいいのか

それで、このように定義した場合後はどうすればいいかというとEntityとRepositoryの置き場さえ守れば、
通常通りEntityとRepositoryを書けます。
PostgreSQLの場合、@EnableJpaRepositoriesと設定ファイルで、
Entity: com.example.multiModule.common.spring.postgres.entities
Repository: com.example.multiModule.common.spring.postgres.repositories
とパッケージを指定してあげてます。
Mongoの場合もほぼ同様で、
Entity: com.example.multiModule.common.spring.mongo.entities
Repository: com.example.multiModule.common.spring.mongo.repositories
つまり、PostgreSQLはpostgesパッケージ配下、MongoDBはmongoパッケージ配下に書けばよいことになります。

詳細はGitHubのリンクを参照してください。

備考

multi-module-batchにある利用する側のモジュールのコードです。

@SpringBootApplication
@ComponentScan(basePackages = { "${packages.common.component}", "${app.packages}" })
public class SampleDbAccessApp implements ApplicationRunner {
    @Autowired
    PosgresSampleService postgresSampleService;
    @Autowired
    MongoSampleService mongoSampleService;
    
    public static void main(String[] args) {
        SpringApplication.run(SampleDbAccessApp.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        PostgresSample postgre = new PostgresSample();
        postgre.setName("John");
        postgresSampleService.save(postgre);
        List<PostgresSample> findResult1 = postgresSampleService.findAll();
        findResult1.forEach(System.out::println);
        
        MongoSample mongo = new MongoSample();
        mongo.setName("Kate");
        mongoSampleService.save(mongo);
        List<MongoSample> findResult2 = mongoSampleService.findAll();
        findResult2.forEach(System.out::println);
    }
}

1点言うことがあるとすれば、マルチモジュールプロジェクトで、コンポーネントのScanする場所が変わっています。
この場合、multi-module-commonプロジェクト配下なので、@ComponentScanで指定してあげる必要があります。 変数を使っていますが、これはAppConfig.javaにより定義して設定値から読み取っています。

使う側から見ると何の変哲もない感じで自然に扱えます。

参考

Spring bootでマルチデータソース対応の実装方法(MybatisとSpring Data JPA ) - Qiita
5.1. データベースアクセス(共通編) — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.1.RELEASE documentation
Multiple MongoDB connectors with Spring Boot