0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java直書きSQL - いまさらながら Javaフレームワークを自作した(10年ぶり3回目) #10

0
Last updated at Posted at 2026-03-07

はじめに

これまでのプロジェクトで私は SQL を XML に書いたり Java に書いたり、エンティティクラスに埋め込んだりさまざまなパターンを経験してきました。どの方式にも一長一短があり、特に次の点が悩みどころでした。

  • 検索条件が動的になると、XML の <if> タグが増えて読みにくくなる
  • バッチ処理でループ内に INSERT/UPDATE が入ると、性能が気になる

SIcore では、この悩みへの回答として 2種類の SQL構築クラス を用意しています。

  • SqlBuilder — 動的に組み立てる SQL(おもにWebサービスの検索処理)
  • SqlConst — 静的(固定)な SQL(おもにバッチ処理)

この記事では両者の紹介と、使い分けの考え方を解説します。

この記事で書くこと

  • XML によるSQL管理の面倒くさいところ
  • SqlBuilder — 動的SQL組み立ての仕組みと特徴
  • SqlConst — 静的SQL定義とバインド型安全
  • SqlConst のプリペアードステートメントキャッシュ
  • Java Text Block(""")との組み合わせ

XMLによるSQL管理の面倒くさいところ

MyBatis(旧 iBatis)のようなO/Rマッパーでは、SQLをXMLファイルに記述します。

<select id="searchUsers" resultType="map">
  SELECT u.user_id, u.user_nm, u.email
  FROM t_user u
  WHERE 1=1
  <if test="userId != null and userId != ''">
    AND u.user_id = #{userId}
  </if>
  <if test="userNm != null and userNm != ''">
    AND u.user_nm LIKE '%' || #{userNm} || '%'
  </if>
  ORDER BY u.user_id
</select>

内容自体は読めますが、使えるようになるまでの手間がいくつかあります。

  • SQL がロジックとは別ファイル(XML)に分離されるため、どこで使われているか追いにくい。Java 側では searchUsers のような ID文字列で紐づけることになり、リネームや削除のときにコンパイルエラーにならない
  • <if test="..."> などの XMLタグ記法を覚える必要がある
  • XMLファイルとJavaクラスを行き来することになる
  • #{}${} の違いを理解しなければSQLインジェクションの罠に落ちる(${} は値を直接埋め込む)

特に「動的SQL」の面倒さは顕著です。条件が増えるほどタグが連なって読みにくくなります。あと地味に嫌なのが < > をエスケープ文字で記載しないとだめなところ。

SIcore は SQLインジェクション対策済み の Java ライブラリでSQLを組み立てる方式を選びました。

SqlBuilder — 動的SQLを組み立てる

SqlBuilder は、SQLとバインドパラメーターを同時に組み立てるクラスです。おもに Webサービスの 検索処理 で使い、条件項目に値があるときだけ WHERE 句に追加する動的SQLに向いています。

基本的な使い方

final SqlBuilder sb = new SqlBuilder();
sb.addQuery("SELECT ");
sb.addQuery("  u.user_id ");
sb.addQuery(", u.user_nm ");
sb.addQuery(", u.email ");
sb.addQuery(" FROM t_user u ").addQuery(" WHERE 1=1 ");  // メソッドチェーンも可

// 値がブランクでない場合のみ WHERE 条件を追加
sb.addQnotB("   AND u.user_id = ? ", io.getString("user_id"));
sb.addQnotB("   AND u.user_nm LIKE '%' || ? || '%' ", io.getString("user_nm"));
sb.addQnotB("   AND u.email LIKE ? || '%' ", io.getString("email"));
sb.addQnotB("   AND u.income_am >= ? ", io.getBigDecimalNullable("income_am"));
sb.addQnotB("   AND u.birth_dt = ? ", io.getDateNullable("birth_dt"));

sb.addQuery(" ORDER BY u.user_id ");

// DB抽出実行
final IoRows rows = SqlUtil.selectBulk(getDbConn(), sb, 5);

addQnotBQuery if not Blank)は、パラメーターが null またはブランクのとき SQL を追加しません。これが動的SQL組み立ての核心部分です。

XML の <if test="..."> と違い、条件の有無にかかわらずコードの見た目は変わりません。addQueryaddQnotB が均一に並ぶだけなので、SQL の構造がコードから直接読み取れます。

複雑な条件分岐が必要な場合は、addQnotB にこだわらず Java の if 文と addQuery を組み合わせることもできます。通常の Java コードとして書けるので、デバッグもしやすく AI にとっても扱いやすいコードになります。

バインド変数(?)を使用しているため、SQLインジェクション対策も万全です。

IN句の可変プレースホルダー

MyBatis で IN句を書くには <foreach> タグが必要で、知らないと意外と手間がかかります。addListInBind を使えば、リストを渡すだけで ? が自動的に展開されます。

final List<Object> statusList = new ArrayList<>();
statusList.add("A");
statusList.add("B");
statusList.add("C");

sb.addQuery(" AND u.status_cs IN (");
sb.addListInBind(statusList);
sb.addQuery(")");
// 生成されるSQL: AND u.status_cs IN (?,?,?)
// リストの要素数に応じて ? が展開され、各値はバインドパラメーターとして渡される

主要メソッド一覧

メソッド 概要
addQuery(sql) SQL のみ追加
addQuery(sql, params...) SQL&パラメーター追加(複数パラメーター可)
addQnotB(sql, param) param が null・ブランクでなければ SQL&パラメーター追加
addParams(params...) パラメーターのみ追加
addListInBind(list) IN句用の可変プレースホルダーとパラメーターを追加
addSqlBuilder(other) 別の SqlBuilder を統合

SqlConst — 静的SQLを定義する

SqlConst は、変化しない固定SQL を定義するためのクラスです。バッチ処理のように同じ SQL を繰り返し実行する場面で特に力を発揮します。

基本的な使い方(クラスフィールドに宣言)

private static final SqlConst SQL_INS_USER = SqlConst.begin()
    .addQuery("INSERT INTO t_user ( ")
    .addQuery("  user_id ")
    .addQuery(", user_nm ")
    .addQuery(", email ")
    .addQuery(", income_am ")
    .addQuery(", birth_dt ")
    .addQuery(" ) VALUES ( ")
    .addQuery("  ? ", "user_id",   BindType.STRING)
    .addQuery(", ? ", "user_nm",   BindType.STRING)
    .addQuery(", ? ", "email",     BindType.STRING)
    .addQuery(", ? ", "income_am", BindType.BIGDECIMAL)
    .addQuery(", ? ", "birth_dt",  BindType.DATE)
    .addQuery(" ) ")
    .end();

begin() から始めてメソッドチェーンで SQL とバインド定義を同時に組み立て、end() でイミュータブルな SqlConst オブジェクトが生成されます。static final で宣言するため、アプリ起動時に1度だけ構築されます。

バインド型(BindType)

SQL項目とバインド型の対応は宣言時に明示します。

BindType 対応するSQL型 取得元の型
STRING VARCHAR など文字列 String
BIGDECIMAL INTEGER、NUMERIC、DECIMAL など数値全般 BigDecimal
DATE DATE(yyyyMMdd形式) java.sql.Date
TIMESTAMP TIMESTAMP(yyyyMMddHHmmssSSS形式) java.sql.Timestamp

bind(io) 実行時、各 BindType に対応する io のメソッド(getStringgetBigDecimalgetDategetTimestamp)で値を取得・変換します。

実行時のバインド

SQL実行時は bind(io) でパラメーター値マップから値を取り出し、バインド済みの SqlBean を生成して渡します。

// io(マップ)からバインドして SqlBean を生成し実行
SqlUtil.executeOne(getDbConn(), SQL_INS_USER.bind(io));

bind() 内部では、宣言時に定義したバインド項目名リストの順にマップから値を取り出し、型に合わせて変換します。宣言時に定義した順序・型が実行時にも保証されるため、型ミスや順序ミスがコード上で管理されます。

SqlConst のプリペアードステートメントキャッシュ

バッチ処理でループ内に INSERT/UPDATE が入る場合、通常の executeOne では毎回プリペアードステートメントを作成・破棄します。executeOneCache(または executeCache)を使うと、プリペアードステートメントをキャッシュして再利用し、性能を改善できます。

@Override
protected void doExecute(final IoItems io) throws Exception {
  // CSVを1行ずつ読み込んで UPSERT
  try (final CsvReader cr = new CsvReader(inputPath, CharSet.UTF8, CsvType.DQ_ALL_LF)) {
    for (final IoItems row : cr) {
      // UPDATE を試み、0件なら INSERT(UPSERT)
      if (!SqlUtil.executeOneCache(getDbConn(), SQL_UPD_USER.bind(row))) {
        SqlUtil.executeOneCache(getDbConn(), SQL_INS_USER.bind(row));
      }
    }
  }
}

キャッシュは DbConn インスタンスに紐づいて 保持されます。接続が切れるとキャッシュも消えます。

メソッド 戻り値 説明
executeOne(conn, sb) boolean 通常実行。1件更新時 true、0件時 false、複数件は例外
executeOneCache(conn, sb) boolean キャッシュ実行。SqlConst のみ対象
execute(conn, sb) int 通常実行。更新件数を返す
executeCache(conn, sb) int キャッシュ実行。更新件数を返す

SqlBuilder(動的SQL)は SQL文字列が毎回変わるためキャッシュ対象外です。キャッシュが活きるのは SqlConst のような固定 SQL だけです。

MyBatis でもキャッシュできるのでは?

MyBatis のような XML 形式のフレームワークでも、プリペアードステートメントのキャッシュ自体は利用できます。ただし JDBC ドライバによってデフォルト動作が異なるため(MySQL Connector/J はデフォルト無効、PostgreSQL はデフォルト有効など)、環境に応じた設定が必要です。また、「どの SQL がキャッシュされているか」をコードから把握する手段はなく、動的 SQL が意図せずキャッシュされてしまうこともあります。

SIcore の executeOneCache を使うと、アプリ側で明示的にキャッシュ対象の SQL を指定できます。どの JDBC ドライバでも一貫して動作し、コードを読むだけで「この SQL はキャッシュして繰り返し使う」という意図が伝わります。

Java Text Block との組み合わせ

Java 15以降(正式版)で使用できる Text Block""")を使うと、SQL を複数行の文字列リテラルとして自然に書けます。

