JDBC APIにおけるResultSet操作、PreparedStatement実行、トランザクション管理、RowSet実装の総合ガイド

ResultSetインターフェースの構造とデータ取得

データベースへのクエリ実行によって生成されるデータグリッドを表現するのがResultSetです。このオブジェクトは内部的なカーソル機構を持ち、初期状態では第一行の手前に位置しています。next()メソッドを呼び出すことで一行ずつ前方へ移動し、各行の列データを取得する仕組みになっています。

以下の例は、コネクション経由でテーブルの内容を取得し標準出力へ表示する基本的なパターンです。

public void fetchCoffees(Connection conn) throws SQLException {
    String sql = "SELECT COF_NAME, SUPPLIER_ID, UNIT_PRICE, SALES_COUNT, TOTAL_STOCK FROM PRODUCTS";
    try (Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(sql)) {

        while (rs.next()) {
            String name = rs.getString("COF_NAME");
            int vendorId = rs.getInt("SUPPLIER_ID");
            double price = rs.getDouble("UNIT_PRICE");
            int soldQty = rs.getInt("SALES_COUNT");
            int stockQty = rs.getInt("TOTAL_STOCK");

            System.out.printf("%s | Vendor:%d | Price:%.2f | Sold:%d | Stock:%d%n",
                              name, vendorId, price, soldQty, stockQty);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

型・同時実行制御・保持性の定義

ResultSetの動作範囲は、Statement作成時に指定する定数によって決定されます。

  • ResultSet.TYPE_FORWARD_ONLY:デフォルト。カーソルの後退や絶対位置への移動が不可。
  • ResultSet.TYPE_SCROLL_INSENSITIVE:前後移動可能。ただしオープン後にソース側で行われた変更は反映されない。
  • ResultSet.TYPE_SCROLL_SENSITIVE:スクロール可能で、アクティブ中に発生した更新も検知して反映する。
  • CONCUR_READ_ONLY:読み取り専用。ResultSet内部からの更新は不可能。
  • CONCUR_UPDATABLE:更新・挿入・削除を含む変更操作が可能。

トランザクションコミット時の動作も制御可能です。HOLD_CURSORS_OVER_COMMITを設定すればコミット後もカーソルが維持され、CLOSE_CURSOR_AT_COMMITにすると自動的にクローズされます。これらのサポート状況はDatabaseMetaDataを通じてDriverごとに確認できます。

カーソル操作とカラム値の取得

通常はインデックス(1から開始)または列名で値を取得しますが、インデックス指定の方がパフォーマンス面で有利な場合が多いです。また、異なるSQL型間の変換が必要な際も適切 Getter/Setter を選択することが推奨されます。

// インデックスを使用した取得例
String productName = rs.getString(1);
double currentPrice = rs.getDouble(3);

ResultSetの更新処理とバッチ実行

同時実行制御がCONCUR_UPDATABLEに設定された場合、メモリ上の行に対して直接書き込みが可能です。対象行へ移動後、updateXxx()系メソッドで値を変更し、最終的にupdateRow()を発行することでデータベース側に反映されます。

try (Statement stmt = conn.createStatement(
        ResultSet.TYPE_SCROLL_SENSITIVE, 
        ResultSet.CONCUR_UPDATABLE);
     ResultSet updatableRs = stmt.executeQuery("SELECT * FROM PRODUCTS")) {
    
    while (updatableRs.next()) {
        double oldPrice = updatableRs.getDouble("UNIT_PRICE");
        double adjustedPrice = oldPrice * 1.05; // 5%値上げ
        
        updatableRs.updateDouble("UNIT_PRICE", adjustedPrice);
        updatableRs.updateRow(); // DB更新へコミット
    }
}

大量のINSERT/UPDATE文を一度に送信するにはバッチ処理を利用します。自動コミットを無効化した状態でaddBatch()へコマンドを累積し、executeBatch()で一括実行します。

conn.setAutoCommit(false);
try (Statement batchStmt = conn.createStatement()) {
    batchStmt.addBatch("INSERT INTO PRODUCTS VALUES('GreenTea', 88, 4.50, 0, 50)");
    batchStmt.addBatch("INSERT INTO PRODUCTS VALUES('Oolong', 88, 5.20, 0, 60)");
    
    int[] results = batchStmt.executeBatch();
    conn.commit(); // 全件成功時のみ確定
    
    // 失敗があった場合はBatchUpdateExceptionが発生するためキャッチして処理
} catch (SQLException ex) {
    conn.rollback();
    throw ex;
} finally {
    conn.setAutoCommit(true);
}

PreparedStatementによる安全性と効率化

動的に構築されるSQL文ではSQLインジェクションリスクが存在します。PreparedStatementはパラメータプレースホルダー(?)を用い、コンパイル段階で構文解析が完了するため、実行時は最適化済みの計画に沿って処理が走ります。これにより攻撃ベクトルが遮断され、同一クエリの再利用時もオーバーヘッドが削減されます。

public void updateWeeklyMetrics(Connection conn, Map salesData) throws SQLException {
    String sqlSales = "UPDATE PRODUCTS SET SALES_COUNT = ? WHERE COF_NAME = ?";
    String sqlTotal = "UPDATE PRODUCTS SET TOTAL_STOCK = TOTAL_STOCK + ? WHERE COF_NAME = ?";
    
    conn.setAutoCommit(false);
    try (PreparedStatement psSales = conn.prepareStatement(sqlSales);
         PreparedStatement psTotal = conn.prepareStatement(sqlTotal)) {
        
        for (Map.Entry entry : salesData.entrySet()) {
            psSales.setInt(1, entry.getValue());
            psSales.setString(2, entry.getKey());
            psSales.executeUpdate();
            
            psTotal.setInt(1, entry.getValue());
            psTotal.setString(2, entry.getKey());
            psTotal.executeUpdate();
        }
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}

トランザクション管理と隔離レベル

複数の更新操作中に中途半端な状態が外部へ漏れないよう、トランザクション単位で制御する必要があります。setAutoCommit(false)により自動確定を停止し、正常終了時にcommit()、異常時にはrollback()を実行します。

並行アクセス時の整合性を保つため、DBMSはロック機構やバージョンチェックを採用します。JDBCでは5つの隔離レベルが定義されており、要求される整合性とパフォーマンスのトレードオフに応じてConnection.setTransactionIsolation()で設定できます。

  • TRANSACTION_READ_UNCOMMITTED:アンコミット読込み許可(最も低速だが競合少ない)
  • TRANSACTION_READ_COMMITTED:通常のDBデフォルト。ダーティリード防止
  • TRANSACTION_REPEATABLE_READ:非重複読出し防止。幻読込みは許可
  • TRANSACTION_SERIALIZABLE:完全逐次実行。最高安定性だがスループット低下

大規模更新の一部だけを取り消すにはセーブポイント機能を使います。

Savepoint mark = conn.setSavepoint("before_update");
// 更新処理...
if (newPrice > limit) {
    conn.rollback(mark); // 該当範囲のみ元に戻す
}

RowSetアーキテクチャとJavaBeans統合

ResultSetはネットワーク接続中のみ使用可能でしたが、RowSetはJavaBeans仕様に準拠し、イベント通知メカニズムやシリアライズ機能を提供します。接続持続の有無によって分類され、それぞれ特定のユースケースに最適化されています。

JdbcRowSet:接続常駐型のスマートラッパー

従来のResultSetを包み込む形でスクロール性と更新性を付与します。内部でDriverManagerと継続的なリンクを維持するため、GUIツールや軽量クライアントとの連携に適しています。

try (JdbcRowSet jrs = RowSetProvider.newFactory().createJdbcRowSet()) {
    jrs.setUrl("jdbc:mysql://localhost:3306/mydb");
    jrs.setUsername("user");
    jrs.setPassword("pass");
    jrs.setCommand("SELECT * FROM PRODUCTS");
    jrs.execute();
    
    // next(), previous(), absolute() 等で自由なナビゲーションが可能
    // jrs.updateDouble("UNIT_PRICE", 12.99); jrs.updateRow(); で即時DB反映
}

CachedRowSet:オフライン編集と同期処理

データ取得後は接続を解除し、メモリ上にスナップショットを保持します。改変が完了した後、acceptChanges()呼び出しでバックエンドへ一括アップロードします。この際、他セッションによる併用変更(競合)を検知するSyncProviderが裏で稼働し、衝突発生時はSyncResolver経由で解決策を選択できます。

CachedRowSet crs = RowSetProvider.newFactory().createCachedRowSet();
crs.setUsername(user); crs.setPassword(pw); crs.setUrl(url);
crs.setCommand("SELECT ITEM_CODE, NAME, QUANTITY FROM INVENTORY");
crs.execute();

// オフラインでデータ加工
while (crs.next()) {
    if (crs.getInt("QUANTITY") == 0) crs.deleteRow();
}

// バックエンドへ差分を反映
try {
    crs.acceptChanges(conn);
} catch (SyncProviderException spe) {
    SyncResolver resolver = spe.getSyncResolver();
    // resolver.getStatus() と setResolvedValue() で衝突回避
}

JoinRowSetとFilteredRowSet:切断状態でのデータ操作

両者ともCachedRowSetの拡張仕様です。JoinRowSetは複数レコードセット間のSQL JOIN演算をローカルで完結させます。共通キーとなる列(マッチングカラム)を指定し、addRowSet(source, keyCol)で結合体を構築します。

JoinRowSet joinSet = factory.createJoinRowSet();
joinSet.addRowSet(catalogRs, "VENDOR_ID");
joinSet.addRowSet(pricingRs, "VENDOR_ID");

while (joinSet.next()) {
    // 結合済みデータを参照可能
}

FilteredRowSetはPredicateインターフェースを実装したフィルタクラスを適用し、表示行数を制限します。サーバー負荷を軽減しつつ関連情報のみ抽出したい場合に有効です。

class RegionFilter implements Predicate {
    public boolean evaluate(RowSet rs) throws SQLException {
        return "CA".equals(rs.getString("STORE_REGION"));
    }
}
frs.setFilter(new RegionFilter()); // CA店舗のみ可視化

WebRowSet:XML直列化に対応した通信モジュール

Webサービス間でデータ交換する際、自身をXMLドキュメントへ変換する機能が備わっています。writeXml()でストリームへ出力し、受信側はreadXml()で復元します。現在の値だけでなく変更前の生値(Original Value)もメタデータとして保持するため、同期時の差分認識や監査用途にも適しています。

WebRowSet priceFeed = factory.createWebRowSet();
priceFeed.setUrl(...); priceFeed.setCommand("SELECT NAME, PRICE FROM LIST");
priceFeed.execute();

try (FileOutputStream fos = new FileOutputStream("catalog.xml")) {
    priceFeed.writeXml(fos);
}

タグ: JDBC resultset PreparedStatement connectionpool transactionisolation

7月12日 20:28 投稿