Transaksi dan batch operasi tulis

Firestore mendukung operasi atomik untuk membaca dan menulis data. Dalam kumpulan operasi atomik, seluruh operasi harus berhasil atau, jika ada yang tidak berhasil, semua tidak akan diterapkan. Ada dua jenis operasi atomik di Firestore:

  • Transaksi: transaksi adalah sekumpulan operasi baca dan tulis pada satu atau beberapa dokumen.
  • Batch Operasi Tulis: batch operasi tulis adalah sekumpulan operasi tulis di satu atau beberapa dokumen.

Memperbarui data dengan transaksi

Dengan library klien Firestore, Anda dapat mengelompokkan beberapa operasi ke dalam satu transaksi. Transaksi akan berguna saat Anda ingin memperbarui nilai kolom berdasarkan nilainya saat ini, atau nilai suatu kolom lainnya.

Suatu transaksi terdiri dari sejumlah operasi get() yang diikuti oleh sejumlah operasi tulis, seperti set(), update(), atau delete(). Untuk pengeditan serentak, Firestore akan menjalankan keseluruhan transaksi kembali. Misalnya, jika transaksi membaca beberapa dokumen dan salah satunya diubah oleh klien lain, Firestore akan mencoba menjalankan kembali transaksi. Fitur ini memastikan bahwa transaksi dilakukan pada data yang terkini dan konsisten.

Transaksi tidak pernah menerapkan penulisan secara parsial. Semua penulisan akan dijalankan di akhir transaksi yang sukses.

Saat menggunakan transaksi, perhatikan bahwa:

  • Operasi baca harus dilakukan sebelum operasi tulis.
  • Fungsi yang memanggil transaksi (fungsi transaksi) dapat dijalankan lebih dari satu kali jika pengeditan serentak memengaruhi dokumen yang dibaca transaksi tersebut.
  • Fungsi transaksi sebaiknya tidak langsung mengubah status aplikasi.
  • Transaksi akan gagal saat klien sedang offline.

Contoh berikut menunjukkan cara membuat dan menjalankan transaksi:

Web versi 9

import { runTransaction } from "firebase/firestore";

try {
  await runTransaction(db, async (transaction) => {
    const sfDoc = await transaction.get(sfDocRef);
    if (!sfDoc.exists()) {
      throw "Document does not exist!";
    }

    const newPopulation = sfDoc.data().population + 1;
    transaction.update(sfDocRef, { population: newPopulation });
  });
  console.log("Transaction successfully committed!");
} catch (e) {
  console.log("Transaction failed: ", e);
}

Web versi 8

// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");

// Uncomment to initialize the doc.
// sfDocRef.set({ population: 0 });

return db.runTransaction((transaction) => {
    // This code may get re-run multiple times if there are conflicts.
    return transaction.get(sfDocRef).then((sfDoc) => {
        if (!sfDoc.exists) {
            throw "Document does not exist!";
        }

        // Add one person to the city population.
        // Note: this could be done without a transaction
        //       by updating the population using FieldValue.increment()
        var newPopulation = sfDoc.data().population + 1;
        transaction.update(sfDocRef, { population: newPopulation });
    });
}).then(() => {
    console.log("Transaction successfully committed!");
}).catch((error) => {
    console.log("Transaction failed: ", error);
});
Swift
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
let sfReference = db.collection("cities").document("SF")

do {
  let _ = try await db.runTransaction({ (transaction, errorPointer) -> Any? in
    let sfDocument: DocumentSnapshot
    do {
      try sfDocument = transaction.getDocument(sfReference)
    } catch let fetchError as NSError {
      errorPointer?.pointee = fetchError
      return nil
    }

    guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
      let error = NSError(
        domain: "AppErrorDomain",
        code: -1,
        userInfo: [
          NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)"
        ]
      )
      errorPointer?.pointee = error
      return nil
    }

    // Note: this could be done without a transaction
    //       by updating the population using FieldValue.increment()
    transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
    return nil
  })
  print("Transaction successfully committed!")
} catch {
  print("Transaction failed: \(error)")
}
Objective-C
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
FIRDocumentReference *sfReference =
    [[self.db collectionWithPath:@"cities"] documentWithPath:@"SF"];
[self.db runTransactionWithBlock:^id (FIRTransaction *transaction, NSError **errorPointer) {
  FIRDocumentSnapshot *sfDocument = [transaction getDocument:sfReference error:errorPointer];
  if (*errorPointer != nil) { return nil; }

  if (![sfDocument.data[@"population"] isKindOfClass:[NSNumber class]]) {
    *errorPointer = [NSError errorWithDomain:@"AppErrorDomain" code:-1 userInfo:@{
      NSLocalizedDescriptionKey: @"Unable to retreive population from snapshot"
    }];
    return nil;
  }
  NSInteger oldPopulation = [sfDocument.data[@"population"] integerValue];

  // Note: this could be done without a transaction
  //       by updating the population using FieldValue.increment()
  [transaction updateData:@{ @"population": @(oldPopulation + 1) } forDocument:sfReference];

  return nil;
} completion:^(id result, NSError *error) {
  if (error != nil) {
    NSLog(@"Transaction failed: %@", error);
  } else {
    NSLog(@"Transaction successfully committed!");
  }
}];
Kotlin+KTX
Android
val sfDocRef = db.collection("cities").document("SF")

