AbstractRoutingDataSource の代わりに TransactionTemplate を使う
はじめに
Spring Framework によるウェブアプリケーションを開発をする際、データベースのライターインスタンスとリーダーインスタンスの切り替えやシャーディングの実装についてインターネットを検索すると、 AbstractRoutingDataSource を利用した実装を紹介するブログ記事がいくつも出てきます。
AbstractRoutingDataSource を利用した複数 DataSource の切り替えは、 ThreadLocal による lookup key の受け渡しが必要になるため、ソースコードの明確さの観点でそれほどメリットがありません。
インターネット検索で AbstractRoutingDataSource の実装例が出てきてしまったり、一部のLLMが AbstractRoutingDataSource による実装を提案することがあるため、そのような情報ばかりに埋め尽くされないようにこの記事を書きました。
AbstractRoutingDataSource の使い方
AbstractRoutingDataSource を避ける理由を説明する前に、その使い方を説明します。
AbstractRoutingDataSource はインターフェース DataSource を実装する抽象クラスです。AbstractRoutingDataSource はその内部で実際に利用する DataSource を切り替えつつ、単一の DataSource のように利用できるようになっています。
DataSource を使う時は DataSource#getConnection か DataSourceUtils#getConnection を呼び出して Connection インスタンスを取得しますが、その際に実際に Connection を取得する DataSource を決定する必要があります。
この動作を実現するために、2つのメソッド
AbstractRoutingDataSource#setTargetDataSources(Map<Object, Object>)AbstractRoutingDataSource#determineCurrentLookupKey()
が重要な役割を果たします。
大まかな流れとして、以下の手順で AbstractRoutingDataSource は実行時に利用する DataSource を決定します。
-
AbstractRoutingDataSourceのインスタンスにsetTargetDataSourcesでルックアップ・キーとDataSourceのマップをセットする -
getConnection等のメソッドが呼び出されたときにdetermineCurrentLookupKeyを呼び出してルックアップキーを取得し、1でセットしたマップから対応するDataSourceのインスタンスを取得する
AbstractRoutingDataSource が実際に利用する DataSource を決定する際に呼び出すメソッドが AbstractRoutingDataSource#determineCurrentLookupKey です。
このメソッドの実装を見てみましょう。抽象メソッドとして定義されているので AbstractRoutingDataSource を継承したクラスで実装する必要があります。また、引数はないのでどこかからルックアップキーを取得する必要があります。通常、このルックアップキーの取得は ThreadLocal を用いて実装します。
AbstractRoutingDataSource の実装例
ThreadLocal を用いてルックアップキーを共有する AbstractRoutingDataSource の実装例をなるべく単純化して示します。この記事で紹介するサンプルコードのソースコード全体は GitHub で公開しています。
まず、 ThreadLocal をラップするクラス ShardContext を用意します。
AbstractRoutingDataSource の実装として ShardingRoutingDataSource を用意します。 ShardingRoutingDataSource は determineCurrentLookupKey を実装します。このメソッドは ShardContext からルックアップキーを取り出して返します。
ShardingRoutingDataSource を通してデータベースにアクセスする際には、 Connection を取得する前に ShardContext にルックアップキーをセットし、クエリ実行後に ShardContext をクリアする必要があります。
この実装では ShardingRoutingDataSource を使う際に必ず ShardContext を操作するという規約が必要になります。また、 ShardContext (= ThreadLocal) のクリアし忘れによって思わぬバグを埋め込む可能性もあります。
発展形: AOPを使ったThreadLocalの操作
AbstractRoutingDataSource を通してデータベースにアクセスするたびに ThreadLocal に値をセットし、処理が終わった後にクリアするとソースコードが煩雑になります。この煩雑さを回避するために、AOPを利用してメソッドにアノテーションを付けるだけで必要な操作ができるようにすることがあります。例えば、以下のコードのように実装できます。
しかし、アノテーションを利用して実装を隠蔽できる一方で、コードの動作を理解するために必要なコード量は増えてしまいます。特に問題が起きなければアノテーションを付けるだけで良いのは便利ですが、何か不具合が発生した場合はAOPが関係する実装の調査は手間がかかりがちです。
また、長期間開発が続いていると当初の意図がぶれてしまい、 @Around アノテーションのポイントカット式がカスタムされ、アドバイスが適用される条件が明確でなくなっていくことがあります。
代替案: TransactionTemplate を利用する
データベースごとに TransactionTemplate を利用することで隠蔽された「魔法」によってデータベースを切り替えることなく、ソースコード上で明白にデータベースを切り替えることができます。
下記の例では EntityManager と TransactionTemplate を組み合わせて利用しています。
この方法では TransactionTemplate と対応する EntityManager や DataSource を適切に組み合わせて利用する必要がありますが、 bean の名前を適切に決めることで、対応関係は明確にできます。
また、データベースのトランザクションを細かく制御する必要がある場合は、 TransactionTemplate を利用した実装の方が適切だと思われます。
ただし、 TransactionTemplate を使い分ける方法を導入する際に難しい点として、 TransactionTempalte のコンストラクタに渡す PlatformTransactionManager の使い方を理解しておく必要があります。
まとめ
- Spring Framework によるウェブアプリケーションの開発で、リーダーインスタンスとライターインスタンスの振り分けやシャーディングの実装で
AbstractRoutingDataSourceを利用した実装例はよく知られているが、それは唯一の方法ではない。 -
TransactionTemplateを利用する場合は定義した bean の使い分けでデータベースの使い分けをコード上で明白にできる。 - 特にトランザクション制御を綿密にコントロールする必要がある場合は
TransactionTemplateを利用した方が良い。
Discussion