Java JDBC Statement
Выполнение SQL в Java с помощью интерфейса Statement — когда использовать его, а когда PreparedStatement.
Statement отправляет в базу данных полную, фиксированную строку SQL. Вы создаёте его из объекта Connection, передаёте SQL и получаете обратно либо ResultSet (для запросов), либо количество изменённых строк (для операций изменения). Это простейший из трёх типов JDBC-выражений — и тот, к которому следует прибегать реже всего, потому что любые переменные данные в SQL приходится вставлять вручную через конкатенацию, а именно так и возникают уязвимости SQL-инъекций.
В этой главе рассматривается, как создать и выполнить Statement, три метода выполнения и когда каждый из них уместен, как настроить курсор и считать сгенерированные ключи, и — что важнее всего — когда следует остановиться и использовать PreparedStatement. Если вы только начинаете знакомство с JDBC, начните с введения в JDBC.
Создание и выполнение
try (Connection conn = DriverManager.getConnection(url, user, pw);
Statement st = conn.createStatement()) {
// a query → ResultSet
try (ResultSet rs = st.executeQuery("SELECT count(*) FROM product")) {
rs.next();
System.out.println(rs.getInt(1));
}
// a change → update count
int rows = st.executeUpdate("UPDATE product SET active = true WHERE price > 0");
System.out.println(rows + " rows updated");
}Три метода выполнения
| Метод | Используется для | Возвращает |
|---|---|---|
executeQuery(sql) | SELECT | ResultSet |
executeUpdate(sql) | INSERT / UPDATE / DELETE / DDL | int — количество затронутых строк |
execute(sql) | неизвестный тип / несколько результатов | boolean (true, если вернулся ResultSet) |
Используйте executeQuery и executeUpdate, когда заранее знаете, какой тип выражения выполняете, — они возвращают нужный тип напрямую. К execute обращайтесь только в универсальных инструментах (SQL-консоль, раннер миграций), где SQL не известен до момента выполнения; после вызова используйте getResultSet() или getUpdateCount() для получения результата.
executeUpdate возвращает 0 для DDL-выражений вроде CREATE TABLE, а для INSERT/UPDATE/DELETE — количество затронутых строк, что удобно для проверки того, что обновление действительно совпало с какой-либо строкой.
Настройка курсора и сгенерированных ключей
При создании выражения можно указать поведение результирующего курсора с помощью createStatement(resultSetType, resultSetConcurrency) — например, TYPE_FORWARD_ONLY, CONCUR_READ_ONLY (значение по умолчанию, самое быстрое). Запрашивайте TYPE_SCROLL_INSENSITIVE только тогда, когда нужно двигаться по результату назад, и CONCUR_UPDATABLE — только когда планируете редактировать строки через курсор; оба режима обходятся дороже.
Для вставок передайте Statement.RETURN_GENERATED_KEYS, а затем считайте присвоенный базой данных первичный ключ через getGeneratedKeys():
try (Statement st = conn.createStatement()) {
st.executeUpdate(
"INSERT INTO product(name, price) VALUES ('Widget', 9.99)",
Statement.RETURN_GENERATED_KEYS);
try (ResultSet keys = st.getGeneratedKeys()) {
if (keys.next()) {
long newId = keys.getLong(1);
System.out.println("inserted id = " + newId);
}
}
}Без этого флага вызов завершится успешно, но getGeneratedKeys() вернёт пустой ResultSet, и получить новый id не удастся.
Когда НЕ следует использовать Statement
Как только любая часть SQL берётся из переменной — имя пользователя, id, поисковый запрос — остановитесь и используйте PreparedStatement. Конкатенация значений в строку Statement небезопасна: значение, содержащее кавычку, может изменить смысл команды. Кроме того, PreparedStatement кэширует план разбора, поэтому запрос, выполняемый в цикле, работает быстрее в виде подготовленного выражения. Следующая глава целиком посвящена этой безопасной альтернативе; для хранимых процедур смотрите CallableStatement.
Оставьте Statement для фиксированного SQL без переменных значений: настройка схемы (CREATE TABLE …), разовые DDL-операции или жёстко заданный SELECT без переменных частей.
Никогда не закрывайте Statement, пока вам ещё нужен его ResultSet — закрытие выражения закрывает и любой полученный им результат. Используйте блок try-with-resources, как в примерах выше, чтобы каждый объект закрывался в правильном порядке.
Развёрнутый пример: константы курсора и ловушка инъекции
Эта программа выводит константы настройки ResultSet/Statement, передаваемые при создании выражения, а затем наглядно демонстрирует, почему SQL, построенный из строк, опасен — показывая, что делает вредоносное значение с текстом команды.
Что можно извлечь из запуска:
- Константы курсора — это обычные
int-значения, передаваемые вcreateStatement.TYPE_FORWARD_ONLY+CONCUR_READ_ONLY— значение по умолчанию, самое экономичное; прокручиваемый или обновляемый курсор запрашивайте только тогда, когда он действительно нужен. Statement.RETURN_GENERATED_KEYS— флаг, позволяющий операцииINSERTвернуть новый id с автоинкрементом черезgetGeneratedKeys()— без него получить присвоенный базой данных ключ невозможно.- Первый конкатенированный запрос безвреден, потому что в строке
Acmeнет SQL-метасимволов. Именно поэтому конкатенация строк кажется рабочей при тестировании — и затем ломается в продакшне на реальных входных данных. - Второе значение содержит кавычку и точку с запятой, поэтому единственный предполагаемый
SELECTпревращается вSELECT, за которым следуетDROP TABLE. Данные вышли из своих кавычек и стали исполняемым SQL — классическое определение инъекции. - Решение — никогда не «экранировать кавычки самостоятельно». Правильный подход — полностью прекратить строить SQL из значений и использовать
PreparedStatement, который отправляет шаблон и данные раздельно — об этом следующая глава.