db.runTransaction { transaction ->
    val snapshot = transaction.get(sfDocRef)

    // Note: this could be done without a transaction
    //       by updating the population using FieldValue.increment()
    val newPopulation = snapshot.getDouble("population")!! + 1
    transaction.update(sfDocRef, "population", newPopulation)

    // Success
    null
}.addOnSuccessListener { Log.d(TAG, "Transaction success!") }
    .addOnFailureListener { e -> Log.w(TAG, "Transaction failure.", e) }
Java
Android
final DocumentReference sfDocRef = db.collection("cities").document("SF");

db.runTransaction(new Transaction.Function<Void>() {
    @Override
    public Void apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
        DocumentSnapshot snapshot = transaction.get(sfDocRef);

        // Note: this could be done without a transaction
        //       by updating the population using FieldValue.increment()
        double newPopulation = snapshot.getDouble("population") + 1;
        transaction.update(sfDocRef, "population", newPopulation);

        // Success
        return null;
    }
}).addOnSuccessListener(new OnSuccessListener<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
        Log.d(TAG, "Transaction success!");
    }
})
.addOnFailureListener(new OnFailureListener() {
    @Override
    public void onFailure(@NonNull Exception e) {
        Log.w(TAG, "Transaction failure.", e);
    }
});

Dart

final sfDocRef = db.collection("cities").doc("SF");
db.runTransaction((transaction) async {
  final snapshot = await transaction.get(sfDocRef);
  // Note: this could be done without a transaction
  //       by updating the population using FieldValue.increment()
  final newPopulation = snapshot.get("population") + 1;
  transaction.update(sfDocRef, {"population": newPopulation});
}).then(
  (value) => print("DocumentSnapshot successfully updated!"),
  onError: (e) => print("Error updating document $e"),
);
Java
// Initialize doc
final DocumentReference docRef = db.collection("cities").document("SF");
City city = new City("SF");
city.setCountry("USA");
city.setPopulation(860000L);
docRef.set(city).get();

// run an asynchronous transaction
ApiFuture<Void> futureTransaction =
    db.runTransaction(
        transaction -> {
          // retrieve document and increment population field
          DocumentSnapshot snapshot = transaction.get(docRef).get();
          long oldPopulation = snapshot.getLong("population");
          transaction.update(docRef, "population", oldPopulation + 1);
          return null;
        });
// block on transaction operation using transaction.get()
Python
transaction = db.transaction()
city_ref = db.collection("cities").document("SF")

@firestore.transactional
def update_in_transaction(transaction, city_ref):
    snapshot = city_ref.get(transaction=transaction)
    transaction.update(city_ref, {"population": snapshot.get("population") + 1})

update_in_transaction(transaction, city_ref)
Python
(Asinkron)
transaction = db.transaction()
city_ref = db.collection("cities").document("SF")

@firestore.async_transactional
async def update_in_transaction(transaction, city_ref):
    snapshot = await city_ref.get(transaction=transaction)
    transaction.update(city_ref, {"population": snapshot.get("population") + 1})

await update_in_transaction(transaction, city_ref)
C++
DocumentReference sf_doc_ref = db->Collection("cities").Document("SF");
db->RunTransaction([sf_doc_ref](Transaction& transaction,
                                std::string& out_error_message) -> Error {
    Error error = Error::kErrorOk;

    DocumentSnapshot snapshot =
        transaction.Get(sf_doc_ref, &error, &out_error_message);

    // Note: this could be done without a transaction by updating the
    // population using FieldValue::Increment().
    std::int64_t new_population =
        snapshot.Get("population").integer_value() + 1;
    transaction.Update(
        sf_doc_ref,
        {{"population", FieldValue::Integer(new_population)}});

    return Error::kErrorOk;
  }).OnCompletion([](const Future<void>& future) {
  if (future.error() == Error::kErrorOk) {
    std::cout << "Transaction success!" << std::endl;
  } else {
    std::cout << "Transaction failure: " << future.error_message() << std::endl;
  }
});
Node.js
// Initialize document
const cityRef = db.collection('cities').doc('SF');
await cityRef.set({
  name: 'San Francisco',
  state: 'CA',
  country: 'USA',
  capital: false,
  population: 860000
});

try {
  await db.runTransaction(async (t) => {
    const doc = await t.get(cityRef);

    // Add one person to the city population.
    // Note: this could be done without a transaction
    //       by updating the population using FieldValue.increment()
    const newPopulation = doc.data().population + 1;
    t.update(cityRef, {population: newPopulation});
  });

  console.log('Transaction success!');
} catch (e) {
  console.log('Transaction failure:', e);
}
Go

import (
	"context"
	"log"

	"cloud.google.com/go/firestore"
)

