📘

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

2024/08/13に公開

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>— toshihiro nakamura (@nakamura_to) <a href="https://twitter.com/nakamura_to/status/1564909041238740992?ref_src=twsrc^tfw">August 31, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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

doma-templateのAPI

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

SqlTemplate クラス

doma-templateの中核となるクラスです。Domaの2-way SQL記法で書かれた文字列を、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公式ドキュメントから引用しています。

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

クラス定義など

SqlParam.java
public record SqlParam<T>(String name, Class<T> valueType, T value) {
}
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メソッドを用意しておきます。

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;
    }

検索

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;
    }
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;
    }

単純な更新

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() がポイント。

UpdateResult.java
public record UpdateResult(int updatedRows, Number key) {
}
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を使いたい人たちの役に立つと嬉しいです。

Discussion