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

# 概要
DB にクエリ発行するメソッドにおいて
- 外部から transaction object を渡してクエリを発行する
- transaction object の省略時はメソッド実行時に tranaction を作成してクエリを発行する

をシンプルに実現したい、というのが基本的なモチベーションとしてありました。
これを実現する方式をいくつか検討しました。

# 基本実装
User を取得するコードを考えます。transaction 内でデータ取得したい場合と、単純な参照系の機能などで transaction を意識せず取得したい場合があり、この両方を同じコードで対応したいです。
 
```java
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 を作成して渡す必要があります。

```java
// without transaction
void executeUsecase()
{
    try (Connection conn = DriverManager.getConnection(...))
    {
        var user = userRepository.findById(userId, condition, conn);
    }
}
```
呼び出し元(usecase)で DB 接続ライブラリが提供する transaction object に依存する必要も無いように、出来れば下記のようにシンプルに記述したいですね。
```java
// without transaction
void executeUsecase()
{
    // 呼び出し先で自動で transaction object を生成・破棄してくれる
    var user = userRepository.findById(userId, condition);
}
```


# 実装案2. 呼び出し先制御
呼び出し先で制御することを考えます。
```java
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. オーバーロード
オーバーロードで対応します。
```java
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` で対応します。

```java
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);
    }
}
```
呼び出し元
```java
// 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 等でオブジェクト定義する必要があります。

# 結論
いずれの方式も一長一短があります。
私の現時点のプロダクトでは、結局案１．を採用することにしました。
理由：
- transaction が必要な usecase では DB を意識する必要がある。transaction 不要な usecase でも DB 呼び出しがあることを意識はしているので、ここで不要な transaction object が出てきても許容できる
- ライブラリの都合上、transaction は DB connection と密接な関係がある

もっとスマートな方式もありそうな気はしています。
（相応の framework を使えば楽に実現もできるのかも）