1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

本記事は、実務で必ず使う3つのテーマを深掘りします。

  • PreparedStatement ― 安全で高速なSQL実行
  • トランザクション管理(commit / rollback) ― データ整合性の守り方
  • 接続プーリング(HikariCP) ― 本番環境で耐えうる接続管理

この3つを理解すれば、JDBCの実務知識はほぼ完成します。

第1章:PreparedStatement

Statement との根本的な違い

Statement はSQLを毎回文字列として組み立て、DBへ送ります。PreparedStatementSQLの骨格を先にコンパイルしておき、パラメータだけを後から渡す設計です。

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

プールから接続を取得して使う

DataSourcegetConnection() はプールから接続を借ります。try-with-resourcesclose() すると、接続は切断されずにプールへ返却されます。

// 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を学ぶと、「このフレームワークは裏でこういうことをやっているのか」という解像度が格段に上がります。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?