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>— 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つを保持するクラスです。
-
rawSql
- JDBC形式のSQL(パラメータは
?
のまま)です。 - 例:
select * from emp where name = ? and salary = ?
- JDBC形式のSQL(パラメータは
-
formattedSql
- インデントがされた状態で、パラメータに実際の値が代入されています。
- 例:
select * from emp where name = 'Smith' and salary = 200000
(実際にはインデントされています)
-
arguments
-
rawSql
の?
の部分にバインドされるべきパラメータ値をリスト形式で保持します。 - パラメータ値の順番は、
rawSql
内の?
の順番に対応しています。
-
SqlArgument
クラス
パラメータ値を保持するクラスです。1つのパラメータの値( value
)とその型( type
)を保持しています。
使い方の例
Doma公式ドキュメントから引用しています。
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にあります。
クラス定義など
public record SqlParam<T>(String name, Class<T> valueType, T value) {
}
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メソッドを用意しておきます。
@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;
}
検索
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;
}
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;
}
単純な更新
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()
がポイント。
public record UpdateResult(int updatedRows, Number key) {
}
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