W3docs

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)SELECTResultSet
executeUpdate(sql)INSERT / UPDATE / DELETE / DDLint — количество затронутых строк
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, построенный из строк, опасен — показывая, что делает вредоносное значение с текстом команды.

java— editable, runs on the server

Что можно извлечь из запуска:

  • Константы курсора — это обычные int-значения, передаваемые в createStatement. TYPE_FORWARD_ONLY + CONCUR_READ_ONLY — значение по умолчанию, самое экономичное; прокручиваемый или обновляемый курсор запрашивайте только тогда, когда он действительно нужен.
  • Statement.RETURN_GENERATED_KEYS — флаг, позволяющий операции INSERT вернуть новый id с автоинкрементом через getGeneratedKeys() — без него получить присвоенный базой данных ключ невозможно.
  • Первый конкатенированный запрос безвреден, потому что в строке Acme нет SQL-метасимволов. Именно поэтому конкатенация строк кажется рабочей при тестировании — и затем ломается в продакшне на реальных входных данных.
  • Второе значение содержит кавычку и точку с запятой, поэтому единственный предполагаемый SELECT превращается в SELECT, за которым следует DROP TABLE. Данные вышли из своих кавычек и стали исполняемым SQL — классическое определение инъекции.
  • Решение — никогда не «экранировать кавычки самостоятельно». Правильный подход — полностью прекратить строить SQL из значений и использовать PreparedStatement, который отправляет шаблон и данные раздельно — об этом следующая глава.

Практика

Практика
Ваш код строит запрос, конкатенируя значение из веб-формы прямо в строку SQL после WHERE owner =. Каков правильный способ исправить это?
Ваш код строит запрос, конкатенируя значение из веб-формы прямо в строку SQL после WHERE owner =. Каков правильный способ исправить это?
Was this page helpful?