W3docs

Java JDBC Batch Processing

Esegui molte istruzioni SQL in modo efficiente in Java con il batch processing JDBC — addBatch e executeBatch.

Quando devi eseguire centinaia o migliaia di insert o update, inviarli uno alla volta significa un round trip di rete per ciascuno — il costo dominante. Il batching raccoglie molte istruzioni e le invia al database in un unico round trip, spesso trasformando i secondi in millisecondi. È la tecnica standard per il caricamento massivo.

Questo capitolo spiega come accodare le istruzioni con addBatch() e come eseguirle con executeBatch(), cosa significa l'int[] restituito (inclusi i suoi due marcatori speciali), come racchiudere un batch in una transazione e come gestire il caso in cui un'istruzione fallisca. Si basa su JDBC PreparedStatement e JDBC Transactions.

addBatch e executeBatch

Accodi le istruzioni con addBatch() e le esegui tutte con executeBatch(), che restituisce un int[] con i conteggi degli aggiornamenti per ciascuna istruzione:

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
}

Con un PreparedStatement si associano i parametri e si chiama addBatch() per ogni riga; con uno Statement semplice si passa una stringa SQL completa a addBatch(sql). La forma preparata è preferibile — stessa sicurezza nell'associazione dei parametri (nessuna SQL injection) e stessi vantaggi di riutilizzo del piano come sempre. Si noti che un singolo batch di PreparedStatement deve eseguire una stringa SQL fissa con parametri variabili; se hai bisogno di istruzioni genuinamente diverse, usa uno Statement semplice.

Il valore restituito e i suoi marcatori speciali

executeBatch() restituisce un elemento per ogni istruzione accodata. La maggior parte sono conteggi di righe, ma due costanti segnalano casi particolari:

  • Statement.SUCCESS_NO_INFO (−2): l'istruzione è riuscita, ma il driver non sa quante righe ha interessato.
  • Statement.EXECUTE_FAILED (−3): questa particolare istruzione è fallita (visibile solo tramite BatchUpdateException).

Batch + transazione

Esegui sempre un batch all'interno di una transazione esplicita (setAutoCommit(false)), in modo che un errore faccia il rollback dell'intero batch anziché lasciarlo applicato a metà. Svuota i batch di grandi dimensioni periodicamente — ogni ~1000 righe — chiamando executeBatch() e poi clearBatch(), in modo che la coda in memoria del driver non cresca senza 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 together

Poiché tutti i chunk condividono una transazione, le chiamate periodiche a executeBatch() non eseguono alcun commit autonomamente — è il commit() finale che rende l'intero caricamento durevole, e un singolo rollback() annulla tutto.

Quando un'istruzione nel batch fallisce

Se un'istruzione fallisce, executeBatch() lancia BatchUpdateException. Il suo metodo getUpdateCounts() restituisce i conteggi raccolti fino a quel momento — permettendoti di vedere quali istruzioni sono state eseguite prima del fallimento — e contiene i consueti dati SQLException come getSQLState().

Un esempio pratico: conteggi, marcatori e un batch fallito

Questo programma costruisce un batch, stampa le due costanti di marcatore speciale, mostra l'int[] che una esecuzione pulita restituisce, e costruisce una BatchUpdateException per dimostrare esattamente cosa riporta getUpdateCounts() quando un'istruzione fallisce — tutto senza un database reale.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • addBatch() accoda il lavoro senza inviarlo; executeBatch() invia l'intera coda in un unico round trip. Il vantaggio sta esclusivamente nel numero di round trip — tre insert qui, ma la stessa struttura scala a migliaia dove il risparmio è enorme.
  • Un'esecuzione pulita restituisce [1, 1, 1] — un conteggio degli aggiornamenti per ogni istruzione accodata, in ordine. Leggi questo array per confermare che ogni istruzione ha interessato le righe che ti aspettavi.
  • SUCCESS_NO_INFO (−2) significa "ha funzionato ma non conto le righe". Alcuni driver lo restituiscono per le istruzioni in batch, quindi tratta qualsiasi valore negativo-ma-non-fallito come successo, mai come errore.
  • In caso di errore il driver lancia BatchUpdateException, e getUpdateCounts() restituisce [1, -3, -2]: il primo insert è riuscito, il secondo è fallito (EXECUTE_FAILED = −3), e il comportamento per il resto è definito dal driver. Quell'array è il modo per individuare l'istruzione problematica.
  • L'eccezione contiene un SQLState (23505 è il codice standard di violazione del vincolo di integrità). Combinato con una transazione circostante e rollback(), questo è il modo in cui un caricamento massivo fallito lascia il database intatto anziché parzialmente scritto.

Esercitazione

Pratica
Un insert massivo chiama addBatch() in un ciclo e poi executeBatch(), e una riga viola un vincolo di unicità. Cosa fa il driver e come si individua la riga che ha causato il fallimento?
Un insert massivo chiama addBatch() in un ciclo e poi executeBatch(), e una riga viola un vincolo di unicità. Cosa fa il driver e come si individua la riga che ha causato il fallimento?
Was this page helpful?