Processamento em Lote com Java JDBC
Execute muitos comandos SQL com eficiência em Java usando JDBC batch processing — addBatch e executeBatch.
Quando você tem centenas ou milhares de inserções ou atualizações para executar, enviá-las uma a uma significa uma viagem de rede de ida e volta cada — o custo dominante. O processamento em lote coleta muitos comandos e os envia ao banco de dados em uma única viagem de ida e volta, muitas vezes convertendo segundos em milissegundos. É a técnica padrão para carregamento em massa.
Este capítulo aborda como enfileirar comandos com addBatch() e executá-los com executeBatch(), o que o int[] retornado significa (incluindo seus dois marcadores especiais), como envolver um lote em uma transação e como se recuperar quando um comando falha. Ele se baseia em JDBC PreparedStatement e JDBC Transactions.
addBatch e executeBatch
Você enfileira comandos com addBatch() e os dispara todos com executeBatch(), que retorna um int[] de contagens de atualização por comando:
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
}Com um PreparedStatement você vincula parâmetros e chama addBatch() por linha; com um Statement simples você passa uma string SQL completa para addBatch(sql). A forma preparada é preferida — a mesma segurança de vinculação de parâmetros (sem SQL injection) e benefícios de reutilização de plano como sempre. Note que um único lote de PreparedStatement deve executar uma string SQL fixa com parâmetros variados; se você precisar de comandos genuinamente diferentes, use um Statement simples.
O valor de retorno e seus marcadores especiais
executeBatch() retorna uma entrada por comando enfileirado. A maioria representa a contagem de linhas, mas duas constantes sinalizam casos especiais:
Statement.SUCCESS_NO_INFO(−2): o comando teve êxito, mas o driver não sabe quantas linhas ele afetou.Statement.EXECUTE_FAILED(−3): este comando específico falhou (visto apenas viaBatchUpdateException).
Lote + transação
Sempre execute um lote dentro de uma transação explícita (setAutoCommit(false)), para que uma falha reverta o lote inteiro em vez de deixá-lo parcialmente aplicado. Libere lotes grandes periodicamente — a cada ~1000 linhas — chamando executeBatch() e depois clearBatch(), para que a fila em memória do driver não cresça sem limite:
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 togetherComo todos os blocos compartilham uma transação, as chamadas periódicas de executeBatch() não confirmam nada por conta própria — o commit() no final é o que torna toda a carga durável, e um único rollback() desfaz tudo.
Quando um comando no lote falha
Se algum comando falhar, executeBatch() lança BatchUpdateException. Seu getUpdateCounts() retorna as contagens coletadas até aquele momento — permitindo que você veja quais comandos foram executados antes da falha — e carrega os dados habituais de SQLException, como getSQLState().
Um exemplo prático: contagens, marcadores e um lote com falha
Este programa constrói um lote, imprime as duas constantes de marcadores especiais, mostra o int[] que uma execução bem-sucedida retorna e constrói uma BatchUpdateException para demonstrar exatamente o que getUpdateCounts() relata quando um comando falha — tudo sem um banco de dados ativo.
O que aprender com a execução:
addBatch()enfileira trabalho sem enviá-lo;executeBatch()envia toda a fila em uma única viagem de ida e volta. O ganho está puramente na contagem de viagens — três inserções aqui, mas o mesmo padrão escala para milhares onde a economia é enorme.- Uma execução bem-sucedida retorna
[1, 1, 1]— uma contagem de atualização por comando enfileirado, em ordem. Você lê esse array para confirmar que cada comando afetou as linhas esperadas. SUCCESS_NO_INFO(−2) significa "funcionou, mas não estou contando linhas." Alguns drivers o retornam para comandos em lote, portanto trate qualquer valor negativo que não seja falha como sucesso, nunca como erro.- Em caso de falha, o driver lança
BatchUpdateException, egetUpdateCounts()retorna[1, -3, -2]: a primeira inserção teve êxito, a segunda falhou (EXECUTE_FAILED= −3), e o comportamento para o restante é definido pelo driver. Esse array é a forma de localizar o comando problemático. - A exceção carrega um
SQLState(23505é o código padrão de violação de restrição de integridade). Combinado com uma transação ao redor erollback(), é assim que uma carga em massa com falha deixa o banco de dados intocado em vez de parcialmente escrito.