DBUtils3 version 0.2.0

tamuraです。 DBUtils3のバージョンアップを行いました。

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:0aCOLON,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);
    }
}

putParamを返しているのはこんなことをやりたいためです。

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>

で使うことができます。

関連記事

comments powered by Disqus