PreparedStatement
安全なパラメータ化クエリです。
「PreparedStatement」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。
なぜPreparedStatementを使うのですか?
ユーザー入力を連結してSQLを組み立てると、危険であるだけでなく処理も遅くなります。
PreparedStatementは、次の2つの問題を解決します。
- 安全性 — 値がSQLとは別に送信されるため、SQLインジェクションを防止できます。
- パフォーマンス — データベースがクエリを一度だけ解析して実行計画を作成し、その後再利用できます。
SQLインジェクションの問題
文字列連結でクエリを組み立てる場合を考えてみましょう。ユーザーが名前として'; DROP TABLE users; --と入力すると、連結されたSQLはそれをコマンドとして実行します。
これはSQLインジェクションと呼ばれ、最も古く、被害の大きいWeb脆弱性の1つです。PreparedStatementは設計によってこの問題を不可能にします。
public class Main {
public static void main(String[] args) {
String userInput = "'; DROP TABLE users; --";
// Dangerous: never do this
String unsafe = "SELECT * FROM users WHERE name = '" + userInput + "'";
System.out.println("Injected SQL becomes:");
System.out.println(unsafe);
}
}プレースホルダー
PreparedStatementでは、後から指定する値ごとに?を記述します。
その後、型付きのセッターで値をバインドします。
ps.setString(1, name)ps.setInt(2, age)
パラメーターのインデックスは、?の順番に対応する1始まりです。
安全なクエリ
prepareStatementでステートメントを作成し、パラメーターをバインドしてからexecuteQueryを呼び出します。
値がSQLテキストに触れることはないため、ユーザーが何を入力してもインジェクションは発生しません。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class Main {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost:5432/shop";
String sql = "SELECT id FROM users WHERE name = ?";
try (Connection conn = DriverManager.getConnection(url, "app", "secret");
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "Ada");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getInt("id"));
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}複数のパラメーターをバインドする
各?に対して、それぞれセッターを呼び出します。順序が重要です。パラメーター1は最初の?、パラメーター2は2番目の?に対応し、以下も同様です。
列の型に合ったセッターを使用してください。setInt、setString、setDouble、setBooleanなどがあります。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class Main {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost:5432/shop";
String sql = "INSERT INTO users(name, age, active) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(url, "app", "secret");
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "Grace");
ps.setInt(2, 42);
ps.setBoolean(3, true);
int rows = ps.executeUpdate();
System.out.println("Inserted " + rows + " row");
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}NULLを処理する
SQLのNULLをバインドするには、java.sql.Typesの定数を指定してsetNull(index, type)を使用します。
JavaのnullをsetStringに渡して動作することを期待せず、明示的に指定してください。
ps.setNull(2, java.sql.Types.INTEGER)
生成されたキーを取得する
INSERTの後には、自動生成されたIDが必要になることがよくあります。
準備時にStatement.RETURN_GENERATED_KEYSを渡し、その後ps.getGeneratedKeys()を読み取ります。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class Main {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost:5432/shop";
String sql = "INSERT INTO users(name) VALUES (?)";
try (Connection conn = DriverManager.getConnection(url, "app", "secret");
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "Linus");
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) System.out.println("New id: " + keys.getLong(1));
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}バッチ挿入
何千行も1つずつステートメントで挿入すると、処理が遅くなります。バッチ処理を使用してください。
- パラメーターをバインドしてから、
ps.addBatch()を呼び出します。 - 各行について繰り返します。
- 最後に
ps.executeBatch()を1回呼び出します。
これにより行がまとめて送信され、データベースとの往復回数を大幅に減らせます。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class Main {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost:5432/shop";
String sql = "INSERT INTO tags(label) VALUES (?)";
String[] labels = {"java", "jdbc", "sql"};
try (Connection conn = DriverManager.getConnection(url, "app", "secret");
PreparedStatement ps = conn.prepareStatement(sql)) {
for (String label : labels) {
ps.setString(1, label);
ps.addBatch();
}
int[] counts = ps.executeBatch();
System.out.println("Batched " + counts.length + " inserts");
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}ステートメントを再利用する
1つのPreparedStatementを、異なるパラメーターで何度も実行できます。
プレースホルダーに値を再度バインドして、もう一度executeを呼び出します。データベースはキャッシュされた実行計画を再利用するため、繰り返しクエリではプリペアドステートメントの方が高速になります。
連結しない
鉄則は、変化する値はすべて?パラメーターにすることです。
連結してよいのは、固定されたコード管理下の識別子(許可リストに登録された列名など)だけです。その場合でも、許可リストに対して検証してください。ユーザーから提供されたデータは、必ずバインドし、決して連結しないでください。
クローズと再利用のスコープ
PreparedStatementはConnectionに関連付けられているため、処理が終わったらクローズする必要があります。
try-with-resources内では、ステートメントが接続より先にクローズされるように宣言してください。ループ内でステートメントを何度も再利用する場合は、ループの外で1回だけ準備し、ループ内ではパラメーターだけを再バインドします。反復ごとに準備し直すと、キャッシュされた実行計画が破棄されてしまいます。
クイックチェック
なぜ PreparedStatement はSQLインジェクションを防げるのでしょうか?
まとめ
安全で高速なクエリの書き方を学びました。
?プレースホルダーと型付きセッターを使用します(インデックスは1始まりです)。setString、setInt、setNullなどで値をバインドします。RETURN_GENERATED_KEYSとgetGeneratedKeysで自動生成キーを取得します。addBatch+executeBatchで多数の書き込みをバッチ処理します。- 決してユーザー入力をSQLに連結してはいけません。
よくある質問
「PreparedStatement」レッスンは無料ですか?
はい。「PreparedStatement」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。
「PreparedStatement」で何を学びますか?
安全なパラメータ化クエリです。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Java Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「PreparedStatement」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このJava Academyレッスンでコードを書いて実行できますか?
はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- JDBC の基礎
- PreparedStatement
- トランザクション
- HikariCP によるコネクションプール