func runSimpleTransaction(ctx context.Context, client *firestore.Client) error {
	// ...

	ref := client.Collection("cities").Doc("SF")
	err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		doc, err := tx.Get(ref) // tx.Get, NOT ref.Get!
		if err != nil {
			return err
		}
		pop, err := doc.DataAt("population")
		if err != nil {
			return err
		}
		return tx.Set(ref, map[string]interface{}{
			"population": pop.(int64) + 1,
		}, firestore.MergeAll)
	})
	if err != nil {
		// Handle any errors appropriately in this section.
		log.Printf("An error has occurred: %s", err)
	}

	return err
}
PHP
$cityRef = $db->collection('samples/php/cities')->document('SF');
$db->runTransaction(function (Transaction $transaction) use ($cityRef) {
    $snapshot = $transaction->snapshot($cityRef);
    $newPopulation = $snapshot['population'] + 1;
    $transaction->update($cityRef, [
        ['path' => 'population', 'value' => $newPopulation]
    ]);
});
Unity
DocumentReference cityRef = db.Collection("cities").Document("SF");
db.RunTransactionAsync(transaction =>
    {
        return transaction.GetSnapshotAsync(cityRef).ContinueWith((snapshotTask) =>
        {
            DocumentSnapshot snapshot = snapshotTask.Result;
            long newPopulation = snapshot.GetValue<long>("Population") + 1;
            Dictionary<string, object> updates = new Dictionary<string, object>
            {
                { "Population", newPopulation}
            };
            transaction.Update(cityRef, updates);
        });
    });
C#
DocumentReference cityRef = db.Collection("cities").Document("SF");
await db.RunTransactionAsync(async transaction =>
{
    DocumentSnapshot snapshot = await transaction.GetSnapshotAsync(cityRef);
    long newPopulation = snapshot.GetValue<long>("Population") + 1;
    Dictionary<string, object> updates = new Dictionary<string, object>
    {
        { "Population", newPopulation}
    };
    transaction.Update(cityRef, updates);
});
Ruby
city_ref = firestore.doc "#{collection_path}/SF"

firestore.transaction do |tx|
  new_population = tx.get(city_ref).data[:population] + 1
  puts "New population is #{new_population}."
  tx.update city_ref, { population: new_population }
end

Meneruskan informasi dari transaksi

Jangan ubah status aplikasi di dalam fungsi transaksi Anda. Jika hal tersebut dilakukan, akan muncul masalah serentak. Hal ini terjadi karena fungsi transaksi dapat dijalankan beberapa kali, dan tidak dijamin dapat dijalankan di UI thread. Sebagai gantinya, teruskan informasi yang Anda butuhkan dari fungsi transaksi. Contoh berikut melanjutkan contoh sebelumnya untuk menunjukkan cara meneruskan informasi dari transaksi:

Web versi 9

import { doc, runTransaction } from "firebase/firestore";

// Create a reference to the SF doc.
const sfDocRef = doc(db, "cities", "SF");

try {
  const newPopulation = await runTransaction(db, async (transaction) => {
    const sfDoc = await transaction.get(sfDocRef);
    if (!sfDoc.exists()) {
      throw "Document does not exist!";
    }

    const newPop = sfDoc.data().population + 1;
    if (newPop <= 1000000) {
      transaction.update(sfDocRef, { population: newPop });
      return newPop;
    } else {
      return Promise.reject("Sorry! Population is too big");
    }
  });

  console.log("Population increased to ", newPopulation);
} catch (e) {
  // This will be a "population is too big" error.
  console.error(e);
}

Web versi 8

// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");

db.runTransaction((transaction) => {
    return transaction.get(sfDocRef).then((sfDoc) => {
        if (!sfDoc.exists) {
            throw "Document does not exist!";
        }

        var newPopulation = sfDoc.data().population + 1;
        if (newPopulation <= 1000000) {
            transaction.update(sfDocRef, { population: newPopulation });
            return newPopulation;
        } else {
            return Promise.reject("Sorry! Population is too big.");
        }
    });
}).then((newPopulation) => {
    console.log("Population increased to ", newPopulation);
}).catch((err) => {
    // This will be an "population is too big" error.
    console.error(err);
});
Swift
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
let sfReference = db.collection("cities").document("SF")

