SQLビルダークラス

問題点

Java開発者が行う中で最も厄介な作業の1つは、JavaコードにSQLを埋め込むことです。通常、これはSQLを動的に生成する必要があるために行われます。そうでなければ、ファイルまたはストアドプロシージャに外部化できます。すでに見たように、MyBatisはXMLマッピング機能で動的SQL生成に対する強力なソリューションを提供しています。しかし、Javaコード内でSQLステートメント文字列を構築する必要がある場合もあります。その場合、MyBatisには、プラス記号、引用符、改行、フォーマットの問題、余分なコンマやAND結合を処理するためのネストされた条件などを扱う典型的な混乱に陥る前に役立つもう1つの機能があります。実際、JavaでSQLコードを動的に生成することは、まさに悪夢になる可能性があります。例えば

String sql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, "
"P.LAST_NAME,P.CREATED_ON, P.UPDATED_ON " +
"FROM PERSON P, ACCOUNT A " +
"INNER JOIN DEPARTMENT D on D.ID = P.DEPARTMENT_ID " +
"INNER JOIN COMPANY C on D.COMPANY_ID = C.ID " +
"WHERE (P.ID = A.ID AND P.FIRST_NAME like ?) " +
"OR (P.LAST_NAME like ?) " +
"GROUP BY P.ID " +
"HAVING (P.LAST_NAME like ?) " +
"OR (P.FIRST_NAME like ?) " +
"ORDER BY P.ID, P.FULL_NAME";

解決策

MyBatis 3は、この問題を解決する便利なユーティリティクラスを提供します。SQLクラスを使用すると、インスタンスを作成して、そのインスタンスに対してメソッドを呼び出し、SQLステートメントを一度に1ステップずつ構築できます。上記の例題は、SQLクラスで書き直すと次のようになります。

private String selectPersonSql() {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
    SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
    FROM("PERSON P");
    FROM("ACCOUNT A");
    INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
    INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
    WHERE("P.ID = A.ID");
    WHERE("P.FIRST_NAME like ?");
    OR();
    WHERE("P.LAST_NAME like ?");
    GROUP_BY("P.ID");
    HAVING("P.LAST_NAME like ?");
    OR();
    HAVING("P.FIRST_NAME like ?");
    ORDER_BY("P.ID");
    ORDER_BY("P.FULL_NAME");
  }}.toString();
}

この例で何がそれほど特別なのでしょうか?よく見ると、「AND」キーワードを誤って複製したり、「WHERE」と「AND」のどちらか、または何も選択する必要がありません。SQLクラスは、「WHERE」をどこに配置する必要があるか、「AND」をどこに使用する必要があるか、そしてすべての文字列連結を理解する役割を担います。

SQLクラス

いくつかの例を以下に示します。

// Anonymous inner class
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

// Builder / Fluent style
public String insertPersonSql() {
  String sql = new SQL()
    .INSERT_INTO("PERSON")
    .VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
    .VALUES("LAST_NAME", "#{lastName}")
    .toString();
  return sql;
}

// With conditionals (note the final parameters, required for the anonymous inner class to access them)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
  return new SQL() {{
    SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
    FROM("PERSON P");
    if (id != null) {
      WHERE("P.ID like #{id}");
    }
    if (firstName != null) {
      WHERE("P.FIRST_NAME like #{firstName}");
    }
    if (lastName != null) {
      WHERE("P.LAST_NAME like #{lastName}");
    }
    ORDER_BY("P.LAST_NAME");
  }}.toString();
}

public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

public String insertPersonSql() {
  return new SQL() {{
    INSERT_INTO("PERSON");
    VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
    VALUES("LAST_NAME", "#{lastName}");
  }}.toString();
}

public String updatePersonSql() {
  return new SQL() {{
    UPDATE("PERSON");
    SET("FIRST_NAME = #{firstName}");
    WHERE("ID = #{id}");
  }}.toString();
}
メソッド 説明
  • SELECT(String)
  • SELECT(String...)
SELECT句を開始するか、追加します。複数回呼び出すことができ、パラメータはSELECT句に追加されます。パラメータは通常、コンマで区切られた列とエイリアスのリストですが、ドライバで許容されるものであれば何でもかまいません。
  • SELECT_DISTINCT(String)
  • SELECT_DISTINCT(String...)
SELECT句を開始するか、追加し、生成されたクエリにDISTINCTキーワードも追加します。複数回呼び出すことができ、パラメータはSELECT句に追加されます。パラメータは通常、コンマで区切られた列とエイリアスのリストですが、ドライバで許容されるものであれば何でもかまいません。
  • FROM(String)
  • FROM(String...)
