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