do {
  let object = try await db.runTransaction({ (transaction, errorPointer) -> Any? in
    let sfDocument: DocumentSnapshot
    do {
      try sfDocument = transaction.getDocument(sfReference)
    } catch let fetchError as NSError {
      errorPointer?.pointee = fetchError
      return nil
    }

    guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
      let error = NSError(
        domain: "AppErrorDomain",
        code: -1,
        userInfo: [
          NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)"
        ]
      )
      errorPointer?.pointee = error
      return nil
    }

    // Note: this could be done without a transaction
    //       by updating the population using FieldValue.increment()
    let newPopulation = oldPopulation + 1
    guard newPopulation <= 1000000 else {
      let error = NSError(
        domain: "AppErrorDomain",
        code: -2,
        userInfo: [NSLocalizedDescriptionKey: "Population \(newPopulation) too big"]
      )
      errorPointer?.pointee = error
      return nil
    }

    transaction.updateData(["population": newPopulation], forDocument: sfReference)
    return newPopulation
  })
  print("Population increased to \(object!)")
} catch {
  print("Error updating population: \(error)")
}
Objective-C
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
FIRDocumentReference *sfReference =
[[self.db collectionWithPath:@"cities"] documentWithPath:@"SF"];
[self.db runTransactionWithBlock:^id (FIRTransaction *transaction, NSError **errorPointer) {
  FIRDocumentSnapshot *sfDocument = [transaction getDocument:sfReference error:errorPointer];
  if (*errorPointer != nil) { return nil; }

  if (![sfDocument.data[@"population"] isKindOfClass:[NSNumber class]]) {
    *errorPointer = [NSError errorWithDomain:@"AppErrorDomain" code:-1 userInfo:@{
      NSLocalizedDescriptionKey: @"Unable to retreive population from snapshot"
    }];
    return nil;
  }
  NSInteger population = [sfDocument.data[@"population"] integerValue];

  population++;
  if (population >= 1000000) {
    *errorPointer = [NSError errorWithDomain:@"AppErrorDomain" code:-2 userInfo:@{
      NSLocalizedDescriptionKey: @"Population too big"
    }];
    return @(population);
  }

  [transaction updateData:@{ @"population": @(population) } forDocument:sfReference];

  return nil;
} completion:^(id result, NSError *error) {
  if (error != nil) {
    NSLog(@"Transaction failed: %@", error);
  } else {
    NSLog(@"Population increased to %@", result);
  }
}];
Kotlin+KTX
Android
val sfDocRef = db.collection("cities").document("SF")

db.runTransaction { transaction ->
    val snapshot = transaction.get(sfDocRef)
    val newPopulation = snapshot.getDouble("population")!! + 1
    if (newPopulation <= 1000000) {
        transaction.update(sfDocRef, "population", newPopulation)
        newPopulation
    } else {
        throw FirebaseFirestoreException(
            "Population too high",
            FirebaseFirestoreException.Code.ABORTED,
        )
    }
}.addOnSuccessListener { result ->
    Log.d(TAG, "Transaction success: $result")
}.addOnFailureListener { e ->
    Log.w(TAG, "Transaction failure.", e)
}
Java
Android
final DocumentReference sfDocRef = db.collection("cities").document("SF");

db.runTransaction(new Transaction.Function<Double>() {
    @Override
    public Double apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
        DocumentSnapshot snapshot = transaction.get(sfDocRef);
        double newPopulation = snapshot.getDouble("population") + 1;
        if (newPopulation <= 1000000) {
            transaction.update(sfDocRef, "population", newPopulation);
            return newPopulation;
        } else {
            throw new FirebaseFirestoreException("Population too high",
                    FirebaseFirestoreException.Code.ABORTED);
        }
    }
}).addOnSuccessListener(new OnSuccessListener<Double>() {
    @Override
    public void onSuccess(Double result) {
        Log.d(TAG, "Transaction success: " + result);
    }
})
.addOnFailureListener(new OnFailureListener() {
    @Override
    public void onFailure(@NonNull Exception e) {
        Log.w(TAG, "Transaction failure.", e);
    }
});

Dart

final sfDocRef = db.collection("cities").doc("SF");
db.runTransaction((transaction) {
  return transaction.get(sfDocRef).then((sfDoc) {
    final newPopulation = sfDoc.get("population") + 1;
    transaction.update(sfDocRef, {"population": newPopulation});
    return newPopulation;
  });
}).then(
  (newPopulation) => print("Population increased to $newPopulation"),
  onError: (e) => print("Error updating document $e"),
);
Java
final DocumentReference docRef = db.collection("cities").document("SF");
ApiFuture<String> futureTransaction =
    db.runTransaction(
        transaction -> {
          DocumentSnapshot snapshot = transaction.get(docRef).get();
          Long newPopulation = snapshot.getLong("population") + 1;
          // conditionally update based on current population
          if (newPopulation <= 1000000L) {
            transaction.update(docRef, "population", newPopulation);
            return "Population increased to " + newPopulation;
          } else {
            throw new Exception("Sorry! Population is too big.");
          }
        });
// Print information retrieved from transaction
System.out.println(futureTransaction.get());
Python
transaction = db.transaction()
city_ref = db.collection("cities").document("SF")

@firestore.transactional
def update_in_transaction(transaction, city_ref):
    snapshot = city_ref.get(transaction=transaction)
    new_population = snapshot.get("population") + 1

    if new_population < 1000000:
        transaction.update(city_ref, {"population": new_population})
        return True
    else:
        return False

result = update_in_transaction(transaction, city_ref)
if result:
    print("Population updated")
else:
    print("Sorry! Population is too big.")
Python
(Asinkron)
transaction = db.transaction()
city_ref = db.collection("cities").document("SF")

@firestore.async_transactional
async def update_in_transaction(transaction, city_ref):
    snapshot = await city_ref.get(transaction=transaction)
    new_population = snapshot.get("population") + 1

    if new_population < 1000000:
        transaction.update(city_ref, {"population": new_population})
        return True
    else:
        return False

