SpringBoot+Doma2の裏側

[1]:https://javadoc.io/static/org.seasar.doma/doma-core/2.58.0/org/seasar/doma/jdbc/Config.html
[2]:https://docs.spring.io/spring-framework/docs/6.1.6/javadoc-api/org/springframework/jdbc/datasource/DataSourceTransactionManager.html
[3]:https://javadoc.io/static/org.seasar.doma.boot/doma-spring-boot-autoconfigure/1.8.0/org/seasar/doma/boot/autoconfigure/DomaConfig.html
[4]:https://javadoc.io/static/org.seasar.doma.boot/doma-spring-boot-autoconfigure/1.8.0/org/seasar/doma/boot/autoconfigure/DomaConfigBuilder.html
[5]:https://docs.spring.io/spring-framework/docs/6.1.6/javadoc-api/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.html

## 概要
DBアクセスフレームワークである Doma は、SpringBoot と合わせて使用することで、AutoConfiguration 機能によりデータソースや Doma の設定がインジェクションされ、単純な記述によりデータアクセスが可能になります。

一方で、トランザクションやDBコネクションも SpringBoot のアノテーションにより巧妙に隠蔽されるため、内部構造を把握していないと、複数のデータソースを扱いたい場合など、複雑な問題に対処するのが難しくなります。

複数のデータソースを使う場合のノウハウについてはインターネット上でもいろいろ紹介されていますが、この記事では、SpringBoot と Doma2 がどのような仕組みでDBアクセスを行っているかを説明し、複数データソースを扱う方法について紹介します。

## 動作環境

* spring-boot-3.2.5
* spring-boot-starter-jdbc-3.2.5
* doma-spring-boot-starter-1.8.0
* doma-core-2.58.0
* doma-processor-2.58.0

## Doma2概要
Doma を使ってDBアクセスを行うには、テーブルのカラムに対応したエンティティクラスと、データベースにアクセスするためのSQLに対応したDAOクラスを作成します。

```Java:UserMstEntity.java
@Entity
@Table(name = "UserMst")
public class UserMstEntity {

    @Column(name = "usr_id")
    private String usrId;
    @Column(name = "usr_name")
    private String usrName;
    ...
```

```Java:UserMstDao.java
@Dao
@ConfigAutowireable
public interface UserMstDao {
	
    @Select
    public List<UserMstEntity> selectUserMst();
    ...
```

`@Entity` `@Table` `@Column` `@Dao` `@Select` が doma-core のアノテーションです。
Daoクラスはインタフェースのみを作成すれば、doma-proceccer のアノテーションプロセッサが、インタフェース名に接尾辞`Impl`をつけたDaoの実装クラスを生成します。

`@ConfigAutowireble`は doma-spring-boot-core のアノテーションです。
Spring と合わせて使用する場合、Daoの実装クラスがDIコンテナで管理されるため、Daoのインタフェースに対して`@Autowired`を付与すれば、
生成されたDaoの実装クラスがインジェクションされます。このため、生成された実装クラスを意識することなく使用することができます。

## データソース
生成されたDaoの実装クラスではコンストラクタで [Config][1] を取得し、`getDataSource()`メソッドでデータソースを取得します。

```Java:UserMstDaoImpl.java
    /**
     * @param config the config
     */
    @org.springframework.beans.factory.annotation.Autowired()
    public UserMstDaoImpl(org.seasar.doma.jdbc.Config config) {
        __support = new org.seasar.doma.internal.jdbc.dao.DaoImplSupport(config);
    }
    ...
```

Spring が Daoの実装クラスのインスタンスをDIするとき、`@Autowired`により、 [Config][1] の実装クラスをインジェクションします。この時、Spring は `@ConditionalOnClass`を使って、[Config][1]の実装クラスが存在しなければ、[DomaConfig][3]を生成してインジェクションします。

`DataSource`も `Bean`として定義しておけば、[DomaConfig][3]にインジェクションされるため、Daoの実装クラスは [DomaConfig][3]の`getDataSouce()`でデータソースが取得できるようになります。

SpringBoot では、aplication.yml の `spring.datasource`が、`DataSource`の`Bean`にマッピングされているため、以下のように application.yml を設定するだけで、`DataSource`の `Bean`が自動的に生成されます。

```Yaml:application.yml
spring:
  datasource:
    url: jdbc:mysql://dbserver:3306/local?useSSL=false
    driverClassName: com.mysql.jdbc.Driver
    username: dbuser
    password: dbpassword 
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      maximum-pool-size: 5
      minimum-idle: 1
```

