QueryObject

概要

業務システムではSQL文の構築は複雑になりがちです。 入力がなかったら条件に加えない、値が空の場合はそのカラムは更新対象としないなど、 個々を見れば単純なルールであっても、 SQL文を組み立てるソースはif文の嵐になり、 ソースを一見しただけでは何をしているのか分かりなくなり、 バインド変数と「?」との対応が取れなくなったり数が合わなくなったりと、 コーディングも大変だしバグの温床となっているのが現実です。
そこでSQL文をオブジェクトとして表現できれば、 値が空の場合は条件としないといったルールは個々のオブジェクトにまかせることにより シンプルなコーディングが可能となります。 これはQueryObjectパターンとして知られたもので、 それを簡単に使えるようにと作成したものが本ライブラリです。

サンプルケース

SQL文の組み立てでよく見かけるソースはこのようなものだと思います。
  List conditions = new ArrayList();
  StringBuffer where = new StringBuffer();
  if (orderId != null && orderId.length() > 0) {
      where.append("orderid = ?");
      conditions.add(orderId);
  }
  if (customerId != null && customerId.length() > 0) {
      if (where.length() > 0) {
          where.append(" AND ");
      }
      where.append("customerid = ?");
      conditions.add(customerId);
  }
  if (orderTypes != null && orderTypes.length > 0) {
      if (where.length() > 0) {
          where.append(" AND ");
      }
      where.append("ordertype IN (");
      String delimiter = "";
      for (String type : orderTypes) {
          where.append(delimiter);
          where.append("?");
          conditions.add(type);
          delimiter = ", ";
      }
      where.append(")");
  }
  String sql =  "SELECT orderid, customerid FROM order WHERE " + where.toString();
  PreparedStatement pst = connection.prepareStatement(sql);
  for (int i = 0; i < conditions.size(); i++) {
      pst.setObject(i + 1, conditions.get(i));
  }
  ResultSet rs = pst.executeQuery();
    ...
                

                  突っ込みどころ満載ですが、大体こんな感じでしょう。
                  これはシンプルな例ですが、条件が増える程これが巨大化していきます。
同じことをクエリオブジェクトを使ってコーディングするとこうなります。
  And criteria = new And();
  criteria.add(new Compare("orderid", "=", orderId),
          new Compare("customerid", "=", customerId),
          new In("ordertype", orderTypes));
  PrecompiledQueryVisitor visitor = new PrecompiledQueryVisitor();
  visitor.addSql("SELECT orderid, customerid FROM order WHERE ");
  criteria.accept(visitor);
  PreparedStatement pst = connection.prepareStatement(visitor.getSql());
  visitor.setParamToStatement(pst);
  ResultSet rs = pst.executeQuery();
    ...
変数がヌルだったら、空文字列だったらという判断はオブジェクトが行いますのでif文がなくなり、 バインド変数の管理もやってくれるのでかなりすっきりしたと思います。 またSQL文の構築をオブジェクトに任せる事によってSQLキャッシュのヒット率も向上するでしょう。

SQL文の作成

SqlObjectを実装するクラスのオブジェクトであれば、 それがSQL文全体であってもSQL文の一部であってもSQLを抽出することができます。 SQL文の抽出はビジタークラスが行います。 SqlObjectのaccept(QueryVisitor)メソッドでビジターオブジェクトを渡すと ビジターはそのオブジェクトとそれが内包するSqlObjectを訪れてSQLを蓄積します。
ビジタークラスにはStatement用のSqlQueryVisitorと PreparedStatement用のPrecompiledQueryVisitorの二つがあります。 SqlQueryVisitorはそのまま発行できるSQL文を抽出しますが、 PrecompiledQueryVisitorは変数を「?」に置き換えたSQL文を抽出し、 その変数を内部に蓄積して後にPreparedStatementにセットすることができます。
例を以下に示します。
    SqlObject criteria = new Compare("column", "=", "value");
    SqlQueryVisitor visitor = new SqlQueryVisitor();
    criteria.accept(visitor);
    ResultSet rs = statement.executeQuery("SELECT * FROM table WHERE " + visitor.getSql());
        => "SELECT * FROM table WHERE column = 'value'" が実行される。
    SqlObject criteria = new Compare("column", "=", "value");
    PrecompiledQueryVisitor visitor = new PrecompiledQueryVisitor();
    criteria.accept(visitor);
    PreparedStatement statement =
        connection.prepareStatement("SELECT * FROM table WHERE " + visitor.getSql());
        => "SELECT * FROM table WHERE column = ?" という内容のPreparedStatementが作成される
    visitor.setParamToStatement(statement);
        => 1番目の変数の値として "value" をセット
    ResultSet rs = statement.executeQuery();
        => SQLを実行
