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);
}