概要
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クラスを作成します。
@Entity
@Table(name = "UserMst")
public class UserMstEntity {
@Column(name = "usr_id")
private String usrId;
@Column(name = "usr_name")
private String usrName;
...
@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 を取得し、getDataSource()
メソッドでデータソースを取得します。
/**
* @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 の実装クラスをインジェクションします。この時、Spring は @ConditionalOnClass
を使って、Configの実装クラスが存在しなければ、DomaConfigを生成してインジェクションします。
DataSource
も Bean
として定義しておけば、DomaConfigにインジェクションされるため、Daoの実装クラスは DomaConfigのgetDataSouce()
でデータソースが取得できるようになります。
SpringBoot では、aplication.yml の spring.datasource
が、DataSource
のBean
にマッピングされているため、以下のように application.yml を設定するだけで、DataSource
の Bean
が自動的に生成されます。
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クラスを生成してインジェクションします。
@Service
public class DBService {
private UserMstDao userMstDao;
public DBService(UserMstDao userMstDao) {
this.userMstDao = userMstDao;
}
@Transactional
public void dbaccess001() {
List<UserMstEntity> users = userMstDao.selectUserMst();
@Autowired
private DBService service;
この時、service
には、DBService
のインスタンスではなく、Proxyクラスのインスタンスがインジェクションされているため、@Transactional
を付与したメソッドを呼び出すと、メソッドの前後でトランザクション処理が動作します。
トランザクションを管理しているのは DataSourceTransactionManagerですが、この Bean も TransactionManager
が存在しなければ、AutoConfiguration により自動生成されています。
ここで、DAO がトランザクション管理されるためには、コネクションは TransactionManager
が管理しているコネクションである必要がありますが、前述したように Domaは DAOのコンストラクタでインジェクションされたDomaConfig
が返すDataSource からコネクションを取得するため、そのままでは、TransactionManager
が管理しているコネクションを使えません。
このため、Spring が自動生成するDomaConfig
のDataSource
は、DomaConfigBuilderの中で以下のように TransactionAwareDataSourceProxyでラップしています。
/**
* 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 の実装クラスを自分で定義した場合には、getDataDasource
メソッドでTransactionAwareDataSourceProxyを返すように実装しないと、トランザクション管理されないので注意が必要です。
クラス関連図
このように、SpringBoot と Doma を使ったデータベースアクセスでは、データソース、Doma のコンフィグ、トランザクションマネージャの3つが、AutoConfiguration によってBeanが生成されて、インジェクションにより動作しています。各クラスの関連を図にすると以下のようになります。
複数データソースのトランザクション管理
単一のデータソースの場合は、AutoConfiguration にまかせておけばよいのですが、複数のデータソースを扱う場合には、それぞれの BeanにBean名を定義して対応を付けてあげる必要があります。
データソースは Spring の AutoConfiguration を使わずに、自分で Beanを定義します。ここでは XML Beanによる定義を紹介しておきます。
<?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の bean とトランザクションマネージャの bean を返す Configuration
を定義します。
@Qualifier
で bean名を指定してデータソースをインジェクションします。
Configの bean名は domaconfig001
、トランザクションマネージャの bean名を txManager001
で定義しています。
@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を返すように実装します。
/**
* 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
という名前でアノテーションを再定義してしまいます。
@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
にしておきます。デフォルトではランタイム例外の場合のみロールバック対象ですが、ほとんどの場合、チェック例外でもロールバックしたほうが使いやすいかと思います。
@Retention(RetentionPolicy.RUNTIME)
@Transactional(rollbackFor = Exception.class)
public @interface Transactional001 {
@AliasFor(annotation = Transactional.class, attribute="value")
String value() default "txManager001";
}
@ConfigAutowireble001
をDaoクラスに指定すると、domaconfig001
がインジェクションされて、dataSource001
と関連付けられます。
@Dao
@ConfigAutowireable001
public interface UserMstDao {
@Select
public List<UserMstEntity> selectUserMst();
...
同様に @Transactional001
で dataSource001
を使ったトランザクションマネージャを使うことができます。
@Service
public class DBService {
private UserMstDao userMstDao;
public DBService(UserMstDao userMstDao) {
this.userMstDao = userMstDao;
}
@Transactional001
public void dbaccess001() {
List<UserMstEntity> users = userMstDao.selectUserMst();
関連図は以下のようになります。
複数のデータソースを扱う場合には、上記のクラスのセットを複数定義することで、アノテーションにより接続先のデータベースを使い分けることができるようになります。
まとめ
SpringBoot のアノテーションや AutoConfiguration は非常に強力で、適切にアノテーションを設定すれば、少ないコーディング量でそこそこ動作するアプリケーションを作成することができますが、内部の動作やトランザクション管理の仕組みを理解しておくことは重要です。
本記事が理解の参考になれば幸いです。