Java JDBCでのSQLクエリ実行とResultSetの使い方完全ガイド

結論:JavaでJDBCのSELECTを安全に実行する基本形は、JDBCドライバーを依存関係へ追加し、Connection、PreparedStatement、ResultSetをtry-with-resourcesで閉じ、SQLの値は?プレースホルダーへ型付きで設定する方法です。ResultSetのカーソルは最初の行より前にあるため、必ずwhile (rs.next())の中で値を読みます。SQL NULLを0と区別したい列はgetIntの直後にwasNullを確認するか、getObject("age", Integer.class)でnullを受け取ります。2026年7月時点のMySQL Connector/Jの現行GAは9.7.0で、Maven座標はcom.mysql:mysql-connector-jです。古いmysql:mysql-connector-javaとClass.forName前提の説明は更新が必要です。 最新の画面名や提供条件は更新で変わるため、以下の順番で確認してください。

目次

最初に確認するポイント

症状・条件主な原因最初の確認
No suitable driver found for jdbc:mysql:…が出るConnector/Jが実行時クラスパスにない、Maven座標が古い、依存関係のスコープが誤っている、またはJDBC URLが不正な可能性があります。依存関係ツリーでcom.mysql:mysql-connector-jが実行時に含まれることと、URLがjdbc:mysql://で始まることを確認します。
ユーザー入力を含む検索で構文エラーや不正な検索結果が出る文字列連結でSQLを組み立てている可能性があり、引用符の問題だけでなくSQLインジェクションの危険があります。SQL本文を固定し、値を?へ置き、PreparedStatementのsetString、setIntなどで設定しているか確認します。
ResultSetから値を読むとSQLExceptionが出る、または最初の行を取りこぼすカーソルが最初は1行目の前にあることを無視して読み取ったか、nextを二重に呼んでいる可能性があります。読み取り処理がwhile (rs.next())の内側にあり、ループ前後で不要なnextを呼んでいないか確認します。
DBのNULLと数値0を区別できないgetIntやgetLongなどのプリミティブ型getterはSQL NULLに対して0を返すため、wasNullを見ていないと区別できません。getObject(columnLabel, Integer.class)を使うか、getInt直後にrs.wasNull()を確認します。
しばらく動かすとコネクション不足やカーソル不足になるConnection、Statement、ResultSetが例外経路でcloseされず、リソースリークしている可能性があります。3つすべてがtry-with-resourcesの宣言または入れ子のtryへ入っているか確認します。
複数のUPDATEの途中で失敗し、一部だけ反映されるConnectionは既定でauto-commitが有効で、各SQL文が個別にコミットされている可能性があります。複数操作を一つの単位にする必要があるならsetAutoCommit(false)、commit、例外時rollbackを実装します。

安全な対処手順

1. JDBCドライバーを現在の依存関係で追加する

MySQLを例にすると、2026年7月時点のConnector/J現行GAは9.7.0で、MavenのgroupIdはcom.mysql、artifactIdはmysql-connector-jです。Connector/Jはドライバーの自動登録に対応しているため、通常はClass.forName("com.mysql.cj.jdbc.Driver")をアプリケーションコードへ書く必要はありません。利用中のMySQL ServerとJavaの対応範囲を確認してからバージョンを固定します。

  1. pom.xmlへ公式のMaven座標を追加する
  2. 本番では依存関係のバージョンを明示して再現可能にする
  3. JARが実行時クラスパスへ含まれることを確認する
  4. 古いmysql:mysql-connector-javaから移行する場合はリリースノートを確認する
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <version>9.7.0</version>
</dependency>

注意:Connector/J 9.7はMySQL Server 8.0以降向けです。古いサーバーへ機械的に上げず、公式互換性表とステージングで確認してください。

2. PreparedStatementとtry-with-resourcesでSELECTを実行する

接続情報はソースコードへ直書きせず、環境変数やシークレット管理から取得します。SQL本文は固定し、検索値を?へ設定します。ConnectionとPreparedStatementを外側、ResultSetを内側のtry-with-resourcesに入れると、ResultSet、Statement、Connectionの順に自動で閉じられます。タイムアウトも設定し、応答しないクエリが無期限に残らないようにします。

  1. JDBC_URL、JDBC_USER、JDBC_PASSWORDを安全な設定から読む
  2. PreparedStatementへ型に合うsetterで値を設定する
  3. executeQueryはSELECTなど1つのResultSetを返すSQLに使う
  4. SQLStateとベンダーコードをログへ残し、パスワードや個人データは残さない
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public final class JdbcSelectExample {
    public static void main(String[] args) throws SQLException {
        String url = System.getenv("JDBC_URL");
        String user = System.getenv("JDBC_USER");
        String password = System.getenv("JDBC_PASSWORD");
        String sql = "SELECT id, name, age FROM users WHERE age >= ? ORDER BY id";

        DriverManager.setLoginTimeout(10);

        try (Connection con = DriverManager.getConnection(url, user, password);
             PreparedStatement ps = con.prepareStatement(sql)) {

            ps.setInt(1, 18);
            ps.setQueryTimeout(15);

            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    long id = rs.getLong("id");
                    String name = rs.getString("name");
                    Integer age = rs.getObject("age", Integer.class);
                    System.out.printf("id=%d, name=%s, age=%s%n", id, name, age);
                }
            }
        } catch (SQLException e) {
            System.err.printf("SQLState=%s, vendorCode=%d%n", e.getSQLState(), e.getErrorCode());
            throw e;
        }
    }
}

注意:例外メッセージやSQL全文には接続先、テーブル名、入力値が含まれる場合があります。本番ログへ秘密情報や個人データを出さないでください。