SqlBuilder で使う場合:

sb.addQuery("""
    SELECT
      u.user_id
    , u.user_nm
    , u.email
    FROM t_user u
    WHERE 1=1
    """);
sb.addQnotB("   AND u.user_id = ? ", io.getString("user_id"));

SqlConst で使う場合:

private static final SqlConst SQL_INS_USER = SqlConst.begin()
    .addQuery("""
        INSERT INTO t_user (
          user_id
        , user_nm
        , email
        , income_am
        , birth_dt
        ) VALUES (
        """)
    .addQuery("  ? ", "user_id",   BindType.STRING)
    .addQuery(", ? ", "user_nm",   BindType.STRING)
    .addQuery(", ? ", "email",     BindType.STRING)
    .addQuery(", ? ", "income_am", BindType.BIGDECIMAL)
    .addQuery(", ? ", "birth_dt",  BindType.DATE)
    .addQuery(" ) ")
    .end();

Text Block のSQL部分と、バインド定義が必要な ? の行を分けることで、可読性を保ちながら型安全なバインドが実現できます。

ただし、フレームワーク自体は Java 11 以上を前提としているため、Text Block を使用する場合はプロジェクトの Java バージョンを確認した上で採用してください。

SqlBuilder と SqlConst の使い分け

SqlBuilder SqlConst
SQL の性質 動的(条件によって変わる) 静的(固定)
おもな用途 Webサービスの検索処理 バッチ処理のINSERT/UPDATE など
バインド型指定 不要(Object で渡せる) 必要(BindType で明示)
PSキャッシュ 不可(SQL文字列が変わるため) 可(executeOneCache 使用)
宣言場所 メソッド内 static final クラスフィールド

