結論から言うと、現行JDBCの基本は「対象DBの公式ドライバを依存関係へ追加する」「接続情報をコード外から渡す」「値はPreparedStatementで設定する」「Connection・Statement・ResultSetをtry-with-resourcesで閉じる」の4点です。複数の更新は自動コミットを止め、成功時だけcommit()、失敗時はrollback()します。本番接続ではTLSの暗号化だけでなく、CAとホスト名の検証も有効にします。
JDBCはJava側のAPIを共通化しますが、SQL文、JDBC URL、データ型、認証、TLS設定まで全DBで同じにするものではありません。PostgreSQL、MySQL、SQL Server、Oracleなど、利用するDBとJDKの組み合わせを決め、そのベンダーの公式ドライバ文書を確認するところから始めます。
最初に押さえる構成要素
| 要素 | 役割 | 基本方針 |
|---|---|---|
| JDBCドライバ | JDBC APIと対象DBの通信を実装 | DBベンダーの公式版を依存関係管理で導入 |
| JDBC URL | プロトコル、ホスト、ポート、DB名、接続設定を指定 | 資格情報をURLへ埋め込まず、TLS検証を明示 |
| DriverManager | 登録済みドライバから接続を取得 | 学習用・小規模ツールに分かりやすい |
| DataSource | 接続を生成するファクトリ | Java APIが推奨。プールや分散トランザクションへ拡張可能 |
| Connection | DBとのセッション、トランザクション境界 | 短いスコープで確実に閉じる |
| PreparedStatement | パラメーター化SQLを実行 | 外部値を文字列連結しない |
| ResultSet | SELECT結果を行単位で読む | Statementより内側のtry-with-resourcesで閉じる |
| SQLException | 接続・SQL・制約などのエラー | SQLStateとベンダーコードを秘密なしで記録 |
DriverManagerとDataSourceのどちらを使うか
| 用途 | 選択 | 理由 |
|---|---|---|
| 接続確認、学習用CLI、短いバッチ | DriverManager | 標準APIだけで流れを確認しやすい |
| Webアプリ、常駐サービス | DataSource + 接続プール | 接続再利用、上限、待機、監視を管理しやすい |
| Jakarta EE/アプリサーバー | コンテナ管理DataSource | JNDI、プール、トランザクションを運用側で管理 |
| Springなどのフレームワーク | フレームワーク管理DataSource | 設定、プール、例外変換、トランザクション機能と統合 |
Oracle Java APIはDataSourceを接続取得の推奨手段としています。ただしJDBCの仕組みを理解するにはDriverManagerの最小コードが役立ちます。本番で「SQLを実行するたびに物理接続を新規作成する」設計へそのまま持ち込まず、利用するランタイムの公式DataSourceと接続プールを設定してください。
手順1:公式JDBCドライバを導入する
- 使用するDBサーバーの製品とバージョン、アプリのJDKを確認します。
- PostgreSQLならpgJDBC、MySQLならConnector/J、SQL ServerならMicrosoft JDBC Driver、OracleならOracle JDBCというように、ベンダー公式ページから対応表を確認します。
- MavenまたはGradleで公式アーティファクトを依存関係へ追加します。任意のダウンロードサイトからJARを取得しません。
- ビルド成果物または実行時クラスパスにJARが含まれることを確認します。コンパイルだけ通り、本番実行時にJARが欠ける構成に注意します。
- ドライバ更新時はリリースノートとJDK/DBサポートを確認し、接続・SQL・TLSをステージング環境で再試験します。
Class.forNameは原則不要
JDBC 4.0以降はjava.sql.Driverのサービスプロバイダーを自動検出します。現行ドライバを通常の依存関係として正しく入れたアプリでは、次のような明示ロードを必須手順にする必要はありません。
// 現行JDBCでは通常不要
Class.forName("org.postgresql.Driver");
No suitable driverが出たときはClass.forNameを足して隠す前に、実行時JAR、URLの先頭、ドライバとJDKの互換性を確認します。Java 9以降なら、読み込まれたドライバを次のコードで確認できます。
DriverManager.drivers()
.forEach(driver -> System.out.println(driver.getClass().getName()));
手順2:接続専用ユーザーと秘密情報を準備する
- 最小権限:root、sa、SYSなどの管理者ではなく、必要なDB・スキーマ・表への必要な操作だけを許可したアプリ専用ユーザーを使います。
- 環境分離:開発、検証、本番でユーザーと接続先を分けます。本番資格情報を開発PCへ配りません。
- 秘密管理:パスワードをJavaソース、Git、Dockerfile、ログ、JDBC URLへ直書きしません。実行環境の秘密管理から注入します。
- 重複指定を避ける:DriverManagerのURLとPropertiesの両方へuser/passwordを指定すると優先度が実装依存になり得るため、一箇所だけで渡します。
- TLSの信頼:信頼するCAをJVMまたはアプリのtrust storeへ導入し、DB証明書のSANと接続ホスト名を一致させます。
以下では説明を明確にするため環境変数を使います。環境変数は秘密管理製品そのものではなく、プロセスへ値を渡す一例です。本番では利用基盤のSecret Manager、資格情報プロバイダー、ファイル権限、ローテーション方針に従います。
手順3:更新を行わない最小接続コード
最初のテストではDDLやUPDATEを実行せず、接続が有効か、どのDB製品へつながったかだけを確認します。
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public final class JdbcConnectionCheck {
private static String requiredEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException(name + " is not set");
}
return value;
}
public static void main(String[] args) {
String url = requiredEnv("DB_URL");
Properties properties = new Properties();
properties.setProperty("user", requiredEnv("DB_USER"));
properties.setProperty("password", requiredEnv("DB_PASSWORD"));
DriverManager.setLoginTimeout(10);
try (Connection connection =
DriverManager.getConnection(url, properties)) {
if (!connection.isValid(5)) {
throw new SQLException("Connection validation failed");
}
DatabaseMetaData meta = connection.getMetaData();
System.out.println("Database: " + meta.getDatabaseProductName());
System.out.println("Driver: " + meta.getDriverName());
} catch (SQLException e) {
System.err.println("SQLState=" + e.getSQLState());
System.err.println("VendorCode=" + e.getErrorCode());
System.err.println("Message=" + e.getMessage());
System.exit(1);
}
}
}
このコードはパスワードを表示しません。ただしSQLException#getMessage()にはホスト名や内部情報が含まれる場合があります。本番では構造化ログへ必要な情報だけ記録し、利用者画面へ例外全文を返しません。
DB製品別のJDBC URLとTLS
次はdb.example.internalとappdbを使った構成例です。ポート、サービス名、trust storeは環境の公式設定へ置き換えます。
| DB | 検証を有効にするURL例 | 注意 |
|---|---|---|
| PostgreSQL | jdbc:postgresql://db.example.internal:5432/appdb?sslmode=verify-full | 既定preferはCAとホスト名の完全検証ではない |
| MySQL | jdbc:mysql://db.example.internal:3306/appdb?sslMode=VERIFY_IDENTITY | PREFERREDは非暗号化へフォールバックし得る |
| SQL Server | jdbc:sqlserver://db.example.internal:1433;databaseName=appdb;encrypt=true;trustServerCertificate=false | 既定値の版差に依存せず検証を明示 |
| Oracle Thin | jdbc:oracle:thin:@tcps://db.example.internal:1522/appdb | サービス名、wallet/trust storeを公式Oracle文書で設定 |
暗号化とサーバー認証は同じではありません。PostgreSQLのrequire、MySQLのREQUIRED、SQL ServerのtrustServerCertificate=trueは、構成によって通信を暗号化しても正しいサーバーかを十分に検証しません。証明書エラーは検証を無効にせず、CA、SAN、接続DNS名、時刻を修正します。
手順4:PreparedStatementでSELECTする
ユーザー入力やAPIパラメーターをSQL文字列へ連結しません。値を?へ分離し、型に合うsetterで渡します。
String sql = """
SELECT id, name, retry_count
FROM users
WHERE status = ?
ORDER BY id
""";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "ACTIVE");
statement.setQueryTimeout(15);
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
long id = result.getLong("id");
String name = result.getString("name");
Integer retryCount =
result.getObject("retry_count", Integer.class);
System.out.printf(
"id=%d name=%s retryCount=%s%n",
id,
name,
retryCount
);
}
}
}
getInt()はSQL NULLを0として返し、別途wasNull()を確認する必要があります。NULLと0を区別する列では、対応ドライバでgetObject("retry_count", Integer.class)のようにラッパー型で受けると意図が明確です。
PreparedStatementでも識別子は渡せない
?へ渡せるのは値です。テーブル名、列名、ASC/DESCなどSQL構造はパラメーター化できません。並び替え列を利用者が選ぶ場合は、外部文字列をそのまま連結せず、許可リストから固定SQLへ変換します。
String orderBy = switch (requestedSort) {
case "name" -> "name";
case "created" -> "created_at";
default -> "id";
};
String sql = "SELECT id, name FROM users ORDER BY " + orderBy;
この連結が許されるのは、出力がコード内の3つの固定文字列だけだからです。入力値自体をSQLへ連結していません。
手順5:更新件数を確認する
INSERT、UPDATE、DELETEはexecuteUpdate()の戻り値で影響行数を確認します。対象が1件のはずなのに0件または複数件なら、成功扱いにしません。
String sql = "UPDATE users SET display_name = ? WHERE id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "Taro Yamada");
statement.setLong(2, 1001L);
int updated = statement.executeUpdate();
if (updated != 1) {
throw new SQLException("Unexpected update count: " + updated);
}
}
本番データで試さず、バックアップのある検証DBと専用テスト行を使います。WHEREなしのUPDATE/DELETEをアプリから発行できないよう、コードレビュー、権限、テストを重ねます。
手順6:所有するConnectionでトランザクションを完結させる
新しいConnectionは既定でauto-commitが有効です。そのまま2つのUPDATEを実行すると、1つ目だけ確定した後に2つ目が失敗する可能性があります。ここではメソッド自身がトランザクションを所有すると決め、DataSourceからこの処理専用の新しいConnectionを取得します。呼び出し元がすでに使っているConnectionは受け取らないため、別の未確定処理を誤ってcommitすることがありません。
static void transfer(
javax.sql.DataSource dataSource,
long sourceId,
long destinationId,
java.math.BigDecimal amount) throws SQLException {
java.util.Objects.requireNonNull(dataSource, "dataSource");
java.util.Objects.requireNonNull(amount, "amount");
if (sourceId == destinationId || amount.signum() <= 0) {
throw new IllegalArgumentException("Invalid transfer request");
}
String debitSql = """
UPDATE accounts
SET balance = balance - ?
WHERE account_id = ? AND balance >= ?
""";
String creditSql = """
UPDATE accounts
SET balance = balance + ?
WHERE account_id = ?
""";
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
try {
try (PreparedStatement debit = connection.prepareStatement(debitSql);
PreparedStatement credit = connection.prepareStatement(creditSql)) {
debit.setBigDecimal(1, amount);
debit.setLong(2, sourceId);
debit.setBigDecimal(3, amount);
credit.setBigDecimal(1, amount);
credit.setLong(2, destinationId);
if (debit.executeUpdate() != 1) {
throw new SQLException("Debit was not applied");
}
if (credit.executeUpdate() != 1) {
throw new SQLException("Credit was not applied");
}
}
connection.commit();
} catch (SQLException original) {
try {
connection.rollback();
} catch (SQLException rollbackFailure) {
original.addSuppressed(rollbackFailure);
}
throw original;
}
}
}
外側のtry-with-resourcesが、このメソッドの所有するConnectionを最後に閉じます。接続プールのDataSourceならclose()は通常プールへの返却です。内側のtry-with-resourcesは2つのPreparedStatementを逆順に閉じ、両方のcloseが完了してからcommitへ進みます。したがってstatementのclose失敗は未commitの段階でcatchされ、同じConnectionでrollbackできます。rollback失敗は元のSQLExceptionへaddSuppressed()し、元原因を失わず、最後にConnectionも閉じます。
この所有方式ではautoCommitを元へ戻してConnectionを使い回さず、必ず閉じて返却します。既存Connectionへ参加する別メソッドを設計するなら、そのメソッドはsetAutoCommit、commit、rollbackを呼ばず、トランザクション境界を呼び出し元だけに持たせます。既存トランザクションを無条件にcommitする実装と、所有方式を同じAPIへ混在させてはいけません。
フレームワークの契約上、外部管理の専用ConnectionでautoCommitを復元する設計が必要な場合は、commitまたはrollbackの結果を確定した後のfinallyで元値へ戻します。復元のsetAutoCommitがSQLExceptionになった接続は正常化できていないため、復元失敗を元例外へsuppressedとして残し、close、abort、またはプール固有のevictで再利用不可にします。復元に失敗した接続を通常のプールへ返してはいけません。なおcommit後のConnection.close失敗では確定済みかもしれないため、業務処理を無条件に再実行せず、取引IDや監査記録で結果を確認します。
SQLExceptionの診断表
| 症状・例外 | 主な確認 | 避ける対処 |
|---|---|---|
| No suitable driver | 実行時JAR、URL先頭、JDK互換性、DriverManager.drivers() | 不明なJARを追加、無条件Class.forName |
| Connection refused | DNS、ポート、DBリスナー、ファイアウォール | DBをインターネットへ無制限公開 |
| Connection timeout | 経路、FW、接続上限、プール枯渇、login timeout | タイムアウトを無期限にする |
| Authentication failed | ユーザー、秘密、対象DB、ロック、認証方式 | root/saへ置き換える |
| PKIX path building failed | CAチェーン、trust store、期限、SANと接続名 | 証明書検証を無効化 |
| Permission denied | 専用ユーザーの必要権限とスキーマ | 管理者権限を丸ごと付与 |
| SQL syntax error | 対象DB方言、予約語、列名、生成SQL | 利用者画面へSQL全文を表示 |
| Deadlock / serialization failure | トランザクション順序、範囲、SQLState | 一部処理だけ無条件再実行 |
| Too many connections | close漏れ、プール上限、DB上限、長時間処理 | DB上限だけを際限なく増やす |
SQLStateはエラー分類、getErrorCode()はベンダー固有コードです。再試行する場合はSQLStateとDB公式文書で一時障害か確認し、べき等性のあるトランザクション全体を回数制限と待機時間付きで再実行します。INSERTの一部だけを繰り返すと重複を作ります。
本番向けの安全チェック
- DataSourceと実績のある接続プールを使い、最大接続数、待機時間、接続寿命、リーク検出をDB上限と合わせる。
- クエリタイムアウトとトランザクションタイムアウトを決め、無期限の待機を避ける。
- SELECT *を避け、必要な列だけ取得する。大量結果はページング、fetch size、ストリーミングをドライバ文書で確認する。
- 日時はDB型、タイムゾーン、Java型の対応を決め、サーバー既定タイムゾーンに依存しない。
- 秘密は定期ローテーションし、接続プールが新資格情報へ切り替わる手順を検証する。
- SQLパラメーター、パスワード、個人情報をログへ出さない。監査に必要な相関ID、SQLState、処理名を記録する。
- アプリ利用者のパスワード保存とDB接続パスワードを混同しない。生のSHA-256を使う自作パスワード保存例は採用しない。
変更と切り戻し
- ドライバ更新、URL変更、TLS変更はステージングで接続・CRUD・トランザクション・障害時ロールバックを試します。
- 旧ドライバ、旧設定、DB証明書チェーンの有効期限を記録し、アプリ配布を戻せるリリース単位にします。
- 本番DML前にDBバックアップと復元手順を確認し、更新件数の上限と監視を設けます。
- DataSourceから取得した所有Connectionはcommitまたはrollbackの後に必ずcloseします。外部管理ConnectionのautoCommitを復元する設計では、復元失敗をsuppressedとして記録し、その接続をclose、abort、またはevictして再利用不可にします。
- 新設定で障害が出たら、TLS検証を弱めるのではなく旧アプリ版・旧ドライバへ戻し、CA、SAN、互換性を調査します。
よくある質問
JDBCドライバはJDKに含まれていますか?
java.sqlなどのJDBC APIはJava SEに含まれますが、PostgreSQLやMySQLなど各DBへ接続する実装ドライバは別です。対象DBの公式ドライバを依存関係へ追加します。
Class.forNameを書かないと接続できませんか?
JDBC 4.0以降の現行ドライバでは通常不要です。書かないと接続できない場合は、実行時JAR、サービスプロバイダー、URL、古いドライバの仕様を確認します。
Statementは使ってはいけませんか?
外部値を含まない完全な固定SQLには使えます。ただし値が入る基本例はPreparedStatementへ統一した方が安全です。動的な識別子は許可リストで固定文字列へ変換します。
try-with-resourcesでConnectionだけ閉じれば十分ですか?
StatementとResultSetもそれぞれ閉じます。外側からConnection、PreparedStatement、ResultSetの順にtry-with-resourcesを重ねると、逆順に確実に解放されます。トランザクションではPreparedStatementの内側ブロックを終了してcloseを完了させ、その後でcommitします。commitをPreparedStatementのresource block内へ置きません。
証明書エラー時にtrustServerCertificate=trueを使えますか?
恒久対策にはしません。暗号化されても接続先の正当性を検証できなくなります。信頼するCAをtrust storeへ入れ、接続ホスト名を証明書SANに合わせます。
SQLExceptionをそのまま画面に表示してよいですか?
避けてください。ホスト名、SQL、表名、内部構成が含まれる場合があります。利用者には相関IDと一般的な案内を表示し、管理ログへ秘密を除いたSQLState、ベンダーコード、処理名を残します。
公式資料
- Java SE 25: DriverManager
- Java SE 25: Connection
- Java SE 25: DataSource
- Java SE 25: java.sql package
- pgJDBC: Initializing the Driver and Connecting to the Database
- MySQL Connector/J: Security properties
- Microsoft JDBC Driver for SQL Server: Building the connection URL
- Oracle AI Database JDBC Java API Reference
まとめ
JDBCでは、公式ドライバと正しいURLを用意し、接続資格情報をコード外から一度だけ渡します。現行JDBCではClass.forNameを通常必要とせず、値はPreparedStatement、リソースはtry-with-resourcesで管理します。複数更新はDataSourceから取得した専用Connectionをメソッドが所有し、statementを閉じてからcommitし、SQLExceptionでは同じConnectionをrollbackしてからcloseします。既存Connectionへ参加する処理はcommitせず、所有境界を混ぜません。本番は接続プール、最小権限、タイムアウト、TLSのCA・ホスト名検証を組み合わせ、失敗時にデータと接続状態を安全に扱えるところまで設計します。

コメント