doma-templateでJdbcTemplateを2-way SQL化したサンプルをApache License 2.0で公開しました

# doma-templateの紹介
## doma-templateとは？
常々、SpringのJdbcTemplateでDomaの2-way SQLを使えたらいいのになあ・・・と思っていました。

で、👇のようなことをつぶやいたら、Doma作者のnakamura_toさんからこんなリプライをいただきました！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">例えばですが、doma-coreライブラリに依存する形で新しいライブラリを提供するとかなら比較的簡単に実現できる気がします。ライブラリのgroup idがorg.seasar.domaでも構わないですよね？</p>&mdash; toshihiro nakamura (@nakamura_to) <a href="https://twitter.com/nakamura_to/status/1564909041238740992?ref_src=twsrc%5Etfw">August 31, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ということで誕生したのがdoma-templateです。

- [公式ドキュメント](https://doma.readthedocs.io/en/latest/sql/#doma-template-module)
- [ソースコード](https://github.com/domaframework/doma/tree/2.53.0/doma-template)

Domaから2-way SQL APIを切り出すのではなく、Domaに依存する形で作られています。

## doma-templateのAPI
doma-templateにはクラスが3つしかありません。それぞれのかんたんな使い方を紹介していきます。

### `SqlTemplate` クラス
doma-templateの中核となるクラスです。[Domaの2-way SQL記法](https://doma.readthedocs.io/en/latest/sql/#overview)で書かれた文字列を、JDBC形式のSQL（ `SELECT ... FROM ... WHERE hoge = ?` ）とパラメータのリストに変換します。

### `SqlStatement` クラス
下記の3つを保持するクラスです。

1. `rawSql`
    - JDBC形式のSQL（パラメータは`?`のまま）です。
    - 例: `select * from emp where name = ? and salary = ?`
2. `formattedSql`
    - インデントがされた状態で、パラメータに実際の値が代入されています。
    - 例: `select * from emp where name = 'Smith' and salary = 200000` （実際にはインデントされています）
3. `arguments`
    - `rawSql` の `?` の部分にバインドされるべきパラメータ値をリスト形式で保持します。
    - パラメータ値の順番は、 `rawSql` 内の `?` の順番に対応しています。

### `SqlArgument` クラス
パラメータ値を保持するクラスです。1つのパラメータの値（ `value` ）とその型（ `type` ）を保持しています。

## 使い方の例
> [Doma公式ドキュメント](https://doma.readthedocs.io/en/latest/sql/#usage)から引用しています。

```java:doma-teamplateの使い方の例
String sql = "select * from emp where name = /* name */'' and salary = /* salary */0";
SqlStatement statement =
    new SqlTemplate(sql)
        .add("name", String.class, "abc")
        .add("salary", int.class, 1234)
        .execute();
String rawSql = statement.getRawSql(); // select * from emp where name = ? and salary = ?
List<SqlArgument> arguments = statement.getArguments();
```

# JdbcTemplateの2-way SQL化
では、doma-templateを利用して、JdbcTemplateで2-way SQLを使えるようにしてみます。

2-way SQLはファイルに記述して、src/main/resourcesに配置します。

コードの全体像はGitHubにあります。

https://github.com/MasatoshiTada/doma-template-jdbctemplate

## クラス定義など
```java:SqlParam.java
public record SqlParam<T>(String name, Class<T> valueType, T value) {
}
```

```java:TwoWayJdbcTemplate.java（定義など）
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.template.SqlStatement;
import org.seasar.doma.template.SqlTemplate;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.StatementCreatorUtils;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.PreparedStatement;
import java.util.List;

public class TwoWayJdbcTemplate {

    private final JdbcTemplate jdbcTemplate;
    
    private final Dialect dialect;

    public TwoWayJdbcTemplate(JdbcTemplate jdbcTemplate, Dialect dialect) {
        this.jdbcTemplate = jdbcTemplate;
        this.dialect = dialect;
    }
```

そして、こんなprivateメソッドを用意しておきます。

```java:TwoWayJdbcTemplate.java（privateメソッド）
    @SuppressWarnings({"rawtypes", "unchecked"})
    private SqlStatement getSqlStatement(String sqlOnClassPath, SqlParam<?>... sqlParams) {
        URL sqlFileUrl = this.getClass().getClassLoader().getResource(sqlOnClassPath);
        if (sqlFileUrl == null) {
            throw new TwoWayJdbcException(sqlOnClassPath + " not found");
        }
        try {
            Path sqlFile = Path.of(sqlFileUrl.toURI());
            String sql = Files.readString(sqlFile, StandardCharsets.UTF_8);
            SqlTemplate sqlTemplate = new SqlTemplate(sql, dialect);
            for (SqlParam sqlParam : sqlParams) {
                sqlTemplate = sqlTemplate.add(sqlParam.name(), sqlParam.valueType(), sqlParam.value());
            }
            SqlStatement sqlStatement = sqlTemplate.execute();
            return sqlStatement;
        } catch (URISyntaxException e) {
            throw new TwoWayJdbcException(e);
        } catch (IOException e) {
            throw new TwoWayJdbcException(e);
        }
    }

    private Object[] getParams(SqlStatement sqlStatement) {
        Object[] params = sqlStatement.getArguments()
                .stream()
                .map(arg -> arg.getValue())
                .toArray();
        return params;
    }
```

## 検索

```java:TwoWayJdbcTemplate.java（単一検索メソッド）
    public <T> T queryForObject(String sqlOnClassPath, Class<T> resultType, SqlParam<?>... sqlParams) throws TwoWayJdbcException {
        SqlStatement sqlStatement = getSqlStatement(sqlOnClassPath, sqlParams);
        Object[] params = getParams(sqlStatement);
        String rawSql = sqlStatement.getRawSql();
        T result = jdbcTemplate.queryForObject(rawSql, new DataClassRowMapper<>(resultType), params);
        return result;
    }
```

```java:TwoWayJdbcTemplate.java（複数検索メソッド）
    public <T> List<T> query(String sqlOnClassPath, Class<T> resultType, SqlParam<?>... sqlParams) throws TwoWayJdbcException {
        SqlStatement sqlStatement = getSqlStatement(sqlOnClassPath, sqlParams);
        Object[] params = getParams(sqlStatement);
        String rawSql = sqlStatement.getRawSql();
        List<T> result = jdbcTemplate.query(rawSql, new DataClassRowMapper<>(resultType), params);
        return result;
    }
```

## 単純な更新
```java:TwoWayJdbcTemplate.java（単純な更新メソッド）
    public int update(String sqlOnClassPath, SqlParam<?>... sqlParams) {
        SqlStatement sqlStatement = getSqlStatement(sqlOnClassPath, sqlParams);
        Object[] params = getParams(sqlStatement);
        String rawSql = sqlStatement.getRawSql();
        int rows = jdbcTemplate.update(rawSql, params);
        return rows;
    }
```

## 更新＋生成された主キー値を取得
`StatementCreatorUtils.javaTypeToSqlParameterType()` がポイント。

```java:UpdateResult.java
public record UpdateResult(int updatedRows, Number key) {
}
```

```java:TwoWayJdbcTemplate.java（更新＋生成された主キー値取得）
    public UpdateResult updateAndGetKey(String sqlOnClassPath, String pkColumnName, SqlParam<?>... sqlParams) {
        SqlStatement sqlStatement = getSqlStatement(sqlOnClassPath, sqlParams);
        String rawSql = sqlStatement.getRawSql();
        KeyHolder keyHolder = new GeneratedKeyHolder();
        int rows = jdbcTemplate.update(con -> {
            PreparedStatement statement = con.prepareStatement(rawSql, new String[]{pkColumnName});
            for (int i = 0; i < sqlParams.length; i++) {
                SqlParam<?> sqlParam = sqlParams[i];
                statement.setObject(i + 1, sqlParam.value(), StatementCreatorUtils.javaTypeToSqlParameterType(sqlParam.valueType()));
            }
            return statement;
        }, keyHolder);
        Number key = keyHolder.getKey();
        return new UpdateResult(rows, key);
    }
```

# なぜサンプルをApache License 2.0で公開したのか
当初はdoma-templateとJdbcTemplateを組み合わせたものをOSSライブラリとして公開することを考えていました。

OSSライブラリとして公開するとメンテナンスを継続的に行っていく必要があります。しかし、その時間を捻出するのはちょっと難しいのが現状です。

そこで、サンプルのみをOSSとして公開し、

- コードのコピーや改編が可能かつそれらの公開が求められない
- かつ、依存するDomaも同じライセンス

ということで、Apache License 2.0としました。

この記事とサンプルコードが、2-way SQLを使いたい人たちの役に立つと嬉しいです。