動的に条件が変化するなら SqlBuilder、固定SQLなら SqlConst ——この単純な基準でほぼ使い分けられます。

なお SqlUtil には、テーブル名を渡すだけで INSERT/UPDATE の SQL を DBメタ情報から自動生成するメソッドも用意されています。SQL を自分で書かずに済むシンプルな単一レコード操作にも対応しています。

おわりに

SqlBuilderSqlConst はどちらも最終的に SqlBean (SQL文字列+パラメーターリスト)を生成し SqlUtil に渡す——という同じインターフェースで動くため、呼び出し側のコードは変わりません。この統一感が、動的・静的の使い分けを迷わせない設計になっています。

SQL を Java コードで書くと、XMLを覚える必要もなく、SQLとバインドパラメーターが同じ行に並ぶため、コードの流れが追いやすくなります。

AI によるコード生成という観点でも、Java 直書きに分があります。XML 方式では、AI が Java コードを読む際に SQL が別ファイルに分離されているため、XML ファイルも参照しタグ構文を解釈しなければなりません。Java 直書きなら SQL・条件分岐・バインド値が同じファイルにまとまっているため、1ファイルを読むだけで処理全体を把握でき、正確な補完や修正提案につながります。

実際のコードはこちらで公開しています。

関連記事リンク

他の記事もぜひご覧ください!

SIcoreフレームワーク リンク

実装コードと資料はすべてこちらで公開しています。


読んでいただきありがとうございました!
❤いいね!をしていただけると励みになります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?