PreparedStatementを使用する場合でも、 ログ出力用としてSqlQueryVisitorを併用すれば SqlPlus*などで実行可能なSQL文はデバッグや障害対応に役立ちます。
    SqlObject sqlObj = ....
    PrecompiledQueryVisitor visitor = new PrecompiledQueryVisitor();
    SqlQueryVisitor logVisitor = new SqlQueryVisitor();
    sqlObj.accept(visitor);
    sqlObj.accept(logVisitor);
    log.debug(sqlLogVisitor.getSql());
    PreparedStatement statement = connection.prepareStatement(visitor.getSql());
        ...

列要素

列は検索の対象となるほか条件文の要素ともなるもので、 これを表現するクラスとしてColumn、Value、Function、Operationがあります。 メソッドで列を指定する引数がObject型のものが多く存在します。 引数がこれらクラスのオブジェクトでない場合、通常はオブジェクトをそのまま保持する Valueオブジェクトが作成されます。 但し、引数にStringオブジェクトを渡した場合は それがカラム名であるか文字列の値であるかはコンテキストにより決定されます。 カラム名と解釈される場合はColumnオブジェクトが作成され、 そうでない場合はその文字列を値とするValueオブジェクトが作成されることになります。 期待したものと異なるSQLが出力される場合は ColumnかValueオブジェクトを作成して渡してください。

Column
テーブルのカラム名を表現するクラスで通常はカラム名がそのままSQLとして出力されますが、 別名やテーブルを設定すると以下のようにカラム名が修飾されます。
  Table table = new Table("table");
  Column column = new Column("column", "alias", table);
  Criteria criteria = new Compare(column, "=", "123");
  SelectStatement select = new SelectStatement(table, column, criteria);
  SqlQueryVisitor visitor = new SqlQueryVisitor();
  select.accept(visitor);
      => SELECT table.column alias FROM table WHERE table.column = '123'
                    
Value
Objectを値として保持するクラスで、 PrecompiledQueryVisitorを受容した場合にSQLとして'?'を出力し PreparedStatementにはパラメータとしてそのObjectをセットします。
  Column variable = new Value(new Timestamp(new Date().getTime()));
  PrecompiledQueryVisitor visitor = new PrecompiledQueryVisitor();
  variable.accept(visitor);
      => ? を出力
  visitor.setParamToStatement(preparedStatement, n);
      => n番目のパラメータにタイムスタンプをセット
                    
SqlQueryVisitorの場合は値の文字表現をシングルクォートで囲った文字列が出力されます。
  Column variable = new Value(new Timestamp(new Date().getTime()));
  SqlQueryVisitor visitor = new SqlQueryVisitor();
  variable.accept(visitor);
      => '2009-02-17 17:09:47.069'
                    
但し、ObjectがNumberのサブクラスのオブジェクトである場合はシングルクォートで囲いません。
  Column variable = new Value(new BigDecimal("123.456"));
  SqlQueryVisitor visitor = new SqlQueryVisitor();
  variable.accept(visitor);
      => 123.456
                    
Valueオブジェクトはデフォルトでは値が空文字列もしくはnullの場合は SQLを出力せず、パラメータも蓄積しません。 この場合これを要素とする条件文はその条件文自体が空となります (Inは有効な要素がひとつでもあれば空にはなりません)。 INSERT文、UPDATE文のカラム値の場合は登録・更新対象から外されます。 空文字列・nullを許容するには setBlankable(boolean) か setNullable(boolean) によりフラグをセットします。 詳細はAPIドキュメントを参照してください。

Function
関数を表現します。引数をStringで渡した場合はValueオブジェクトとなりますので、 カラム名を渡したい場合はColumnオブジェクトを作成して渡してください。
  Column function = new Function("CONCAT", "value", new Column("column"));
      => CONCAT('value', column)
                    
Operation
計算式を表現し、四則演算のほかOracleの '||' なども可能です。
  Column operation = new Operation("||", new Column("column"), "STR1", "STR2");
      => column || 'STR1' || 'STR2'
                    

条件文

条件文はSQLの構築で最も複雑になりやすいものです。 Criteriaは条件文を表現する抽象クラスで、 その子クラスには比較を表現するCompareのようにひとつの条件を表すものと AndやOrのように複数の条件を保持するものがあります。