result = await update_in_transaction(transaction, city_ref)
if result:
    print("Population updated")
else:
    print("Sorry! Population is too big.")
C++
// This is not yet supported.
Node.js
const cityRef = db.collection('cities').doc('SF');
try {
  const res = await db.runTransaction(async t => {
    const doc = await t.get(cityRef);
    const newPopulation = doc.data().population + 1;
    if (newPopulation <= 1000000) {
      await t.update(cityRef, { population: newPopulation });
      return `Population increased to ${newPopulation}`;
    } else {
      throw 'Sorry! Population is too big.';
    }
  });
  console.log('Transaction success', res);
} catch (e) {
  console.log('Transaction failure:', e);
}
Go

import (
	"context"
	"errors"
	"log"

	"cloud.google.com/go/firestore"
)

func infoTransaction(ctx context.Context, client *firestore.Client) (int64, error) {
	var updatedPop int64
	ref := client.Collection("cities").Doc("SF")
	err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		doc, err := tx.Get(ref)
		if err != nil {
			return err
		}
		pop, err := doc.DataAt("population")
		if err != nil {
			return err
		}
		newpop := pop.(int64) + 1
		if newpop <= 1000000 {
			err := tx.Set(ref, map[string]interface{}{
				"population": newpop,
			}, firestore.MergeAll)
			if err == nil {
				updatedPop = newpop
			}
			return err
		}
		return errors.New("population is too big")
	})
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	return updatedPop, err
}
PHP
$cityRef = $db->collection('samples/php/cities')->document('SF');
$transactionResult = $db->runTransaction(function (Transaction $transaction) use ($cityRef) {
    $snapshot = $transaction->snapshot($cityRef);
    $newPopulation = $snapshot['population'] + 1;
    if ($newPopulation <= 1000000) {
        $transaction->update($cityRef, [
            ['path' => 'population', 'value' => $newPopulation]
        ]);
        return true;
    } else {
        return false;
    }
});

if ($transactionResult) {
    printf('Population updated successfully.' . PHP_EOL);
} else {
    printf('Sorry! Population is too big.' . PHP_EOL);
}
Unity
DocumentReference cityRef = db.Collection("cities").Document("SF");
db.RunTransactionAsync(transaction =>
{
    return transaction.GetSnapshotAsync(cityRef).ContinueWith((task) =>
    {
        long newPopulation = task.Result.GetValue<long>("Population") + 1;
        if (newPopulation <= 1000000)
        {
            Dictionary<string, object> updates = new Dictionary<string, object>
            {
                { "Population", newPopulation}
            };
            transaction.Update(cityRef, updates);
            return true;
        }
        else
        {
            return false;
        } 
    });
}).ContinueWith((transactionResultTask) =>
{
    if (transactionResultTask.Result)
    {
        Console.WriteLine("Population updated successfully.");
    }
    else
    {
        Console.WriteLine("Sorry! Population is too big.");
    } 
});
C#
DocumentReference cityRef = db.Collection("cities").Document("SF");
bool transactionResult = await db.RunTransactionAsync(async transaction =>
{
    DocumentSnapshot snapshot = await transaction.GetSnapshotAsync(cityRef);
    long newPopulation = snapshot.GetValue<long>("Population") + 1;
    if (newPopulation <= 1000000)
    {
        Dictionary<string, object> updates = new Dictionary<string, object>
        {
            { "Population", newPopulation}
        };
        transaction.Update(cityRef, updates);
        return true;
    }
    else
    {
        return false;
    }
});

if (transactionResult)
{
    Console.WriteLine("Population updated successfully.");
}
else
{
    Console.WriteLine("Sorry! Population is too big.");
}
Ruby
city_ref = firestore.doc "#{collection_path}/SF"

updated = firestore.transaction do |tx|
  new_population = tx.get(city_ref).data[:population] + 1
  if new_population < 1_000_000
    tx.update city_ref, { population: new_population }
    true
  end
end

if updated
  puts "Population updated!"
else
  puts "Sorry! Population is too big."
end

Kegagalan transaksi

Transaksi bisa gagal karena alasan berikut:

  • Dalam transaksi tersebut, ada operasi baca yang berlangsung setelah operasi tulis. Seharusnya, operasi baca dilakukan sebelum operasi tulis.
  • Transaksi membaca dokumen yang telah diubah di luar transaksi. Dalam hal ini, transaksi akan berjalan kembali secara otomatis. Transaksi tersebut dicoba kembali beberapa kali.
  • Transaksi melebihi ukuran permintaan maksimum 10 MiB.

    Ukuran transaksi bergantung pada ukuran dokumen dan entri indeks yang dimodifikasi oleh transaksi. Untuk operasi penghapusan, ini termasuk ukuran dokumen target dan ukuran entri indeks yang dihapus sebagai tanggapan terhadap operasi.

Transaksi yang gagal akan menampilkan error dan tidak menulis apa pun ke database. Anda tidak perlu me-roll back transaksi; Firestore akan melakukannya secara otomatis.