FROM句を開始するか、追加します。複数回呼び出すことができ、パラメータはFROM句に追加されます。パラメータは通常、テーブル名とエイリアス、またはドライバで許容されるものであれば何でもかまいません。
  • JOIN(String)
  • JOIN(String...)
  • INNER_JOIN(String)
  • INNER_JOIN(String...)
  • LEFT_OUTER_JOIN(String)
  • LEFT_OUTER_JOIN(String...)
  • RIGHT_OUTER_JOIN(String)
  • RIGHT_OUTER_JOIN(String...)
呼び出されたメソッドに応じて、適切なタイプの新しいJOIN句を追加します。パラメータには、列と結合条件を含む標準的な結合を含めることができます。
  • WHERE(String)
  • WHERE(String...)
ANDで連結された新しいWHERE句条件を追加します。複数回呼び出すことができ、そのたびに新しい条件をANDで連結します。OR()を使用してORで分割します。
OR() 現在のWHERE句条件をORで分割します。複数回呼び出すことができますが、連続して複数回呼び出すと、不安定なSQLが生成されます。
AND() 現在のWHERE句条件をANDで分割します。複数回呼び出すことができますが、連続して複数回呼び出すと、不安定なSQLが生成されます。WHEREHAVINGはどちらも自動的にANDで連結されるため、これは非常にまれに使用されるメソッドであり、完全性のために含まれているだけです。
  • GROUP_BY(String)
  • GROUP_BY(String...)
コンマで連結された新しいGROUP BY句要素を追加します。複数回呼び出すことができ、そのたびに新しい条件をコンマで連結します。
  • HAVING(String)
  • HAVING(String...)
ANDで連結された新しいHAVING句条件を追加します。複数回呼び出すことができ、そのたびに新しい条件をANDで連結します。OR()を使用してORで分割します。
  • ORDER_BY(String)
  • ORDER_BY(String...)
コンマで連結された新しいORDER BY句要素を追加します。複数回呼び出すことができ、そのたびに新しい条件をコンマで連結します。
  • LIMIT(String)
  • LIMIT(int)
LIMIT句を追加します。このメソッドは、SELECT()、UPDATE()、DELETE()と併用する場合に有効です。また、SELECT()と併用する場合はOFFSET()と併用するように設計されています。(3.5.2以降で使用可能)
  • OFFSET(String)
  • OFFSET(long)
OFFSET句を追加します。このメソッドは、SELECT()と併用する場合に有効です。また、LIMIT()と併用するように設計されています。(3.5.2以降で使用可能)
  • OFFSET_ROWS(String)
  • OFFSET_ROWS(long)
OFFSET n ROWS句を追加します。このメソッドは、SELECT()と併用する場合に有効です。また、FETCH_FIRST_ROWS_ONLY()と併用するように設計されています。(3.5.2以降で使用可能)
  • FETCH_FIRST_ROWS_ONLY(String)
  • FETCH_FIRST_ROWS_ONLY(int)
FETCH FIRST n ROWS ONLY句を追加します。このメソッドは、SELECT()と併用する場合に有効です。また、OFFSET_ROWS()と併用するように設計されています。(3.5.2以降で使用可能)
DELETE_FROM(String) 削除ステートメントを開始し、削除するテーブルを指定します。通常、これはWHEREステートメントに続く必要があります!
INSERT_INTO(String) 挿入ステートメントを開始し、挿入するテーブルを指定します。これには、1つ以上のVALUES()またはINTO_COLUMNS()とINTO_VALUES()の呼び出しが続く必要があります。
  • SET(String)
  • SET(String...)
更新ステートメントの「set」リストに追加します。
UPDATE(String) 更新ステートメントを開始し、更新するテーブルを指定します。これには、1つ以上のSET()呼び出し、そして通常はWHERE()呼び出しが続く必要があります。
VALUES(String, String) 挿入ステートメントに追加します。最初の引数は挿入する列、2番目の引数は値です。
INTO_COLUMNS(String...) 挿入ステートメントに列句を追加します。これはINTO_VALUES()と併せて呼び出す必要があります。
INTO_VALUES(String...) 挿入ステートメントに値句を追加します。これはINTO_COLUMNS()と併せて呼び出す必要があります。
ADD_ROW() 一括挿入用の新しい行を追加します。(3.5.2以降で使用可能)

注意 SQLクラスは、LIMITOFFSETOFFSET n ROWSFETCH FIRST n ROWS ONLY句を生成されたステートメントにそのまま書き込むことに注意することが重要です。つまり、ライブラリは、これらの句を直接サポートしていないデータベースに対してこれらの値を正規化しようとしません。したがって、ターゲットデータベースがこれらの句をサポートしているかどうかをユーザーが理解することが非常に重要です。ターゲットデータベースがこれらの句をサポートしていない場合、このサポートを使用すると、ランタイムエラーが発生するSQLが作成される可能性が高くなります。

