W3docs

Пакетная обработка Java JDBC

Эффективное выполнение множества SQL-запросов в Java с помощью пакетной обработки JDBC — addBatch и executeBatch.

Когда нужно выполнить сотни или тысячи операций вставки или обновления, отправка их по одной означает один сетевой round trip на каждую — это основные затраты. Пакетная обработка собирает множество операторов и отправляет их в базу данных за один round trip, превращая секунды в миллисекунды. Это стандартный подход для массовой загрузки данных.

В этой главе рассматривается, как ставить операторы в очередь с помощью addBatch() и выполнять их через executeBatch(), что означает возвращаемый int[] (включая два специальных маркера), как обернуть пакет в транзакцию и как восстановиться после сбоя одного из операторов. Глава опирается на материалы JDBC PreparedStatement и JDBC Transactions.

addBatch и executeBatch

Операторы ставятся в очередь с помощью addBatch(), а выполняются все сразу через executeBatch(), который возвращает int[] с количеством затронутых строк для каждого оператора:

String sql = "INSERT INTO log(msg) VALUES (?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
  for (String msg : messages) {
    ps.setString(1, msg);
    ps.addBatch();          // queue this set of parameters
  }
  int[] counts = ps.executeBatch();   // one round trip
}

С PreparedStatement параметры привязываются и вызывается addBatch() для каждой строки; с обычным Statement в addBatch(sql) передаётся полная SQL-строка. Предпочтительнее использовать подготовленную форму — те же преимущества безопасности привязки параметров (нет SQL-инъекций) и повторного использования плана выполнения. Обратите внимание: пакет одного PreparedStatement должен выполнять одну фиксированную SQL-строку с разными параметрами; если нужны принципиально разные операторы, используйте обычный Statement.

Возвращаемое значение и его специальные маркеры

executeBatch() возвращает одну запись на каждый поставленный в очередь оператор. Большинство значений — это количество затронутых строк, но две константы сигнализируют об особых случаях:

  • Statement.SUCCESS_NO_INFO (−2): оператор выполнен успешно, но драйвер не знает, сколько строк было затронуто.
  • Statement.EXECUTE_FAILED (−3): данный конкретный оператор завершился с ошибкой (видно только через BatchUpdateException).

Пакет и транзакция

Всегда выполняйте пакет внутри явной транзакции (setAutoCommit(false)), чтобы в случае сбоя весь пакет откатился, а не оказался частично применён. Сбрасывайте большие пакеты периодически — примерно каждые 1000 строк — вызывая executeBatch(), а затем clearBatch(), чтобы очередь драйвера в памяти не росла безгранично:

conn.setAutoCommit(false);
int n = 0;
for (String msg : messages) {
  ps.setString(1, msg);
  ps.addBatch();
  if (++n % 1000 == 0) {
    ps.executeBatch();      // flush a chunk
    ps.clearBatch();        // free the queued statements
  }
}
ps.executeBatch();          // flush the remainder
conn.commit();              // make every chunk durable together

Поскольку все фрагменты используют одну транзакцию, периодические вызовы executeBatch() сами по себе ничего не фиксируют — именно commit() в конце делает всю загрузку постоянной, а единственный rollback() отменяет всё целиком.

Когда оператор в пакете завершается с ошибкой

Если какой-либо оператор завершается неудачно, executeBatch() выбрасывает BatchUpdateException. Его метод getUpdateCounts() возвращает количества, собранные до момента сбоя — позволяя увидеть, какие операторы выполнились до ошибки — а также содержит стандартные данные SQLException, такие как getSQLState().

Практический пример: счётчики, маркеры и неудачный пакет

Эта программа создаёт пакет, выводит две константы специальных маркеров, показывает int[], возвращаемый при успешном выполнении, и формирует BatchUpdateException, чтобы наглядно продемонстрировать, что именно возвращает getUpdateCounts() при сбое одного оператора — всё без подключения к реальной базе данных.

java— editable, runs on the server

Выводы из запуска:

  • addBatch() ставит работу в очередь, не отправляя её; executeBatch() отправляет всю очередь за один round trip. Выигрыш исключительно в количестве round trip — здесь три вставки, но та же схема масштабируется до тысяч, где экономия огромна.
  • Успешный запуск возвращает [1, 1, 1] — одно количество обновлений на каждый поставленный в очередь оператор, по порядку. Этот массив позволяет убедиться, что каждый оператор затронул ожидаемое число строк.
  • SUCCESS_NO_INFO (−2) означает «выполнено, но строки не считались». Некоторые драйверы возвращают его для пакетных операторов, поэтому любое отрицательное значение, не являющееся ошибкой, следует считать успехом, а не сбоем.
  • При сбое драйвер выбрасывает BatchUpdateException, и getUpdateCounts() возвращает [1, -3, -2]: первая вставка выполнена, вторая завершилась ошибкой (EXECUTE_FAILED = −3), а поведение для остальных определяется драйвером. Этот массив позволяет найти проблемный оператор.
  • Исключение содержит SQLState (23505 — стандартный код нарушения ограничения целостности). В сочетании с окружающей транзакцией и rollback() именно так неудачная массовая загрузка оставляет базу данных нетронутой, а не частично изменённой.

Практика

Практика
Массовая вставка вызывает addBatch() в цикле, затем executeBatch(), и одна строка нарушает ограничение уникальности. Что делает драйвер и как найти проблемную строку?
Массовая вставка вызывает addBatch() в цикле, затем executeBatch(), и одна строка нарушает ограничение уникальности. Что делает драйвер и как найти проблемную строку?
Was this page helpful?