Java JDBC Batch-Verarbeitung
Viele SQL-Anweisungen effizient in Java mit JDBC Batch-Verarbeitung ausführen — addBatch und executeBatch.
Wenn Sie Hunderte oder Tausende von Einfügungen oder Aktualisierungen durchführen müssen, bedeutet das einzelne Senden jeder Anweisung einen Netzwerk-Round-Trip pro Anweisung — der dominante Kostenfaktor. Batching sammelt viele Anweisungen und überträgt sie in einem einzigen Round-Trip an die Datenbank, was aus Sekunden oft Millisekunden macht. Es ist die Standardtechnik für Massenladevorgänge.
Dieses Kapitel erklärt, wie Anweisungen mit addBatch() in die Warteschlange gestellt und mit executeBatch() ausgeführt werden, was das zurückgegebene int[] bedeutet (einschließlich seiner zwei speziellen Marker), wie ein Batch in eine Transaktion eingebettet wird und wie man vorgeht, wenn eine Anweisung fehlschlägt. Es baut auf JDBC PreparedStatement und JDBC Transactions auf.
addBatch und executeBatch
Sie stellen Anweisungen mit addBatch() in die Warteschlange und führen sie alle mit executeBatch() aus, das ein int[] mit Update-Zählern pro Anweisung zurückgibt:
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
}Mit einem PreparedStatement binden Sie Parameter und rufen addBatch() pro Zeile auf; mit einem einfachen Statement übergeben Sie einen vollständigen SQL-String an addBatch(sql). Die vorbereitete Form wird bevorzugt — dieselbe Parameterübergabesicherheit (kein SQL-Injection) und Plan-Wiederverwendungsvorteile wie immer. Beachten Sie, dass ein einzelner PreparedStatement-Batch eine feste SQL-Zeichenkette mit variierenden Parametern ausführen muss; wenn Sie wirklich unterschiedliche Anweisungen benötigen, verwenden Sie ein einfaches Statement.
Der Rückgabewert und seine speziellen Marker
executeBatch() gibt einen Eintrag pro eingereihter Anweisung zurück. Die meisten sind die Zeilenanzahl, aber zwei Konstanten signalisieren Sonderfälle:
Statement.SUCCESS_NO_INFO(−2): Die Anweisung war erfolgreich, aber der Treiber weiß nicht, wie viele Zeilen betroffen waren.Statement.EXECUTE_FAILED(−3): Diese bestimmte Anweisung ist fehlgeschlagen (nur überBatchUpdateExceptionsichtbar).
Batch + Transaktion
Führen Sie einen Batch immer innerhalb einer expliziten Transaktion (setAutoCommit(false)) aus, damit ein Fehler den gesamten Batch zurücksetzt und nicht nur einen Teil davon angewendet lässt. Leeren Sie große Batches regelmäßig — etwa alle 1000 Zeilen — indem Sie executeBatch() und dann clearBatch() aufrufen, damit die In-Memory-Warteschlange des Treibers nicht unbegrenzt wächst:
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 togetherDa alle Chunks eine Transaktion teilen, führen die regelmäßigen executeBatch()-Aufrufe selbst kein Commit durch — commit() am Ende macht den gesamten Ladevorgang dauerhaft, und ein einzelnes rollback() macht alles rückgängig.
Wenn eine Anweisung im Batch fehlschlägt
Wenn eine Anweisung fehlschlägt, wirft executeBatch() eine BatchUpdateException. Ihre getUpdateCounts()-Methode gibt die bisher gesammelten Zähler zurück — so können Sie sehen, welche Anweisungen vor dem Fehler ausgeführt wurden — und sie enthält die üblichen SQLException-Daten wie getSQLState().
Ein praktisches Beispiel: Zähler, Marker und ein fehlgeschlagener Batch
Dieses Programm erstellt einen Batch, gibt die beiden speziellen Markerkonstanten aus, zeigt das int[], das ein fehlerfreier Durchlauf zurückgibt, und konstruiert eine BatchUpdateException, um genau zu demonstrieren, was getUpdateCounts() meldet, wenn eine Anweisung fehlschlägt — alles ohne eine live Datenbank.
Was aus dem Durchlauf zu entnehmen ist:
addBatch()stellt Arbeit in die Warteschlange, ohne sie zu senden;executeBatch()sendet die gesamte Warteschlange in einem einzigen Round-Trip. Der Vorteil liegt ausschließlich in der Anzahl der Round-Trips — hier drei Einfügungen, aber dieselbe Struktur skaliert auf Tausende, wo die Einsparung enorm ist.- Ein fehlerfreier Durchlauf gibt
[1, 1, 1]zurück — einen Update-Zähler pro eingereihter Anweisung, in Reihenfolge. Dieses Array wird gelesen, um zu bestätigen, dass jede Anweisung die erwarteten Zeilen beeinflusst hat. SUCCESS_NO_INFO(−2) bedeutet "es hat funktioniert, aber ich zähle keine Zeilen." Einige Treiber geben es für gebatchte Anweisungen zurück, daher sollte jeder negative, aber nicht fehlgeschlagene Wert als Erfolg behandelt werden, niemals als Fehler.- Bei einem Fehler wirft der Treiber
BatchUpdateException, undgetUpdateCounts()gibt[1, -3, -2]zurück: Die erste Einfügung war erfolgreich, die zweite schlug fehl (EXECUTE_FAILED= −3), und das Verhalten für den Rest ist treiberabhängig. Dieses Array zeigt, wo die fehlerhafte Anweisung liegt. - Die Ausnahme enthält einen
SQLState(23505ist der Standard-Integritätsverletzungscode). In Kombination mit einer umgebenden Transaktion undrollback()stellt dies sicher, dass ein fehlgeschlagener Massenladevorgang die Datenbank unberührt lässt statt sie nur teilweise zu beschreiben.