Java JDBC PreparedStatement
Безопасное выполнение параметризованных SQL-запросов в Java с помощью PreparedStatement для защиты от SQL-инъекций.
PreparedStatement — это SQL-шаблон с заполнителями ?, куда подставляются значения. Значения задаются отдельно по индексу, и драйвер передаёт шаблон и данные по разным каналам — поэтому значение никогда не может быть разобрано как SQL. Это главная привычка в JDBC: она делает инъекцию структурно невозможной и позволяет базе данных переиспользовать план запроса. Предпочитайте PreparedStatement обычному Statement практически везде.
В этой главе рассматривается создание, привязка и выполнение PreparedStatement, почему он останавливает SQL-инъекцию, как привязать NULL и типизированные значения, а также когда выгодно повторно использовать один объект.
Создание, привязка, выполнение
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name); // bind by 1-based index
ps.setInt(2, age);
int rows = ps.executeUpdate();
}Заполнители нумеруются с 1, а не с 0 — постоянный источник ошибок на единицу. Каждый setXxx соответствует типу столбца: setString, setInt, setBigDecimal, setTimestamp и так далее.
Почему он предотвращает инъекцию
При использовании Statement значение является частью SQL-текста, поэтому кавычка в значении может завершить литерал и внедрить новые команды. С PreparedStatement SQL фиксирован и разбирается до привязки любого значения; значение затем передаётся как типизированный параметр. Нет строки, из которой кавычка злоумышленника могла бы вырваться — опасное значение из главы про Statement просто становится буквальным именем.
После выполнения запроса вы читаете его строки с помощью ResultSet, точно так же, как при работе со Statement.
Привязка NULL и специальных типов
Вы не можете передать Java null в setInt (он принимает примитив), а setString(i, null) неоднозначен для некоторых типов. Явная форма — setNull(index, sqlType), где указывается тип столбца из java.sql.Types:
ps.setNull(3, java.sql.Types.VARCHAR);Повторное использование подготовленного выражения
Подготовленное выражение создано для многократного выполнения с разными значениями — задать, выполнить, очистить, повторить. База данных разбирает и планирует SQL один раз и переиспользует его, поэтому подготовленные выражения также быстрее в цикле, чем пересборка строки Statement каждый раз. Для массовых вставок сочетайте это с пакетной обработкой.
Практический пример: анатомия шаблона и его привязок
Эта программа рассматривает SQL-шаблон как данные: подсчитывает заполнители, перебирает значения, которые вы бы к ним привязали (включая вредоносную строку, сломавшую Statement), и показывает код типа setNull — все составные части привязки параметров, без реальной базы данных.
Что следует усвоить из выполнения:
- Шаблон содержит три заполнителя
?, и программа их подсчитывает — именно столько вызововsetXxxнеобходимо сделать. Несоответствие (привязка параметра 4 в запросе с 3 заполнителями) приведёт к исключению при выполнении. - Привязки начинаются с 1: параметр 1 — это первый
?. Цикл выводитbind 1,bind 2,bind 3, чтобы закрепить это — начало с 0 является наиболее частой ошибкой новичков. - Первое значение — та же строка
x'; DROP TABLE users;--, которая взломалаStatementв предыдущей главе. Здесь она просто данные, привязанные к параметру 1; драйвер сохраняет её дословно как имя. Инъекция нейтрализована по конструкции, а не экранированием. nullв параметре 3 — вот почему существуетsetNull(index, Types.VARCHAR). JDBC нужен SQL-тип, чтобы сообщить базе данных, какой именно NULL — вы указываете его константойjava.sql.Types.- Каждое значение несёт неявный тип —
String,int, NULL-of-VARCHAR — вот почему существуетsetXxxдля каждого типа, а не один строковый сеттер. Соответствие сеттера типу столбца — это дисциплина, которая делает подготовленные выражения одновременно безопасными и корректными.