Compare
'=', '<' などの二つの要素の比較を表現します。 コンストラクタに列としてStingを渡した場合、 1番目の引数はカラム名、3番目の引数は値と解釈されます。
  Criteria comparison = new Compare("column", "=", "value");
      => column = 'value'
                
二つの要素のいずれかのSQL表現が空の場合はこの条件文全体が空となります。 空文字列を検索条件としたい場合は Valueオブジェクトを作成して空白を許容するようにセットする必要があります。
  Criteria comparison = new Compare("column", "=", "");
      => 空文字列
            
  Value value = new Value("");
  value.setBlankable(true);
  Criteria comparison = new Compare("column", "=", value);
      => column = ''
                
外部結合は、SQL Server であれば演算子に '*=' '=*' を指定します。 Oracleの場合はカラムを外部結合とする Column#setOuterJoin(boolean) を使用します。また、テーブルにも Table#setOuterJoin(boolean) メソッドがあり、 TableをColumnにセットした場合はTableの外部結合フラグを引き継ぎます。
  Table table1 = new Table("table1");
  Table table2 = new Table("table2");
  table1.setOuterJoin(true);
  Compare criteria = new Compare(
      new Column("column1", table1), "=", new Column("column2", table2));
          => table1.column1(+) = table2.column2
                
CriteriaText
CriteriaTextは条件文を表すテキストで基本的にはテキストをそのままSQLとして出力します。 オブジェクト構造をコーディングするよりテキストで書いた方が早い場合はこれを使用してください。 テキストに'?'が含まれる場合はバインド変数と解釈され、 渡された引数をSQLに埋込むかパラメータに蓄積します。 この場合はパラメータのいずれかがSQLとして空の場合にはこの条件全体が空となります。
  Criteria criteria = new CriteriaText("(column1 || '-' || column2) = ?", "9999-99");
  SqlQueryVisitor visitor = new SqlQueryVisitor();
  criteria.accept(visitor);
      => (column1 || '-' || column2) = '9999-99'
                  
Like
文字列のパターンマッチングを行います。
  Criteria like = new Like("column", Like.CONTAINS, "abc");
  SqlQueryVisitor sqlVisitor = new SqlQueryVisitor();
  like.accept(sqlVisitor);
      => column LIKE '%abc%'
  PrecompiledQueryVisitor precompiledVisitor = new PrecompiledQueryVisitor();
  like.accept(precompiledVisitor);
      => column LIKE ?
      => パラメータとして"%abc%"を保持
                   
部分一致の Like.CONTAINS のほかに前方一致の Like.STARTS_WITH と Like.ENDS_WITH を用意してあります。 それ以外のパターンを使用する場合は2番目の引数でパターンを指定します。 パターンの中に '?' があると、順に3番目以降の引数で置換していきます。
  Criteria like = new Like("column", "?_?", "abc", "def");
  SqlQueryVisitor sqlVisitor = new SqlQueryVisitor();
  like.accept(sqlVisitor);
      => column LIKE '%abc%column LIKE 'abc_def'
  PrecompiledQueryVisitor precompiledVisitor = new PrecompiledQueryVisitor();
  like.accept(precompiledVisitor);
      => column LIKE ?
      => パラメータとして"abc_def"を保持
                   
Likeのコンストラクタの1番目の引数にStringを渡した場合はカラム名と解釈されます。 またすべての要素のうちひとつでも空の要素があるとこの条件全体が空となります。

Between
範囲指定です。
  Criteria between = new Between("column", new BigDecimal("1.23"), new BigDecimal("3.45"));
      => column BETWEEN 1.23 AND 3.45
                    
IN演算子
Inのパラメータはひとつずつ設定する以外に 配列やCollection、またはSELECT文をセットすることができます。 パラメータのうち空でない要素がひとつもない場合はこの条件全体が空になります。
  String[] array = new String[] { "a", "b", "c" };
  Criteria in = new In("column", array);
      => column IN ('a', 'b', 'c')
                    
Exists
SELECT文を要素とする存在チェックです。
  SelectStatement select = new SelectStatement("table", "*");
  Criteria exists = new Exists(select);
      => EXISTS (SELECT * FROM table)
                    
IsNull
列の値がNULLであるか、もしくはNULLでないかの判断条件です。 コンストラクタの引数にStringを渡した場合はカラム名と解釈されます。
  Criteria isNull = new IsNull("column");
      => column IS NULL
                    
IS NOT NULL の場合はNotで修飾します。
  Criteria isNull = new Not(new IsNull("column"));
      => column IS NOT NULL
                    
