0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DB 更新メソッドの引数にて transaction object をオプション引数的に扱う方式案

Last updated at Posted at 2023-12-30

概要

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 を使えば楽に実現もできるのかも)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?