このように、SpringBoot の Autoconfiguration の仕組みにより、いつのまにか必要な Bean が作成されてインジェクションされて動作しています。

Bean を自分で定義しておくとそちらが優先されるのですが、Bean の定義の方法も XMLや`@Configuration`を使った方法など、複数のやり方があるため混乱の元となっているように思います。

## トランザクション
SpringBoot でトランザクションを制御するには、`@Transactional`アノテーションを付与するだけです。

`@Transactional`アノテーションを付与されていると、Spring はそのクラスのインスタンスをインジェクションする時に、メソッドの前後にコネクションの取得とコミット・ロールバック処理を挿入するための Proxyクラスを生成してインジェクションします。

```Java:DBService.java
@Service
public class DBService {
	
	private UserMstDao userMstDao;
	
	public DBService(UserMstDao userMstDao) {
		this.userMstDao = userMstDao;
	}
	
	@Transactional
	public void dbaccess001() {
		
		List<UserMstEntity> users = userMstDao.selectUserMst();
```

```Java:
    @Autowired
	private DBService service;
```
この時、`service`には、`DBService`のインスタンスではなく、Proxyクラスのインスタンスがインジェクションされているため、`@Transactional`を付与したメソッドを呼び出すと、メソッドの前後でトランザクション処理が動作します。

トランザクションを管理しているのは [DataSourceTransactionManager][2]ですが、この Bean も `TransactionManager`が存在しなければ、AutoConfiguration により自動生成されています。

ここで、DAO がトランザクション管理されるためには、コネクションは `TransactionManager`が管理しているコネクションである必要がありますが、前述したように Domaは DAOのコンストラクタでインジェクションされた`DomaConfig`が返すDataSource からコネクションを取得するため、そのままでは、`TransactionManager`が管理しているコネクションを使えません。

このため、Spring が自動生成する`DomaConfig`の`DataSource`は、[DomaConfigBuilder][4]の中で以下のように [TransactionAwareDataSourceProxy][5]でラップしています。

```Java:
	/**
	 * Set dataSource<br>
	 * <p>
	 * Note that the given dataSource is wrapped by
	 * {@link TransactionAwareDataSourceProxy}.
	 *
	 * @param dataSource dataSource to use
	 * @return chained builder
	 */
	public DomaConfigBuilder dataSource(DataSource dataSource) {
		this.dataSource = new TransactionAwareDataSourceProxy(dataSource);
		return this;
	}

```

これによって、Doma が生成したDAOがデータソースからコネクションを取得すると、Spring がトランザクション管理しているコネクションを取得する仕組みになっています。

[Config][1] の実装クラスを自分で定義した場合には、`getDataDasource`メソッドで[TransactionAwareDataSourceProxy][5]を返すように実装しないと、トランザクション管理されないので注意が必要です。

## クラス関連図
このように、SpringBoot と Doma を使ったデータベースアクセスでは、データソース、Doma のコンフィグ、トランザクションマネージャの３つが、AutoConfiguration によってBeanが生成されて、インジェクションにより動作しています。各クラスの関連を図にすると以下のようになります。


![classes1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3476949/5e548a56-ef12-cc6c-e112-355fa1018218.png)


## 複数データソースのトランザクション管理
単一のデータソースの場合は、AutoConfiguration にまかせておけばよいのですが、複数のデータソースを扱う場合には、それぞれの BeanにBean名を定義して対応を付けてあげる必要があります。


データソースは Spring の AutoConfiguration を使わずに、自分で Beanを定義します。ここでは XML Beanによる定義を紹介しておきます。

```xml:dbconfig001.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="dialect001" class="org.seasar.doma.jdbc.dialect.MysqlDialect" />

    <bean id="dataSource001" class="com.zaxxer.hikari.HikariDataSource"
        destroy-method="close">
        <constructor-arg ref="hikari001" />
    </bean>
	<bean id="hikari001" class="com.zaxxer.hikari.HikariConfig">
		<property name="jdbcUrl" value="jdbc:mysql://dbserver001:3306/example001?useSSL=false" />
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="minimumIdle" value="1" />
        <property name="maximumPoolSize" value="3" />
        <property name="username" value="testuser" />
        <property name="password" value="testuser" />
    </bean>
</beans>
```

