はじめに
本記事は、実務で必ず使う3つのテーマを深掘りします。
-
PreparedStatement― 安全で高速なSQL実行 - トランザクション管理(
commit/rollback) ― データ整合性の守り方 - 接続プーリング(HikariCP) ― 本番環境で耐えうる接続管理
この3つを理解すれば、JDBCの実務知識はほぼ完成します。
第1章:PreparedStatement
Statement との根本的な違い
Statement はSQLを毎回文字列として組み立て、DBへ送ります。PreparedStatement はSQLの骨格を先にコンパイルしておき、パラメータだけを後から渡す設計です。
【Statement の流れ】
SQL文字列を組み立て → DBへ送信 → DBがコンパイル → 実行
↑ 毎回ここで時間がかかる
【PreparedStatement の流れ】
SQLの骨格をコンパイル済みで登録 → パラメータだけ渡す → 実行
↑ 初回のみ ↑ 2回目以降はここだけ
基本的な使い方
String sql = "INSERT INTO products (name, price) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "Apple"); // 1つ目の ? に "Apple" をセット
ps.setInt(2, 150); // 2つ目の ? に 150 をセット
int count = ps.executeUpdate();
System.out.println(count + "件 挿入しました");
}
?(プレースホルダ)の番号は 1始まりです。
パラメータセットメソッド一覧
| メソッド | Javaの型 | SQL型の例 |
|---|---|---|
setString(n, val) |
String |
VARCHAR, CHAR |
setInt(n, val) |
int |
INT |
setLong(n, val) |
long |
BIGINT |
setDouble(n, val) |
double |
DOUBLE, DECIMAL |
setDate(n, val) |
java.sql.Date |
DATE |
setTimestamp(n, val) |
java.sql.Timestamp |
DATETIME |
setBoolean(n, val) |
boolean |
BOOLEAN |
setNull(n, sqlType) |
null |
NULL をセットしたい場合 |
SQLインジェクションを防ぐ仕組み
Statement で危険だったコードと、PreparedStatement での安全なコードを比較します。
// ❌ 危険:Statement での文字列連結
String input = "' OR '1'='1"; // 攻撃的な入力値
String sql = "SELECT * FROM users WHERE name = '" + input + "'";
// → SELECT * FROM users WHERE name = '' OR '1'='1'
// 全件取得されてしまう
// ✅ 安全:PreparedStatement のプレースホルダ
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, input);
// → name の値として文字通り "' OR '1'='1" が検索される
// SQLとして解釈されない
setString() 等のメソッドは、値をSQLとして解釈させないよう自動的にエスケープします。これがSQLインジェクション防止の本質です。
バッチ処理:大量データを効率的に挿入する
同じSQLを繰り返し実行する場合、addBatch() と executeBatch() を使うと DB への往復回数を減らして大幅に高速化できます。
String sql = "INSERT INTO products (name, price) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false); // トランザクション開始(後述)
for (int i = 0; i < 1000; i++) {
ps.setString(1, "Product_" + i);
ps.setInt(2, i * 100);
ps.addBatch(); // バッチに追加(まだ実行しない)
if (i % 100 == 0) {
ps.executeBatch(); // 100件ごとにまとめて実行
ps.clearBatch();
}
}
ps.executeBatch(); // 残りを実行
conn.commit();
}
1件ずつ executeUpdate() するより、バッチ処理の方が数倍〜数十倍高速になります。
自動採番キーの取得
INSERT後に生成された AUTO_INCREMENT の値を取得したい場合は、prepareStatement() の第2引数に RETURN_GENERATED_KEYS を渡します。
String sql = "INSERT INTO products (name, price) VALUES (?, ?)";
try (PreparedStatement ps =
conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "Banana");
ps.setInt(2, 200);
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
long newId = keys.getLong(1);
System.out.println("生成されたID: " + newId);
}
}
}
第2章:トランザクション管理
トランザクションとは
複数のSQL操作をひとまとまりとして扱い、全部成功するか、全部なかったことにするかを保証する仕組みです。
代表例:銀行振込
① A の口座から 10,000円 を引く (UPDATE)
② B の口座に 10,000円 を足す (UPDATE)
①だけ成功して②が失敗したら、10,000円が消えてしまいます。トランザクションがあれば、②が失敗した時点で①も取り消せます。
デフォルト動作:AutoCommit
JDBCのデフォルトは autoCommit = true です。この状態では、SQLを実行するたびに即座にコミットされます。
conn.setAutoCommit(true); // デフォルト
stmt.executeUpdate(sql1); // ← 実行と同時に確定。取り消せない
stmt.executeUpdate(sql2); // ← 同上
トランザクションを使いたい場合は、まず AutoCommit を無効にします。
基本パターン:commit と rollback
conn.setAutoCommit(false); // トランザクション開始
try {
// ① A の残高を減らす
PreparedStatement ps1 =
conn.prepareStatement("UPDATE accounts SET balance = balance - ? WHERE id = ?");
ps1.setInt(1, 10000);
ps1.setInt(2, 1); // A のID
ps1.executeUpdate();
// ② B の残高を増やす
PreparedStatement ps2 =
conn.prepareStatement("UPDATE accounts SET balance = balance + ? WHERE id = ?");
ps2.setInt(1, 10000);
ps2.setInt(2, 2); // B のID
ps2.executeUpdate();
conn.commit(); // 全部成功 → 確定
System.out.println("振込完了");
} catch (SQLException e) {
conn.rollback(); // 何か失敗 → 全部取り消し
System.out.println("振込失敗:ロールバックしました");
throw e;
} finally {
conn.setAutoCommit(true); // AutoCommit を元に戻す
}
セーブポイント:途中まで戻る
rollback() は全体を取り消しますが、Savepoint を使うと途中の地点までの取り消しが可能です。
conn.setAutoCommit(false);
try {
stmt.executeUpdate(sqlA); // 操作A
Savepoint sp = conn.setSavepoint("point1"); // セーブポイント設定
stmt.executeUpdate(sqlB); // 操作B
if (someCondition) {
conn.rollback(sp); // 操作B だけ取り消し(操作A は残る)
}
conn.commit(); // 操作A を確定
} catch (SQLException e) {
conn.rollback(); // 全体を取り消し
}
トランザクション分離レベル
複数のトランザクションが同時に動く場合、どこまで互いの影響を受けるかを制御する設定です。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード |
|---|---|---|---|
READ_UNCOMMITTED |
発生する | 発生する | 発生する |
READ_COMMITTED |
防止 | 発生する | 発生する |
REPEATABLE_READ |
防止 | 防止 | 発生する |
SERIALIZABLE |
防止 | 防止 | 防止 |
// 例:REPEATABLE_READ に設定(MySQL のデフォルト)
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
実務では READ_COMMITTED(PostgreSQL デフォルト)か REPEATABLE_READ(MySQL デフォルト)を使うことが多いです。レベルを上げるほど安全ですが、同時実行性(スループット)は下がります。
トランザクション処理の鉄則
✅ setAutoCommit(false) でトランザクション開始
✅ 成功時は必ず commit()
✅ 失敗時(catch)は必ず rollback()
✅ finally で setAutoCommit(true) に戻す(または接続をプールに返す前に)
✅ try-with-resources でリソースリークを防ぐ
第3章:接続プーリング(HikariCP)
なぜ接続プーリングが必要か
DBへの接続確立(DriverManager.getConnection())は、内部でTCPハンドシェイク・認証・セッション初期化を行うため、数十〜数百ミリ秒かかります。
【プーリングなし:リクエストのたびに接続確立】
リクエスト → 接続確立(重い) → SQL実行 → 接続切断
リクエスト → 接続確立(重い) → SQL実行 → 接続切断
リクエスト → 接続確立(重い) → SQL実行 → 接続切断
【プーリングあり:接続を使い回す】
起動時に接続を N 本プールとして保持
リクエスト → プールから借りる(軽い) → SQL実行 → プールへ返す
リクエスト → プールから借りる(軽い) → SQL実行 → プールへ返す
本番環境でプーリングなしはパフォーマンス的に成立しません。
HikariCP とは
Java の接続プールライブラリの中で現在最速・最も広く使われているのが HikariCP です。Spring Boot では spring-boot-starter-jdbc を追加するだけでデフォルトとして組み込まれています。
依存関係の追加
Maven(pom.xml)
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
<!-- MySQLドライバ -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
Gradle(build.gradle)
implementation 'com.zaxxer:HikariCP:5.1.0'
implementation 'com.mysql:mysql-connector-j:8.3.0'
基本的なセットアップ
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Tokyo");
config.setUsername("appuser");
config.setPassword("secret");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
// プールサイズの設定
config.setMaximumPoolSize(10); // プールの最大接続数
config.setMinimumIdle(5); // アイドル状態で維持する最小接続数
config.setConnectionTimeout(30000); // 接続取得タイムアウト(ミリ秒)
config.setIdleTimeout(600000); // アイドル接続の最大保持時間(ミリ秒)
config.setMaxLifetime(1800000); // 接続の最大生存時間(ミリ秒)
HikariDataSource dataSource = new HikariDataSource(config);
プールから接続を取得して使う
DataSource の getConnection() はプールから接続を借ります。try-with-resources で close() すると、接続は切断されずにプールへ返却されます。
// DataSource はアプリ起動時に1度だけ作成し、使い回す
public class ProductRepository {
private final HikariDataSource dataSource;
public ProductRepository(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
public List<String> findAll() throws SQLException {
List<String> names = new ArrayList<>();
String sql = "SELECT name FROM products";
try (Connection conn = dataSource.getConnection(); // プールから借りる
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
names.add(rs.getString("name"));
}
} // ← ここで conn.close() → プールへ返却(接続は切れない)
return names;
}
}
プロパティファイルでの設定(推奨)
コードにURLやパスワードを直書きせず、hikari.properties で管理するのが実務では一般的です。
# hikari.properties
dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource
dataSource.url=jdbc:mysql://localhost:3306/mydb
dataSource.user=appuser
dataSource.password=secret
maximumPoolSize=10
minimumIdle=5
connectionTimeout=30000
idleTimeout=600000
maxLifetime=1800000
poolName=MyPool
HikariConfig config = new HikariConfig("/hikari.properties");
HikariDataSource dataSource = new HikariDataSource(config);
Spring Boot での設定(application.yml)
Spring Boot を使っている場合、HikariCP は自動で有効になります。
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Tokyo
username: appuser
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: AppPool
これだけで HikariCP が機能します。DriverManager の直書きは不要です。
プールサイズの決め方
よく「大きくすればするほど良い」と思われがちですが、DBサーバーの処理能力を超えると逆効果になります。
HikariCP の作者 Brett Wooldridge 氏が提唱する計算式:
最適なプールサイズ = コア数 × 2 + ディスクスピンドル数
例:4コアCPU、SSD(スピンドル数=1)の場合 → 4 × 2 + 1 = 9
実務では 10〜20 を起点にして、負荷試験で調整するアプローチが現実的です。
接続の生死確認:connectionTestQuery
プールが保持する接続がDBサーバー側で切られていた場合に備えて、接続を借りる前に疎通確認するクエリを設定できます。
// MySQL 5.x 以前など、JDBC4 未対応の場合
config.setConnectionTestQuery("SELECT 1");
// JDBC4 以降(MySQL 8系など)では不要
// isValid() が自動で使われる
3つの技術を組み合わせた実装例
実務で書く典型的なパターンです。
public class OrderService {
private final HikariDataSource dataSource;
public void placeOrder(int userId, List<OrderItem> items) throws SQLException {
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false); // トランザクション開始
try {
// ① 注文ヘッダを挿入(PreparedStatement)
long orderId;
String insertOrder = "INSERT INTO orders (user_id, created_at) VALUES (?, NOW())";
try (PreparedStatement ps =
conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) {
ps.setInt(1, userId);
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
keys.next();
orderId = keys.getLong(1);
}
}
// ② 注文明細をバッチ挿入
String insertItem =
"INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(insertItem)) {
for (OrderItem item : items) {
ps.setLong(1, orderId);
ps.setInt(2, item.getProductId());
ps.setInt(3, item.getQuantity());
ps.addBatch();
}
ps.executeBatch();
}
conn.commit(); // 全部成功 → 確定
System.out.println("注文完了: orderId=" + orderId);
} catch (SQLException e) {
conn.rollback(); // 失敗 → 全部取り消し
throw e;
} finally {
conn.setAutoCommit(true);
}
} // conn は プールへ返却
}
}
まとめ
PreparedStatement
- プレースホルダ(
?)でSQLインジェクションを根本から防ぐ - 骨格の事前コンパイルで繰り返し実行が高速
-
addBatch()/executeBatch()で大量データ処理を効率化 -
RETURN_GENERATED_KEYSで自動採番IDを取得
トランザクション管理
-
setAutoCommit(false)でトランザクション開始 - 全部成功したら
commit()、失敗したらrollback() -
Savepointで途中地点への部分ロールバックが可能 - 分離レベルは用途に応じて選択(デフォルトはDBによって異なる)
HikariCP
- 接続の確立コストを吸収し、本番環境で必須の仕組み
-
DataSource.getConnection()でプールから借り、close()で返却 - プールサイズは大きすぎず、負荷試験で調整
- Spring Boot では
application.ymlの数行で有効化できる
試験でよく問われるポイント
-
PreparedStatementがSQLインジェクションを防ぐ理由 -
addBatch()とexecuteBatch()の使い方と効果 -
commit()とrollback()のタイミング -
setAutoCommit(false)を忘れた場合に何が起きるか - 接続プーリングを使わない場合の問題点
- HikariCP の
close()が実際には切断ではなく返却である理由
おわりに
PreparedStatement・トランザクション・HikariCP の3つは、現場で動くJavaアプリの大多数が使っている基礎技術です。これらを理解した上でSpring Data JPAやMyBatisといったORMを学ぶと、「このフレームワークは裏でこういうことをやっているのか」という解像度が格段に上がります。