Android中如何异步执行Firestore多批次写入并实现失败全回滚?
Great question! Let's start by clearing up a key misunderstanding and then walk through practical solutions that fit your requirements.
First: The Critical Limitation of Your Current Code
Your existing implementation submits batches sequentially (waiting for one to finish before starting the next), which is slow. But even bigger issue: it doesn't actually roll back already completed batches if one fails. Firestore WriteBatch operations are independent atomic transactions—once a batch commits successfully, its changes are permanent and can't be undone. Your code only stops future batches, but data from successful batches stays in the database.
To get true "all-or-nothing" behavior (one failure = everything rolls back), we need to work around Firestore's lack of cross-batch atomicity. Here are your best options:
Option 1: Optimized Sequential Submission (Faster Code, No Rollback)
If you don't need strict atomicity (just want faster, cleaner sequential execution), we can use Google's Tasks API to simplify the chain of batch commits. This keeps the sequential flow but is more readable and maintainable than recursion.
btn_batch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { CollectionReference facultyRef = db.collection("Faculty"); List<WriteBatch> writeBatches = new ArrayList<>(); WriteBatch currentBatch = db.batch(); int counter = 0; // Prepare all batches (max 499 operations per batch) for(int i=1; i<=10000; i++){ String username = "user"+i; String password = UUID.randomUUID().toString().substring(0, 7); User user = new User(username, password); currentBatch.set(facultyRef.document(), user); counter++; if(counter == 499){ writeBatches.add(currentBatch); currentBatch = db.batch(); counter = 0; } } // Add the final partial batch if needed if(counter > 0){ writeBatches.add(currentBatch); } // Execute batches sequentially commitBatchesSequentially(writeBatches) .addOnCompleteListener(task -> { if(task.isSuccessful()){ Toast.makeText(MainActivity.this, "Batched Write Success", Toast.LENGTH_SHORT).show(); }else{ Toast.makeText(MainActivity.this, task.getException().getMessage(), Toast.LENGTH_SHORT).show(); } }); } }); private Task<Void> commitBatchesSequentially(List<WriteBatch> batches) { Task<Void> chain = Tasks.forResult(null); for (WriteBatch batch : batches) { chain = chain.continueWithTask(ignored -> batch.commit()); } return chain; }
Note: This still doesn't roll back completed batches if one fails—it just stops future ones. Use this only if partial success is acceptable.
Option 2: Status Marker + Compensation Rollback (Near-Atomic All-or-Nothing)
For true "all-or-nothing" behavior with large datasets, we need to implement a two-phase approach:
- Track the batch job with a status document
- Submit all batches in parallel
- If all succeed, mark the job as complete
- If any fail, delete all created documents to "roll back"
This isn't 100% atomic (there's a tiny window where partial data exists), but it works for most real-world use cases.
btn_batch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { CollectionReference facultyRef = db.collection("Faculty"); // Create a unique status document to track the batch job DocumentReference batchStatusDoc = db.collection("BatchJobs").document(UUID.randomUUID().toString()); List<WriteBatch> writeBatches = new ArrayList<>(); List<DocumentReference> createdDocs = new ArrayList<>(); WriteBatch currentBatch = db.batch(); int counter = 0; // Step 1: Prepare batches and track all new document references for(int i=1; i<=10000; i++){ DocumentReference newUserDoc = facultyRef.document(); createdDocs.add(newUserDoc); String username = "user"+i; String password = UUID.randomUUID().toString().substring(0, 7); User user = new User(username, password); currentBatch.set(newUserDoc, user); counter++; if(counter == 499){ writeBatches.add(currentBatch); currentBatch = db.batch(); counter = 0; } } if(counter > 0){ writeBatches.add(currentBatch); } // Step 2: Initialize job status as "in progress" batchStatusDoc.set(new BatchJobStatus("IN_PROGRESS")) .addOnSuccessListener(aVoid -> { // Step 3: Submit all batches in parallel List<Task<Void>> batchTasks = new ArrayList<>(); for (WriteBatch batch : writeBatches) { batchTasks.add(batch.commit()); } // Step 4: Wait for all batches to finish Tasks.whenAll(batchTasks) .addOnSuccessListener(ignored -> { // All batches succeeded: mark job as complete batchStatusDoc.update("status", "COMPLETED") .addOnSuccessListener(__ -> { Toast.makeText(MainActivity.this, "All Batches Completed Successfully!", Toast.LENGTH_SHORT).show(); }); }) .addOnFailureListener(e -> { // Batch failure: roll back all created documents rollbackCreatedDocuments(createdDocs) .addOnCompleteListener(rollbackTask -> { String statusMsg = rollbackTask.isSuccessful() ? "Batch failed, rolled back all changes" : "Batch failed, partial rollback: " + rollbackTask.getException().getMessage(); // Update status with failure details batchStatusDoc.update( "status", "FAILED", "errorMessage", e.getMessage() ).addOnSuccessListener(__ -> { Toast.makeText(MainActivity.this, statusMsg, Toast.LENGTH_LONG).show(); }); }); }); }) .addOnFailureListener(e -> { Toast.makeText(MainActivity.this, "Failed to start batch job: " + e.getMessage(), Toast.LENGTH_SHORT).show(); }); } }); // Helper method to roll back created documents private Task<Void> rollbackCreatedDocuments(List<DocumentReference> docs) { List<Task<Void>> rollbackTasks = new ArrayList<>(); WriteBatch rollbackBatch = db.batch(); int counter = 0; for (DocumentReference doc : docs) { rollbackBatch.delete(doc); counter++; if (counter == 499) { rollbackTasks.add(rollbackBatch.commit()); rollbackBatch = db.batch(); counter = 0; } } if (counter > 0) { rollbackTasks.add(rollbackBatch.commit()); } return Tasks.whenAll(rollbackTasks); } // Helper class for batch job status public static class BatchJobStatus { public String status; public String errorMessage; // Required empty constructor for Firestore serialization public BatchJobStatus() {} public BatchJobStatus(String status) { this.status = status; } }
Option 3: Firestore Transaction (Strict Atomicity for Small Datasets)
If your total number of write operations is ≤500, use a single Firestore Transaction. Transactions are strictly atomic—either all operations succeed, or none do, with automatic rollback.
btn_batch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { CollectionReference facultyRef = db.collection("Faculty"); db.runTransaction(transaction -> { // Limit: max 500 operations per transaction for(int i=1; i<=500; i++){ String username = "user"+i; String password = UUID.randomUUID().toString().substring(0, 7); User user = new User(username, password); transaction.set(facultyRef.document(), user); } return null; }) .addOnSuccessListener(aVoid -> { Toast.makeText(MainActivity.this, "Transaction Completed Successfully!", Toast.LENGTH_SHORT).show(); }) .addOnFailureListener(e -> { Toast.makeText(MainActivity.this, "Transaction Failed: " + e.getMessage(), Toast.LENGTH_SHORT).show(); }); } });
Final Recommendations
- Small datasets (<=500 operations): Use Option 3 (Transaction) for strict atomicity and simplicity.
- Large datasets + all-or-nothing requirement: Use Option 2 (Status Marker + Rollback) — it's the closest you can get to atomicity with Firestore's current limitations.
- Partial success acceptable: Use Option 1 (Optimized Sequential) or even parallel batch submission for faster execution.
内容的提问来源于stack exchange,提问作者Vince Ybañez