データソースの bean名は `dataSource001`で定義しています。
コネクションプールとして HikariCP を使っていますが、別のコネクションプール実装を使う場合も、このXMLを変更するだけです。

このデータソースを使って、[Config][1]の bean とトランザクションマネージャの bean を返す `Configuration`を定義します。
`@Qualifier`で bean名を指定してデータソースをインジェクションします。

[Config][1]の bean名は `domaconfig001`、トランザクションマネージャの bean名を `txManager001`で定義しています。


```java:DomaConfig001.java
@Configuration
@ImportResource({"classpath:dbconfig001.xml"})
public class DomaConfig001 {

    @Bean(name = "domaconfig001")
    @Primary
    AppDomaConfig domaconfig001(
            @Qualifier("dialect001") Dialect dialect,
            @Qualifier("dataSource001") DataSource dataSource) {
		return new AppDomaConfig(dialect, dataSource);
	}

    @Bean(name = "txManager001")
    DataSourceTransactionManager txManager001(
            @Qualifier("dataSource001") DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
    }
}
```

DomaConfig クラスは以下のように`getDataDasource`メソッドで[TransactionAwareDataSourceProxy][5]を返すように実装します。

```java:AppDomaConfig.java
/**
 * Doma Configuration
 */
public class AppDomaConfig implements Config{
	
	private Dialect dialect;
	private DataSource dataSource;

	public AppDomaConfig (Dialect dialect, DataSource dataSource) {
		this.dialect = dialect;
		this.dataSource = dataSource;
	}

	@Override
	public DataSource getDataSource() {
		return new TransactionAwareDataSourceProxy(dataSource);
	}

	@Override
	public Dialect getDialect() {
		return dialect;
	}
}
```

Doma が生成する Implクラスに、上記の `domaconfig001`をインジェクションさせるためには、`@ConfiguAutowireable`に`@Qualifier`を指定します。
以下のように`ConfigAutowireable001`という名前でアノテーションを再定義してしまいます。

```java:ConfigAutowireable001.java
@AnnotateWith(annotations = {
		@Annotation(target = AnnotationTarget.CLASS, type = Repository.class),
		@Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Autowired.class),
		@Annotation(target = AnnotationTarget.CONSTRUCTOR_PARAMETER, type = Qualifier.class, elements = "\"domaconfig001\"") })
public @interface ConfigAutowireable001 {
}
```

`@Transactional`アノテーションは、`value`パラメータでトランザクションマネージャ名を指定することができます。
`@Transactional`も`@Transactional001`という名前でアノテーションを再定義してしまいます。

ついでに、`rollbackFor` を `Exception.class` にしておきます。デフォルトではランタイム例外の場合のみロールバック対象ですが、ほとんどの場合、チェック例外でもロールバックしたほうが使いやすいかと思います。

```java:Transactional001.java
@Retention(RetentionPolicy.RUNTIME)
@Transactional(rollbackFor = Exception.class)
public @interface Transactional001 {
	@AliasFor(annotation = Transactional.class, attribute="value")
	String value() default "txManager001";
}
```

`@ConfigAutowireble001`をDaoクラスに指定すると、`domaconfig001`がインジェクションされて、`dataSource001`と関連付けられます。

```Java:UserMstDao.java
@Dao
@ConfigAutowireable001
public interface UserMstDao {
	
    @Select
    public List<UserMstEntity> selectUserMst();
    ...
```

同様に `@Transactional001`で `dataSource001`を使ったトランザクションマネージャを使うことができます。

```Java:DBService.java
@Service
public class DBService {
	
	private UserMstDao userMstDao;
	
	public DBService(UserMstDao userMstDao) {
		this.userMstDao = userMstDao;
	}
	
	@Transactional001
	public void dbaccess001() {
		
		List<UserMstEntity> users = userMstDao.selectUserMst();
```

関連図は以下のようになります。

![classes2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3476949/4c3f2ce3-e6b5-39f2-0408-251e6c592ae5.png)


複数のデータソースを扱う場合には、上記のクラスのセットを複数定義することで、アノテーションにより接続先のデータベースを使い分けることができるようになります。

## まとめ
SpringBoot のアノテーションや AutoConfiguration は非常に強力で、適切にアノテーションを設定すれば、少ないコーディング量でそこそこ動作するアプリケーションを作成することができますが、内部の動作やトランザクション管理の仕組みを理解しておくことは重要です。
本記事が理解の参考になれば幸いです。