And, Or
複数の条件文を 'AND' もしくは 'OR' で連結します。 空でない要素がひとつも存在しない場合はこの条件全体が空となり、 空でない要素がひとつだけの場合はその条件自体をSQLとして出力します。 このオブジェクトの要素として AndかOrがセットされた場合は 子要素を括弧で括ります。
  Criteria and = new And(
          new Compare("column1", "=", "value1"));
  CriteriaGroup or = new Or(
          new Compare("column2", "=", "value2"),
          new Compare("column3", "=", "value3"));
  and.add(or);
      => column1 = 'value1' AND (column2 = 'value2' OR column3 = 'value3')
                    
Not
引数で渡された条件文の先頭に 'NOT' を付加します。 必要に応じて条件文を括弧で括ります。
  Criteria orgroup = new Or(
          new Compare("column1", "=", "value1"),
          new Compare("column2", "=", "value2"));
  Criteria not = new Not(orgroup);
      => NOT (column1 = 'value1' OR column2 = 'value2')
                    

SELECT文

SELECT文は最も複雑なSQL文です。 WHERE句とHAVING句以外は 変更要素が含まれる事は稀ですから、 条件文だけにクエリオブジェクトを適用するのも良いかもしれません。

テーブルが1つの場合
引数のあるSelectStatementのコンストラクタは、1番目の引数でテーブルを、 2番目以降の引数でカラムと検索条件を指定します。 複数の検索条件を与えた場合は AND で連結されます。
  SelectStatement select =
      new SelectStatement("table",
              "column1", "column2", "column3",
              new Compare("column1", "=", "value1"),
              new Compare("column2", "= ", "value2"));
      => SELECT column1, column2, column3 FROM table WHERE column1 = 'value1' AND column2 = 'value2'
                
複数テーブルの検索
複数テーブルの検索は少し複雑になります。
  SelectStatement select = new SelectStatement();
  Table table1 = new Table("table1", "a");
  Table table2 = new Table("table2", "b");
  select.addTable(table1, table2);
  Column column1 = new Column("column1", table1);
  select.addColumn(column1);
  select.addWhere(new Compare(column1, "=", new Column("column2", table2)),
          new Compare(new Column("column3", table2), "=", "value"));
      => SELECT a.column1 FROM table1 a, table2 b WHERE a.column1 = b.column2 AND b.column3 = 'value'
                
外部結合
外部結合ではTableオブジェクトの外部結合フラグをセットします。(Oracleの場合のみ)
  Table table1 = new Table("table1");
  Table table2 = new Table("table2");
  table2.setOuterJoin(true);
  SelectStatement select = new SelectStatement();
  select.addTable(table1, table2);
  Column column1 = new Column("column1", table1);
  Column column2 = new Column("column2", table2);
  select.addColumn(column1);
  select.addWhere(new Compare(column1, "=", column2));
      => SELECT table1.column1 FROM table1, table2 WHERE table1.column1 = table2.column2(+)
                    
グループ
SelectStatement#addOrder(Object,boolean)メソッドで GROUP BY句にカラムを追加します。必要ならHAVING句に条件を追加します。
  Column column1 = new Column("column1");
  Column column2 = new Column("column2");
  Column sum = new Function("SUM", column2);
  SelectStatement select = new SelectStatement("table", column1, sum);
  select.addGroup(column1);
  select.addHaving(new Compare(sum, ">=", new Integer(5)));
      => SELECT column1, SUM(column2) FROM table GROUP BY column1 HAVING SUM(column2) >= 5
                    
ソート
SelectStatement#addOrder(Object,boolean)メソッドで ORDER BY句にカラムを追加します。 2番目の引数は昇順フラグで、falseを渡すと降順となります。
  Column column1 = new Column("column1");
  Column column2 = new Column("column2");
  SelectStatement select = new SelectStatement("table", column1, column2);
  select.addOrder(column1);
  select.addOrder(column2, false);
      => SELECT column1, column2 FROM table ORDER BY column1 ASC, column2 DESC
                    
集合演算子
Unionクラスは複数のSELECT文を集合演算子で結合します。
  SelectStatement select1 = new SelectStatement("table1", "*");
  SelectStatement select2 = new SelectStatement("table2", "*");
  Union union = new Union("UNION ALL", select1, select2);
      => SELECT * FROM table1 UNION ALL SELECT * FROM table2
                    

ダウンロード

お問い合わせ
質問、バグの報告等はmochi@simm.jp 望月までお願いします。