Batch operasi tulis

Jika tidak perlu membaca dokumen apa pun dalam kumpulan operasi, Anda dapat menjalankan beberapa operasi tulis sebagai satu batch yang berisi kombinasi operasi set(), update(), atau delete(). Setiap operasi dalam batch dihitung secara terpisah terhadap penggunaan Firestore Anda. Batch operasi tulis akan selesai secara atomik dan dapat menulis ke beberapa dokumen. Contoh berikut menunjukkan cara mem-build dan meng-commit batch operasi tulis:

Web versi 9

import { writeBatch, doc } from "firebase/firestore"; 

// Get a new write batch
const batch = writeBatch(db);

// Set the value of 'NYC'
const nycRef = doc(db, "cities", "NYC");
batch.set(nycRef, {name: "New York City"});

// Update the population of 'SF'
const sfRef = doc(db, "cities", "SF");
batch.update(sfRef, {"population": 1000000});

// Delete the city 'LA'
const laRef = doc(db, "cities", "LA");
batch.delete(laRef);

// Commit the batch
await batch.commit();

Web versi 8

// Get a new write batch
var batch = db.batch();

// Set the value of 'NYC'
var nycRef = db.collection("cities").doc("NYC");
batch.set(nycRef, {name: "New York City"});

// Update the population of 'SF'
var sfRef = db.collection("cities").doc("SF");
batch.update(sfRef, {"population": 1000000});

// Delete the city 'LA'
var laRef = db.collection("cities").doc("LA");
batch.delete(laRef);

// Commit the batch
batch.commit().then(() => {
    // ...
});
Swift
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
// Get new write batch
let batch = db.batch()

// Set the value of 'NYC'
let nycRef = db.collection("cities").document("NYC")
batch.setData([:], forDocument: nycRef)

// Update the population of 'SF'
let sfRef = db.collection("cities").document("SF")
batch.updateData(["population": 1000000 ], forDocument: sfRef)

// Delete the city 'LA'
let laRef = db.collection("cities").document("LA")
batch.deleteDocument(laRef)

// Commit the batch
do {
  try await batch.commit()
  print("Batch write succeeded.")
} catch {
  print("Error writing batch: \(error)")
}
Objective-C
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
// Get new write batch
FIRWriteBatch *batch = [self.db batch];

// Set the value of 'NYC'
FIRDocumentReference *nycRef =
    [[self.db collectionWithPath:@"cities"] documentWithPath:@"NYC"];
[batch setData:@{} forDocument:nycRef];

// Update the population of 'SF'
FIRDocumentReference *sfRef =
    [[self.db collectionWithPath:@"cities"] documentWithPath:@"SF"];
[batch updateData:@{ @"population": @1000000 } forDocument:sfRef];

// Delete the city 'LA'
FIRDocumentReference *laRef =
    [[self.db collectionWithPath:@"cities"] documentWithPath:@"LA"];
[batch deleteDocument:laRef];

// Commit the batch
[batch commitWithCompletion:^(NSError * _Nullable error) {
  if (error != nil) {
    NSLog(@"Error writing batch %@", error);
  } else {
    NSLog(@"Batch write succeeded.");
  }
}];
Kotlin+KTX
Android
val nycRef = db.collection("cities").document("NYC")
val sfRef = db.collection("cities").document("SF")
val laRef = db.collection("cities").document("LA")

// Get a new write batch and commit all write operations
db.runBatch { batch ->
    // Set the value of 'NYC'
    batch.set(nycRef, City())

    // Update the population of 'SF'
    batch.update(sfRef, "population", 1000000L)

    // Delete the city 'LA'
    batch.delete(laRef)
}.addOnCompleteListener {
    // ...
}
Java
Android
// Get a new write batch
WriteBatch batch = db.batch();

// Set the value of 'NYC'
DocumentReference nycRef = db.collection("cities").document("NYC");
batch.set(nycRef, new City());

// Update the population of 'SF'
DocumentReference sfRef = db.collection("cities").document("SF");
batch.update(sfRef, "population", 1000000L);

// Delete the city 'LA'
DocumentReference laRef = db.collection("cities").document("LA");
batch.delete(laRef);

// Commit the batch
batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
    @Override
    public void onComplete(@NonNull Task<Void> task) {
        // ...
    }
});

Dart

// Get a new write batch
final batch = db.batch();

// Set the value of 'NYC'
var nycRef = db.collection("cities").doc("NYC");
batch.set(nycRef, {"name": "New York City"});

// Update the population of 'SF'
var sfRef = db.collection("cities").doc("SF");
batch.update(sfRef, {"population": 1000000});

// Delete the city 'LA'
var laRef = db.collection("cities").doc("LA");
batch.delete(laRef);

// Commit the batch
batch.commit().then((_) {
  // ...
});
Java
// Get a new write batch
WriteBatch batch = db.batch();

// Set the value of 'NYC'
DocumentReference nycRef = db.collection("cities").document("NYC");
batch.set(nycRef, new City());

