0Pricing
Java Academy · Lekcja

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, setNull itd.
  • Automatycznie generowane klucze należy pobierać za pomocą RETURN_GENERATED_KEYS i getGeneratedKeys.
  • 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

  1. Podstawy JDBC
  2. PreparedStatement
  3. Transakcje
  4. Pula połączeń z HikariCP
← Powrót do Java Academy