概要
DB にクエリ発行するメソッドにおいて
- 外部から transaction object を渡してクエリを発行する
- transaction object の省略時はメソッド実行時に tranaction を作成してクエリを発行する
をシンプルに実現したい、というのが基本的なモチベーションとしてありました。
これを実現する方式をいくつか検討しました。
基本実装
User を取得するコードを考えます。transaction 内でデータ取得したい場合と、単純な参照系の機能などで transaction を意識せず取得したい場合があり、この両方を同じコードで対応したいです。
class UserRepository
{
public User findById(String userId, QueryCondition condition, Connection conn)
{
try (
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(...);
) {
return new User(rs);
}
}
}
実装案1. 呼び出し元制御
transaction が必要な場合は、呼び出し元で transaction object (例では Connection) を管理していますので、それをそのまま渡せば良いです。しかし単純な参照系の機能でも必要性が薄い transaction object を作成して渡す必要があります。
// without transaction
void executeUsecase()
{
try (Connection conn = DriverManager.getConnection(...))
{
var user = userRepository.findById(userId, condition, conn);
}
}
呼び出し元(usecase)で DB 接続ライブラリが提供する transaction object に依存する必要も無いように、出来れば下記のようにシンプルに記述したいですね。
// without transaction
void executeUsecase()
{
// 呼び出し先で自動で transaction object を生成・破棄してくれる
var user = userRepository.findById(userId, condition);
}
実装案2. 呼び出し先制御
呼び出し先で制御することを考えます。
class UserRepository
{
public User findById(String userId, QueryCondition condition, Connection connParam)
{
Connection conn = connParam;
if (connParam == null)
{
conn = DriverManager.getConnection(...);
}
try (
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(...);
) {
return new User(rs);
}
finally
{
if (connParam == null)
{
conn.close();
}
}
}
}
呼び出し元は transaction object または null のどちらかを渡すようにするだけで良いです。しかし呼び出し先の全てのメソッドで transaction 生成判定コードを書くことになりますので、コード重複が気になります。
実装案3. オーバーロード
オーバーロードで対応します。
class UserRepository
{
public User findById(String userId, QueryCondition condition, Connection conn)
{
...
}
public User findById(String userId, QueryCondition condition)
{
try (Connection conn = DriverManager.getConnection(...))
{
return findById(userId, condition, conn);
}
}
}
メソッド毎に transaction object を取らないメソッドも定義する必要があるのが、やや面倒かもしれません。transaction object 生成コード部分もコード重複になります。
実装案4. 関数型インターフェース
java.util.functon.Function
で対応します。
class UserRepository
{
// メソッド毎に作成するコード
BiFunction<FindByIdInput, Connection, User> findById = (input, conn) -> {
try (
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(...);
) {
return new User(rs);
}
};
class FindByIdInput
{
String userId;
QueryCondition condition;
}
// ここまで
// 以下のコードは共通化して一つで ok
<T, R>R static query(BiFunction<T, Connection, R> func, T t)
{
try (Connection conn = DriverManager.getConnection(...))
{
return func.apply(t, conn);
}
}
<T, Connection, R>R static query(BiFunction<T, Connection, R> func, T t, Connection conn)
{
return func.apply(t, conn);
}
}
呼び出し元
// without transaction
void executeUsecaseWithoutTransaction()
{
FindByIdInput input = new FindByIdInput(...);
var user = query(userRepository.findById, input);
}
// with transaction
void executeUsecaseWithtTransaction()
{
try (Connection conn = DriverManager.getConnection(...))
{
conn.setAutoCommit(false);
FindByIdInput input = new FindByIdInput(...);
var user = query(userRepository.findById, input, conn);
// こちらでも呼べるが、見た目の統一性から 常に`query()` で良いかも
var user = userRepository.findById.apply(input, conn);
conn.commit();
}
}
コード重複は無くなりますが、入力パラメータを inner class 等でオブジェクト定義する必要があります。
結論
いずれの方式も一長一短があります。
私の現時点のプロダクトでは、結局案1.を採用することにしました。
理由:
- transaction が必要な usecase では DB を意識する必要がある。transaction 不要な usecase でも DB 呼び出しがあることを意識はしているので、ここで不要な transaction object が出てきても許容できる
- ライブラリの都合上、transaction は DB connection と密接な関係がある
もっとスマートな方式もありそうな気はしています。
(相応の framework を使えば楽に実現もできるのかも)