// Update the population of 'SF'
DocumentReference sfRef = db.collection("cities").document("SF");
batch.update(sfRef, "population", 1000000L);

// Delete the city 'LA'
DocumentReference laRef = db.collection("cities").document("LA");
batch.delete(laRef);

// asynchronously commit the batch
ApiFuture<List<WriteResult>> future = batch.commit();
// ...
// future.get() blocks on batch commit operation
for (WriteResult result : future.get()) {
  System.out.println("Update time : " + result.getUpdateTime());
}
Python
batch = db.batch()

# Set the data for NYC
nyc_ref = db.collection("cities").document("NYC")
batch.set(nyc_ref, {"name": "New York City"})

# Update the population for SF
sf_ref = db.collection("cities").document("SF")
batch.update(sf_ref, {"population": 1000000})

# Delete DEN
den_ref = db.collection("cities").document("DEN")
batch.delete(den_ref)

# Commit the batch
batch.commit()
Python
(Asinkron)
batch = db.batch()

# Set the data for NYC
nyc_ref = db.collection("cities").document("NYC")
batch.set(nyc_ref, {"name": "New York City"})

# Update the population for SF
sf_ref = db.collection("cities").document("SF")
batch.update(sf_ref, {"population": 1000000})

# Delete DEN
den_ref = db.collection("cities").document("DEN")
batch.delete(den_ref)

# Commit the batch
await batch.commit()
C++
// Get a new write batch
WriteBatch batch = db->batch();

// Set the value of 'NYC'
DocumentReference nyc_ref = db->Collection("cities").Document("NYC");
batch.Set(nyc_ref, {});

// Update the population of 'SF'
DocumentReference sf_ref = db->Collection("cities").Document("SF");
batch.Update(sf_ref, {{"population", FieldValue::Integer(1000000)}});

// Delete the city 'LA'
DocumentReference la_ref = db->Collection("cities").Document("LA");
batch.Delete(la_ref);

// Commit the batch
batch.Commit().OnCompletion([](const Future<void>& future) {
  if (future.error() == Error::kErrorOk) {
    std::cout << "Write batch success!" << std::endl;
  } else {
    std::cout << "Write batch failure: " << future.error_message() << std::endl;
  }
});
Node.js
// Get a new write batch
const batch = db.batch();

// Set the value of 'NYC'
const nycRef = db.collection('cities').doc('NYC');
batch.set(nycRef, {name: 'New York City'});

// Update the population of 'SF'
const sfRef = db.collection('cities').doc('SF');
batch.update(sfRef, {population: 1000000});

// Delete the city 'LA'
const laRef = db.collection('cities').doc('LA');
batch.delete(laRef);

// Commit the batch
await batch.commit();
Go

import (
	"context"
	"log"

	"cloud.google.com/go/firestore"
)

func batchWrite(ctx context.Context, client *firestore.Client) error {
	// Get a new write batch.
	batch := client.Batch()

	// Set the value of "NYC".
	nycRef := client.Collection("cities").Doc("NYC")
	batch.Set(nycRef, map[string]interface{}{
		"name": "New York City",
	})

	// Update the population of "SF".
	sfRef := client.Collection("cities").Doc("SF")
	batch.Set(sfRef, map[string]interface{}{
		"population": 1000000,
	}, firestore.MergeAll)

	// Delete the city "LA".
	laRef := client.Collection("cities").Doc("LA")
	batch.Delete(laRef)

	// Commit the batch.
	_, err := batch.Commit(ctx)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}

	return err
}
PHP
$batch = $db->batch();

# Set the data for NYC
$nycRef = $db->collection('samples/php/cities')->document('NYC');
$batch->set($nycRef, [
    'name' => 'New York City'
]);

# Update the population for SF
$sfRef = $db->collection('samples/php/cities')->document('SF');
$batch->update($sfRef, [
    ['path' => 'population', 'value' => 1000000]
]);

# Delete LA
$laRef = $db->collection('samples/php/cities')->document('LA');
$batch->delete($laRef);

# Commit the batch
$batch->commit();
Unity
WriteBatch batch = db.StartBatch();

// Set the data for NYC
DocumentReference nycRef = db.Collection("cities").Document("NYC");
Dictionary<string, object> nycData = new Dictionary<string, object>
{
    { "name", "New York City" }
};
batch.Set(nycRef, nycData);

// Update the population for SF
DocumentReference sfRef = db.Collection("cities").Document("SF");
Dictionary<string, object> updates = new Dictionary<string, object>
{
    { "Population", 1000000}
};
batch.Update(sfRef, updates);

// Delete LA
DocumentReference laRef = db.Collection("cities").Document("LA");
batch.Delete(laRef);

// Commit the batch
batch.CommitAsync();
C#
WriteBatch batch = db.StartBatch();

// Set the data for NYC
DocumentReference nycRef = db.Collection("cities").Document("NYC");
Dictionary<string, object> nycData = new Dictionary<string, object>
{
    { "name", "New York City" }
};
batch.Set(nycRef, nycData);

// Update the population for SF
DocumentReference sfRef = db.Collection("cities").Document("SF");
Dictionary<string, object> updates = new Dictionary<string, object>
{
    { "Population", 1000000}
};
batch.Update(sfRef, updates);

