XMLの <if> や <where> で動的SQLを書いていると、IDEの補完が効かない、メソッド名をリネームしてもXML側のミスに気づけない、という不満が溜まってきますよね。MyBatisにはXMLを一切使わず、Javaのメソッドだけで動的SQLを組み立てる仕組みがあります。@SelectProvider / @InsertProvider / @UpdateProvider によるSqlProviderです。
この記事ではXML記法には触れず、SqlProviderでJavaコードだけから動的SQLを組み立てる方法に絞って解説し、最後にXML方式との使い分け基準を整理します。XMLタグの書き方は動的SQLタグの記事、Mapperの基本設定はMapper実装ガイドを参照してください。
@SelectProvider/@InsertProvider/@UpdateProviderの基本
SqlProvider系アノテーションは、SQLを組み立てるJavaメソッドをMapperメソッドに紐付ける仕組みです。type 属性にProviderクラス、method 属性にSQLを生成するメソッド名を指定します。
public interface UserMapper {
@SelectProvider(type = UserSqlProvider.class, method = "selectActiveUsers")
List<User> selectActiveUsers(@Param("minAge") Integer minAge);
@InsertProvider(type = UserSqlProvider.class, method = "insertUser")
int insertUser(User user);
}
Providerクラス側は、Mapperメソッドと同じ引数を受け取り、組み立てたSQL文字列を返すだけです。
public class UserSqlProvider {
public String selectActiveUsers(Integer minAge) {
StringBuilder sql = new StringBuilder(
"SELECT * FROM users WHERE status = 'ACTIVE'");
if (minAge != null) {
sql.append(" AND age >= #{minAge}");
}
return sql.toString();
}
public String insertUser(User user) {
return "INSERT INTO users (name, email) VALUES (#{name}, #{email})";
}
}
戻り値は普通のSQL文字列で構いません。#{minAge} のようなプレースホルダは @Param 名と一致していれば、XMLのときと同じようにバインドされます。@UpdateProvider も書き方はまったく同じです。
ProviderMethodResolverでmethod属性を省略する(MyBatis 3.5.1+)
MyBatis 3.5.1以降では、Providerクラスに ProviderMethodResolver を実装すると method 属性を省略できます。Mapperメソッドと同名のメソッドを自動で解決してくれるので、タイプミスでのメソッド解決失敗を減らせます。
public class UserSqlProvider implements ProviderMethodResolver {
public String selectActiveUsers(Integer minAge) {
return "SELECT * FROM users WHERE status = 'ACTIVE'"
+ (minAge != null ? " AND age >= #{minAge}" : "");
}
}
public interface UserMapper {
@SelectProvider(type = UserSqlProvider.class)
List<User> selectActiveUsers(@Param("minAge") Integer minAge);
}
メソッド名がオーバーロードしているなど命名規則から外れると解決に失敗するので、1メソッド1用途で揃えておくのが無難です。3.5.0以前ではこのインターフェースが存在しないため、method 属性は省略できません。
SqlBuilder(SQLクラス)で動的SQLを組み立てる
文字列結合は手軽ですが、ANDの付け忘れやスペースの打ち間違いに気づきにくいという弱点があります。org.apache.ibatis.jdbc.SQL クラス(俗にSqlBuilderと呼ばれます)を使うと、XMLの <where> や <set> と同じく先頭のANDを自動調整する挙動をJavaコードで得られます。
import org.apache.ibatis.jdbc.SQL;
public class UserSqlProvider {
public String selectActiveUsers(final Integer minAge, final String name) {
return new SQL() {{
SELECT("*");
FROM("users");
WHERE("status = 'ACTIVE'");
if (minAge != null) {
WHERE("age >= #{minAge}");
}
if (name != null) {
WHERE("name LIKE concat('%', #{name}, '%')");
}
}}.toString();
}
}
条件が1つも成立しなければ WHERE 自体が出力されないので、XMLの <where> タグとほぼ同じ安心感があります。条件分岐が少ないクエリなら、先ほどのStringBuilderによる文字列結合のほうがシンプルで読みやすいこともあるので、複雑さに応じて使い分けるとよいでしょう。
複数引数の受け渡し: @ParamとProviderContext
Providerメソッドの引数が2つ以上になる場合は、Mapperメソッド側で @Param を付けて名前を明示しておきます。
public interface UserMapper {
@UpdateProvider(type = UserSqlProvider.class, method = "updatePartial")
int updatePartial(@Param("id") Long id, @Param("user") User user);
}
Providerメソッドの引数名・型・順序はMapperメソッドと一致させる必要があります。ここがズレると実行時にメソッド解決エラーになるので、IDEのリネーム機能を使うときは両方を同時に変更しましょう。
Mapperのクラス名やメソッド名が欲しい場合は、引数に ProviderContext を追加すると受け取れます。型で解決されるので並び順は気にしなくて大丈夫です。
public String updatePartial(ProviderContext context, Long id, User user) {
// context.getMapperMethod() などでメソッド情報を参照できる
return "UPDATE users SET name = #{user.name} WHERE id = #{id}";
}
部分更新のUPDATE文をガード付きで組み立てる
nullのフィールドだけを更新対象から除外する部分更新は、SqlProviderが特に活躍する場面です。
public class UserSqlProvider {
public String updatePartial(final Long id, final User user) {
if (user.getName() == null && user.getEmail() == null) {
throw new IllegalArgumentException("更新対象のフィールドがありません");
}
return new SQL() {{
UPDATE("users");
if (user.getName() != null) {
SET("name = #{user.name}");
}
if (user.getEmail() != null) {
SET("email = #{user.email}");
}
WHERE("id = #{id}");
}}.toString();
}
}
SET() が一度も呼ばれないと、SQLクラスは例外を出さずに SET 句自体を省略した文字列を返してしまいます。その結果 UPDATE users WHERE id = ? という構文エラーのSQLがそのままJDBCに渡り、データベース側で構文エラーになってようやく気づく、という遠回りな失敗の仕方をします。だからこそProviderメソッドの先頭でガードし、SQLを組み立てる前に弾いておくのが安全です。
実装でつまずきやすいポイント
複数引数のProviderメソッドでは @Param の付け忘れに気づきにくく、忘れると引数名が arg0 や param1 になってProvider側の #{name} と噛み合わず実行時エラーになります。複数引数のときは必ず @Param を付ける習慣にしておきましょう。
- Providerメソッドのシグネチャ不一致 は、Mapperメソッドの引数の数・型・
@Param名とProviderメソッドが食い違うと起こり、BindingExceptionやメソッド解決エラーにつながります。 - SqlBuilderの組み立て漏れ は、前述のとおり
SET/VALUES/WHEREの呼び出し忘れがそのまま構文エラーのSQLになって現れるので、ユニットテストで実際にSQLを発行して確認しておくと安心です。
XML動的SQLとSqlProviderの使い分け基準
| 観点 | XML動的SQL | SqlProvider |
|---|---|---|
| IDE補完 | SQL拡張がないと弱い | Javaの補完がそのまま効く |
| 型安全性 | 文字列ベースで実行時まで気づけない | コンパイル時にある程度検出できる |
| リファクタリング耐性 | メソッド名変更に追従しない | IDEのリネームで一緒に追従する |
| SQLの可読性 | SQLに近い見た目で書ける | Javaのロジックに埋め込まれ複雑化しやすい |
| 向いているケース | SQLレビューを重視するチーム | 条件分岐が複雑なクエリ、Java中心のチーム |
シンプルなCRUDはXML、条件分岐が多い検索や部分更新だけSqlProviderに切り出す、という併用パターンもよく見かけます。どちらかに統一する必要はなく、クエリごとの複雑さで選べば十分です。
まとめ
@SelectProvider/@InsertProvider/@UpdateProvider を使えば、XMLを書かずにJavaのメソッドだけで動的SQLを組み立てられます。ProviderMethodResolver でmethod属性を省略し、条件分岐が複雑ならSqlBuilder、シンプルなものは文字列結合、と使い分けるのが実務的な落とし所です。
XML動的SQLの書き方を見直したくなったら動的SQLタグの記事を、Mapperの基本に戻りたくなったらMapper実装ガイドもあわせてどうぞ。