3. ResultSetのカーソル、型、NULLを正しく扱う

ResultSetは作成直後、最初の行より前を指しています。nextがtrueを返した行だけを読みます。列番号より列ラベルの方がSELECT句の変更に強く、ASで付けたラベルも使えます。NULL許容の数値列をgetIntで読むと0になるため、直後にwasNullを確認します。JDBC 4.2対応ドライバーならgetObject("age", Integer.class)でIntegerまたはnullとして受け取る方法が明確です。

  1. nextを1回だけループ条件に置く
  2. 列名の重複があるJOINではASで一意なラベルを付ける
  3. NULL許容のプリミティブ値はwasNullまたはgetObjectで判定する
  4. スクロール可能ResultSetはドライバー対応を確認し、必要な場合だけ要求する
int age = rs.getInt("age");
Integer nullableAge = rs.wasNull() ? null : age;
Integer nullableAge = rs.getObject("age", Integer.class);

注意:TYPE_SCROLL_INSENSITIVEやCONCUR_UPDATABLEは、すべてのドライバーが同じように対応するとは限りません。単純な読み取りでは既定の前方移動・読み取り専用を優先してください。

4. INSERT・UPDATEはトランザクション単位を明示する

1文だけの更新はexecuteUpdateで更新件数を確認します。複数の更新を一体として成功させる必要がある場合はauto-commitを無効にし、すべて成功したときだけcommitします。例外時はrollbackし、rollback自体の失敗は元の例外へaddSuppressedして原因を失わないようにします。接続プールを使う場合に備え、finallyでauto-commitを元へ戻します。

  1. executeUpdateの戻り値で想定件数を検証する
  2. 複数文の前にsetAutoCommit(false)を呼ぶ
  3. 成功時commit、失敗時rollbackを行う
  4. 接続を返す前にauto-commitなどの状態を戻す
static void transfer(String url, String user, String password,
                     java.math.BigDecimal amount, long fromId, long toId)
        throws SQLException {
    try (Connection con = DriverManager.getConnection(url, user, password)) {
        boolean originalAutoCommit = con.getAutoCommit();
        con.setAutoCommit(false);

        try (PreparedStatement debit = con.prepareStatement(
                 "UPDATE accounts SET balance = balance - ? WHERE id = ?");
             PreparedStatement credit = con.prepareStatement(
                 "UPDATE accounts SET balance = balance + ? WHERE id = ?")) {

            debit.setBigDecimal(1, amount);
            debit.setLong(2, fromId);
            credit.setBigDecimal(1, amount);
            credit.setLong(2, toId);

            if (debit.executeUpdate() != 1 || credit.executeUpdate() != 1) {
                throw new SQLException("Expected one row for each account update");
            }
            con.commit();
        } catch (SQLException e) {
            try {
                con.rollback();
            } catch (SQLException rollbackError) {
                e.addSuppressed(rollbackError);
            }
            throw e;
        } finally {
            con.setAutoCommit(originalAutoCommit);
        }
    }
}

注意:分離レベルをむやみにTRANSACTION_SERIALIZABLEへ上げると、待機やデッドロックが増えることがあります。業務要件とDBMSの仕様に合わせて選んでください。

5. 本番ではDataSource、接続プール、監視へ広げる

DriverManagerは学習用や小規模な処理に分かりやすい一方、Java SEのAPI資料はDataSourceを推奨しています。本番Webアプリでは、利用中のフレームワークが提供するDataSourceと接続プールを使い、接続数、待ち時間、クエリ時間、エラー率を監視します。PreparedStatementは入力値を安全に渡すための基本ですが、テーブル名やORDER BY列などの識別子は?へ渡せないため、許可リストから選びます。

  1. DataSourceと接続プールの上限・タイムアウトを設定する
  2. SELECT *を避け、必要な列だけ取得する
  3. 大量結果はページングやfetch sizeをDBMS・ドライバー仕様に合わせて検証する
  4. SQLState、処理時間、更新件数を監視し、入力値と秘密情報はマスクする

注意:PreparedStatementでもテーブル名や列名を文字列連結すれば安全にはなりません。動的な識別子は固定の許可リストで制限してください。

よくある質問

Class.forName("com.mysql.cj.jdbc.Driver")は今も必要ですか?

通常は不要です。現在のConnector/Jは自動登録に対応し、DriverManagerもサービスプロバイダー機構で利用可能なJDBCドライバーを読み込みます。特殊なクラスローダー構成や古い環境では明示ロードが必要な場合がありますが、まず依存関係と実行時クラスパスを正しく構成してください。

executeQuery、executeUpdate、executeはどう使い分けますか?

SELECTのように1つのResultSetを返すSQLはexecuteQuery、INSERT・UPDATE・DELETEのように更新件数を返すSQLはexecuteUpdateを使います。結果の種類が事前に決められない場合はexecuteを使えますが、通常のアプリケーションでは目的に合う専用メソッドの方が読みやすく、誤用も減らせます。

ResultSetのSQL NULLを0と区別するにはどうしますか?

getIntやgetLongを使う場合は、その直後にwasNullを呼びます。JDBC 4.2対応ドライバーなら、getObject("age", Integer.class)のようにラッパー型で取得すると、SQL NULLをJavaのnullとして扱えるため分かりやすいです。

公式情報

画面や仕様が異なる場合は、利用中の製品・契約・管理ポリシーを確認したうえで公式情報を参照してください。

まとめ

原因を一つずつ切り分け、変更前の状態と結果を記録しながら進めることが重要です。操作後は同じ症状が再発しないか確認し、組織管理の端末ではポリシー変更を管理者へ確認してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次