// Delete LA
DocumentReference laRef = db.Collection("cities").Document("LA");
batch.Delete(laRef);

// Commit the batch
await batch.CommitAsync();
Ruby
firestore.batch do |b|
  # Set the data for NYC
  b.set "#{collection_path}/NYC", { name: "New York City" }

  # Update the population for SF
  b.update "#{collection_path}/SF", { population: 1_000_000 }

  # Delete LA
  b.delete "#{collection_path}/LA"
end

Seperti halnya transaksi, batch operasi tulis bersifat atomik. Berbeda dengan transaksi, batch operasi tulis tidak perlu memastikan bahwa dokumen yang dibaca tidak dimodifikasi sehingga dapat mengurangi kegagalan. Batch operasi tulis tidak akan dicoba ulang atau mengalami kegagalan karena terlalu banyak percobaan ulang. Penulisan batch tetap dijalankan meskipun perangkat pengguna sedang offline.

Operasi tulis batch dengan ratusan dokumen mungkin memerlukan banyak pembaruan indeks dan mungkin melebihi batas ukuran transaksi. Dalam kasus ini, kurangi jumlah dokumen per batch. Untuk menulis dokumen dalam jumlah besar, pertimbangkan untuk menggunakan penulis massal atau penulisan individual yang diparalelkan.

Validasi data untuk operasi atomik

Untuk library klien seluler/web, Anda dapat memvalidasi data menggunakan Aturan Keamanan Firestore. Anda dapat memastikan bahwa dokumen yang terkait selalu diperbarui secara atomik dan selalu menjadi bagian dari sebuah transaksi atau batch operasi tulis. Gunakan fungsi aturan keamanan getAfter() untuk mengakses dan memvalidasi status dokumen setelah serangkaian operasi selesai, tetapi sebelum Firestore meng-commit operasi.

Misalnya, anggaplah database untuk contoh cities juga berisi koleksi countries. Setiap dokumen country menggunakan kolom last_updated untuk melacak kapan terakhir kali kota yang terkait dengan negara itu diperbarui. Aturan keamanan berikut mensyaratkan bahwa perubahan pada dokumen city harus juga mengubah kolom last_updated negara terkait secara atomik:

service cloud.firestore {
  match /databases/{database}/documents {
    // If you update a city doc, you must also
    // update the related country's last_updated field.
    match /cities/{city} {
      allow write: if request.auth != null &&
        getAfter(
          /databases/$(database)/documents/countries/$(request.resource.data.country)
        ).data.last_updated == request.time;
    }

    match /countries/{country} {
      allow write: if request.auth != null;
    }
  }
}

Batas aturan keamanan

Dalam aturan keamanan untuk transaksi atau batch operasi tulis, ada batas 20 panggilan akses dokumen untuk keseluruhan operasi atomik, selain batas normal 10 panggilan untuk setiap operasi dokumen tunggal dalam batch.

Misalnya, pelajari aturan berikut untuk aplikasi chat:

service cloud.firestore {
  match /databases/{db}/documents {
    function prefix() {
      return /databases/{db}/documents;
    }
    match /chatroom/{roomId} {
      allow read, write: if request.auth != null && roomId in get(/$(prefix())/users/$(request.auth.uid)).data.chats
                            || exists(/$(prefix())/admins/$(request.auth.uid));
    }
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId
                            || exists(/$(prefix())/admins/$(request.auth.uid));
    }
    match /admins/{userId} {
      allow read, write: if request.auth != null && exists(/$(prefix())/admins/$(request.auth.uid));
    }
  }
}

Cuplikan di bawah ini mengilustrasikan jumlah panggilan akses dokumen yang digunakan untuk beberapa pola akses data:

// 0 document access calls used, because the rules evaluation short-circuits
// before the exists() call is invoked.
db.collection('user').doc('myuid').get(...);

// 1 document access call used. The maximum total allowed for this call
// is 10, because it is a single document request.
db.collection('chatroom').doc('mygroup').get(...);

// Initializing a write batch...
var batch = db.batch();

// 2 document access calls used, 10 allowed.
var group1Ref = db.collection("chatroom").doc("group1");
batch.set(group1Ref, {msg: "Hello, from Admin!"});

// 1 document access call used, 10 allowed.
var newUserRef = db.collection("users").doc("newuser");
batch.update(newUserRef, {"lastSignedIn": new Date()});

// 1 document access call used, 10 allowed.
var removedAdminRef = db.collection("admin").doc("otheruser");
batch.delete(removedAdminRef);

// The batch used a total of 2 + 1 + 1 = 4 document access calls, out of a total
// 20 allowed.
batch.commit();

Untuk mengetahui informasi lebih lanjut tentang cara menyelesaikan masalah latensi yang disebabkan oleh operasi tulis yang besar dan operasi tulis dalam batch, error akibat pertentangan dari transaksi yang tumpang-tindih, dan masalah lainnya, pertimbangkan untuk melihat halaman pemecahan masalah.