3.4.2以降、可変長引数を次のように使用できます。

public String selectPersonSql() {
  return new SQL()
    .SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
    .FROM("PERSON P", "ACCOUNT A")
    .INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
    .WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
    .ORDER_BY("P.ID", "P.FULL_NAME")
    .toString();
}

public String insertPersonSql() {
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{id}", "#{fullName}")
    .toString();
}

public String updatePersonSql() {
  return new SQL()
    .UPDATE("PERSON")
    .SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
    .WHERE("ID = #{id}")
    .toString();
}

3.5.2以降、一括挿入用の挿入ステートメントを次のように作成できます。

public String insertPersonsSql() {
  // INSERT INTO PERSON (ID, FULL_NAME)
  //     VALUES (#{mainPerson.id}, #{mainPerson.fullName}) , (#{subPerson.id}, #{subPerson.fullName})
  return new SQL()
    .INSERT_INTO("PERSON")
    .INTO_COLUMNS("ID", "FULL_NAME")
    .INTO_VALUES("#{mainPerson.id}", "#{mainPerson.fullName}")
    .ADD_ROW()
    .INTO_VALUES("#{subPerson.id}", "#{subPerson.fullName}")
    .toString();
}

3.5.2以降、検索結果行の制限句を含むSELECTステートメントを次のように作成できます。

public String selectPersonsWithOffsetLimitSql() {
  // SELECT id, name FROM PERSON
  //     LIMIT #{limit} OFFSET #{offset}
  return new SQL()
    .SELECT("id", "name")
    .FROM("PERSON")
    .LIMIT("#{limit}")
    .OFFSET("#{offset}")
    .toString();
}

public String selectPersonsWithFetchFirstSql() {
  // SELECT id, name FROM PERSON
  //     OFFSET #{offset} ROWS FETCH FIRST #{limit} ROWS ONLY
  return new SQL()
    .SELECT("id", "name")
    .FROM("PERSON")
    .OFFSET_ROWS("#{offset}")
    .FETCH_FIRST_ROWS_ONLY("#{limit}")
    .toString();
}

SqlBuilderとSelectBuilder(非推奨)

3.2より前のバージョンでは、ThreadLocal変数を使用して、Java DSLをやや面倒にする言語の制限の一部をマスクするという少し異なるアプローチをとっていました。しかし、このアプローチは現在非推奨です。最新のフレームワークでは、このようなものに対してビルダータイプのパターンと匿名内部クラスを使用するという考え方に人が慣れてきたためです。そのため、SelectBuilderとSqlBuilderクラスは非推奨になりました。

次のメソッドは、非推奨のSqlBuilderとSelectBuilderクラスのみに適用されます。

メソッド 説明
BEGIN() / RESET() これらのメソッドは、SelectBuilderクラスのThreadLocal状態をクリアし、新しいステートメントの構築を準備します。BEGIN()は、新しいステートメントを開始する場合に最適です。RESET()は、何らかの理由で実行中にステートメントをクリアする場合(おそらく、ロジックが特定の条件下で完全に異なるステートメントを要求する場合)に最適です。
SQL() これは生成されたSQL()を返し、SelectBuilderの状態をリセットします(BEGIN()またはRESET()が呼び出された場合と同様)。したがって、このメソッドは一度だけ呼び出すことができます!

SelectBuilderとSqlBuilderクラスは魔法ではありませんが、それらがどのように機能するかを知ることは重要です。SelectBuilderとSqlBuilderは、静的インポートとThreadLocal変数の組み合わせを使用して、条件と簡単にインターレースできるクリーンな構文を有効にします。それらを使用するには、次のようにクラスからメソッドを静的にインポートします(両方ではなく、どちらか一方)。

import static org.apache.ibatis.jdbc.SelectBuilder.*;
import static org.apache.ibatis.jdbc.SqlBuilder.*;

これにより、次のようなメソッドを作成できます。

/* DEPRECATED */
public String selectBlogsSql() {
  BEGIN(); // Clears ThreadLocal variable
  SELECT("*");
  FROM("BLOG");
  return SQL();
}
/* DEPRECATED */
private String selectPersonSql() {
  BEGIN(); // Clears ThreadLocal variable
  SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
  SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
  FROM("PERSON P");
  FROM("ACCOUNT A");
  INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
  INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
  WHERE("P.ID = A.ID");
  WHERE("P.FIRST_NAME like ?");
  OR();
  WHERE("P.LAST_NAME like ?");
  GROUP_BY("P.ID");
  HAVING("P.LAST_NAME like ?");
  OR();
  HAVING("P.FIRST_NAME like ?");
  ORDER_BY("P.ID");
  ORDER_BY("P.FULL_NAME");
  return SQL();
}