tamuraです。 DBUtils3のバージョンアップを行いました。
今回は
select
*
from
t_tbl
where
id = :id
のようにSQLを定義して
Param param = new Param();
param.put("id", 123);
List<ResultBean> result = conn.executeQueryWithParam(ResultBean.class, param);
のように名前付きパラメータで値を指定できるようにしました。
きっかけ
- マスタテーブルが5種類ある
- それぞれに有効期限がある
- そのマスタと結合させて情報を抜く
というSQLを何個も書いていました。こんな感じのSQLになります。
select
....
from
t_xxxx trn
m_aaaa m1,
m_bbbb m2,
m_cccc m3,
m_dddd m4
where
...
and
m1.start_ymd <= ? and ? < m1.end_ymd
and
m2.start_ymd <= ? and ? < m2.end_ymd
and
m3.start_ymd <= ? and ? < m3.end_ymd
and
m4.start_ymd <= ? and ? < m4.end_ymd
DBUtilsでSQLを発行するとこういうコードになります。
conn.prepare(selectSQL);
List<ResultBean> result = conn.executeQuery(ResultBean.class,
...,
...,
rangeStartYmd, rangeEndYmd,
rangeStartYmd, rangeEndYmd,
rangeStartYmd, rangeEndYmd,
rangeStartYmd, rangeEndYmd);
どの?
に対するパラメータなのか分かりにくくなります。
実装方法
JDBIの実装を参考にして、ANTLRを使って字句解析をしています。 ANTLRのサンプルとしてMySQLのクエリパーサがあったのですが、今回の内容はそこまで複雑ではありません。 また、SQLとしておかしい構文であってもそれはデータベースがおかしいクエリと判断すれば良いため、このツールでは字句解析のみを行っています。
字句解析
字句解析の一部はこんな感じです。
:a
や:a0
などを見つけたらNAMED_PARAM
として呼び出し元に返します。
:0
や:0a
はCOLON
,NUMBER
,ALPHA
として返されます。
ALPHA
: 'a'..'z'
| 'A'..'Z'
| '_'
;
NUMBER
: '0'..'9'
;
COLON
: ':'
;
NAMED_PARAM
: COLON ALPHA (ALPHA | NUMBER)*
;
呼び出し元はNAMED_PARAM
が返されたら?
を詰め込む、それ以外が返ってきたら解析された字句をそのまま詰め込む、とすればパラメータを置換してprepare
にそのまま渡せる文字列ができあがります。
SQLの解析
ANTLRで作ったLexerでSQLの字句解析を行います。
- NAMED_PARAMの場合
- 名前付きパラメータから名前を取得して、リストに詰め込む -組み立て中のSQLは?を入れる
NAMED_PARAM以外の場合
組み立て中のSQLに返ってきた字句をそのまま入れる
// 何番目の?がなんというパラメータだったかを保持するリスト List<String> keyNames; public String analyzeSQL(String sql) { // ANTLRで生成したLexer SQLLexer l = new SQLLexer(new ANTLRInputStream(sql)); // SQLを生成する StrinbBuilder buf = new StringBuilder(); keyNames = new List<String>(); Token token = l.nextToken(); while(token.getType() != SQLLexer.EOF) { switch (token.getType()) { case SQLLexer.NAMED_PARAM: // 先頭のコロンを除去 keyNames.add(token.getText().substring(1)); // 名前付きパラメータを?に置換 buf.append("?"); break; default: // そのまま詰め込む buf.append(token.getText()); } token = l.nextToken(); } return buf.toString(); }
このメソッドに名前付きパラメータで記述したSQLを渡すと、通常の prepare
に渡す形式のSQLに変換します。
select
*
from
t_tbl
where
id = :id
これが
select
*
from
t_tbl
where
id = ?
こうなって、
keyNames:["id"]
こうなります。
SQLの実行
Map
みたいなクラスを作ってそこにキーと値をセットします。
public class Param {
private Map<String, Object> param;
public Param() {
param = new HashMap<>();
}
public Param put(String key, Object value) {
param.put(key, value);
return this;
}
public Object get(String key) {
return param.get(key);
}
}
put
でParam
を返しているのはこんなことをやりたいためです。
Param param = new Param().put("id", 123).put("name", "tamura");
このクラスで名前とそのパラメータをセットします。
executeQuery
等に渡すため、このパラメータをObject[]
に変換する必要があります。
public Object[] createParams(Param param) {
List<Object> p = new ArrayList<>(keyNames.size());
for (String key: keyNames) {
p.add(param.get(key));
}
return (Object[])p.toArray(new Object[]{});
}
最後に、今までのexecuteQuery
に横流しします。
public <T> List<T> executeQueryWithParam(Class<T> cls, Params params) throws SQLException {
Object[] p = createParams(params);
return executeQuery(cls, p);
}
まとめ
<dependency>
<groupId>com.github.tamurashingo.dbutil3</groupId>
<artifactId>dbutil3</artifactId>
<version>0.2.0</version>
</dependency>
で使うことができます。