PreparedStatement
Bezpieczne zapytania parametryzowane
PreparedStatement to bezpłatna lekcja Java Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Java Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Java Academy zawiera 4 lekcji w sumie.
Dlaczego PreparedStatement?
Tworzenie zapytań SQL przez konkatenację danych użytkownika jest niebezpieczne i powolne.
PreparedStatement rozwiązuje oba problemy:
- Bezpieczeństwo — wartości są wysyłane oddzielnie od SQL, co zapobiega wstrzyknięciu SQL.
- Wydajność — baza danych może przeanalizować i zaplanować zapytanie raz, a następnie ponownie je wykorzystać.
Problem wstrzyknięcia SQL
Rozważmy tworzenie zapytania przez konkatenację ciągów znaków. Jeśli użytkownik wpisze jako imię '; DROP TABLE users; --, skonkatenowany kod SQL wykona tę wartość jako polecenie.
Jest to wstrzyknięcie SQL, jedna z najstarszych i najbardziej szkodliwych luk w zabezpieczeniach aplikacji internetowych. PreparedStatement uniemożliwia je już na poziomie konstrukcji.
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);
}
}Znaczniki zastępcze
W obiekcie PreparedStatement zapisuje się ? dla każdej wartości, która zostanie podana później.
Następnie wartości wiąże się za pomocą setterów odpowiednich dla typu:
ps.setString(1, name)ps.setInt(2, age)
Indeksy parametrów są liczone od 1, zgodnie z kolejnością znaczników ?.
Bezpieczne zapytanie
Należy utworzyć instrukcję za pomocą prepareStatement, powiązać parametry, a następnie wywołać executeQuery.
Wartość nigdy nie trafia do tekstu SQL, więc do wstrzyknięcia nie może dojść, niezależnie od tego, co wpisze użytkownik.
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());
}
}
}Wiązanie wielu parametrów
Każdy znak ? wymaga osobnego wywołania settera. Kolejność ma znaczenie: parametr 1 odpowiada pierwszemu znakowi ?, parametr 2 — drugiemu i tak dalej.
Należy użyć settera zgodnego z typem kolumny — setInt, setString, setDouble, setBoolean itd.
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());
}
}
}Obsługa NULL
Aby powiązać wartość SQL NULL, należy użyć setNull(index, type) wraz ze stałą java.sql.Types.
Nie należy przekazywać wartości Java null do setString i liczyć, że to zadziała — typ trzeba określić jawnie:
ps.setNull(2, java.sql.Types.INTEGER)
Pobieranie wygenerowanych kluczy
Po wykonaniu INSERT często potrzebny jest automatycznie wygenerowany identyfikator.
Podczas przygotowywania należy przekazać Statement.RETURN_GENERATED_KEYS, a następnie odczytać 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());
}
}
}Wstawianie wsadowe
Wstawianie tysięcy wierszy po jednym poleceniu jest powolne. Należy użyć operacji wsadowych:
- Powiązać parametry, a następnie wywołać
ps.addBatch(). - Powtórzyć te czynności dla każdego wiersza.
- Jednorazowo wywołać
ps.executeBatch().
Dzięki temu wiersze są wysyłane razem, co znacznie ogranicza liczbę podróży w obie strony.
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());
}
}
}Ponowne używanie instrukcji
Jednego obiektu PreparedStatement można użyć wiele razy, podając różne parametry.
Należy ponownie ustawić wartości parametrów i wywołać execute. Baza danych ponownie wykorzystuje buforowany plan wykonania, dlatego przygotowane instrukcje są szybsze w przypadku wielokrotnie wykonywanych zapytań.
Nigdy nie konkatenować
Złota zasada: każda zmienna wartość musi być parametrem ?.
Konkatenować można tylko stałe identyfikatory kontrolowane przez kod (np. nazwa kolumny znajdująca się na białej liście) — nawet wtedy należy zweryfikować je względem listy dozwolonych wartości. Dane dostarczone przez użytkownika zawsze należy wiązać jako parametry, nigdy konkatenować.
Zakres zamykania i ponownego użycia
Obiekt PreparedStatement jest powiązany ze swoim obiektem Connection i należy go zamknąć po zakończeniu pracy.
W bloku try-with-resources instrukcję należy zadeklarować tak, aby została zamknięta przed połączeniem. Jeśli instrukcja jest używana wiele razy w pętli, należy przygotować ją raz poza pętlą i tylko ponownie wiązać parametry wewnątrz — ponowne przygotowanie w każdej iteracji powoduje utratę buforowanego planu wykonania.
Szybkie sprawdzenie
Dlaczego PreparedStatement chroni przed wstrzyknięciem SQL?
Podsumowanie
Poznano zasady pisania bezpiecznych i szybkich zapytań:
- Należy używać placeholderów
?i setterów typowanych (indeksowanie od 1). - Wartości należy wiązać za pomocą
setString,setInt,setNullitd. - Automatycznie generowane klucze należy pobierać za pomocą
RETURN_GENERATED_KEYSigetGeneratedKeys. - Wiele operacji zapisu należy grupować za pomocą
addBatch+executeBatch. - Nigdy nie należy konkatenować danych wejściowych użytkownika z kodem SQL.
Często zadawane pytania
Czy lekcja „PreparedStatement” jest bezpłatna?
Tak — pełny tekst „PreparedStatement” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Java Academy, przejdź na CoddyKit PRO. Kurs Java Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „PreparedStatement”?
Bezpieczne zapytania parametryzowane Ćwiczysz Java Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Java Academy?
Nie wymagamy żadnego doświadczenia. Java Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.
Ile czasu zajmuje lekcja „PreparedStatement”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Java Academy?
Tak. Każda lekcja Java Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Podstawy JDBC
- PreparedStatement
- Transakcje
- Pula połączeń z HikariCP