PreparedStatement
Безопасные параметризованные запросы
«PreparedStatement» — бесплатный урок Java Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Зачем нужен PreparedStatement?
Составлять SQL, присоединяя к нему пользовательский ввод, опасно и медленно.
PreparedStatement решает обе проблемы:
- Безопасность — значения передаются отдельно от SQL, что предотвращает внедрение SQL.
- Производительность — база данных может один раз разобрать и спланировать запрос, а затем повторно использовать результат.
Проблема внедрения SQL
Представьте, что запрос формируется объединением строк. Если пользователь введёт '; DROP TABLE users; -- в качестве имени, объединённый SQL выполнит это как команду.
Это внедрение SQL — одна из самых старых и опасных уязвимостей веб-приложений. 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 — второму и так далее.
Используйте метод установки, соответствующий типу столбца: 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, используйте setNull(index, type) с константой java.sql.Types.
Не передавайте Java-значение null в setString в надежде, что всё сработает, — явно укажите тип:
ps.setNull(2, java.sql.Types.INTEGER)
Получение сгенерированных ключей
После INSERT часто требуется автоматически созданный идентификатор.
При подготовке передайте 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());
}
}
}Пакетная вставка
Вставка тысяч строк по одному оператору за раз выполняется медленно. Используйте пакетную обработку:
- Задайте параметры, затем вызовите
ps.addBatch(). - Повторите это для каждой строки.
- Один раз вызовите
ps.executeBatch().
Строки отправляются вместе, что значительно сокращает количество сетевых обменов.
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());
}
}
}Повторное использование оператора
Один PreparedStatement можно выполнять много раз с разными параметрами.
Задайте заполнители заново и снова вызовите execute. База данных повторно использует кэшированный план выполнения, поэтому подготовленные операторы быстрее при повторяющихся запросах.
Никогда не объединяйте строки
Главное правило: любое изменяющееся значение должно быть параметром ?.
Объединять можно только фиксированные идентификаторы, контролируемые кодом (например, имя столбца из списка разрешённых значений), и даже в этом случае их нужно проверять по списку разрешённых значений. Данные пользователя всегда передавайте как параметры, никогда не присоединяйте к строке.
Закрытие и область повторного использования
PreparedStatement связан с объектом Connection, поэтому после завершения работы его следует закрыть.
Внутри блока try с ресурсами объявляйте оператор так, чтобы он закрылся раньше соединения. Если вы многократно используете оператор в цикле, подготовьте его один раз за пределами цикла, а внутри только задавайте параметры заново — повторная подготовка на каждой итерации приводит к потере кэшированного плана.
Быстрая проверка
Почему PreparedStatement предотвращает SQL-инъекции?
Итоги
Вы научились составлять безопасные и быстрые запросы:
- Используйте заполнители
?и типизированные методы установки значений (индексация с 1). - Привязывайте значения с помощью
setString,setInt,setNullи других методов. - Получайте автоматически сгенерированные ключи с помощью
RETURN_GENERATED_KEYSиgetGeneratedKeys. - Обрабатывайте множество операций записи пакетно с помощью
addBatchиexecuteBatch. - Никогда не объединяйте пользовательские входные данные со строкой SQL.
Часто задаваемые вопросы
Урок «PreparedStatement» бесплатный?
Да — полный текст урока «PreparedStatement» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «PreparedStatement»?
Безопасные параметризованные запросы Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «PreparedStatement»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Основы JDBC
- PreparedStatement
- Транзакции
- Пулы соединений с HikariCP