Transakcje i zapisy zbiorcze

Cloud Firestore obsługuje operacje niepodzielne do odczytu i zapisu danych. W zbiorze operacji atomowych albo wszystkie operacje są wykonywane, albo żadna z nich nie jest stosowana. Istnieją 2 rodzaje operacji atomowych:Cloud Firestore

  • Transakcje: transakcja to zestaw operacji odczytu i zapisu dotyczących co najmniej jednego dokumentu.
  • Zapisywanie wsadowe: zapisywanie wsadowe to zestaw operacji zapisu na co najmniej jednym dokumencie.

Aktualizowanie danych za pomocą transakcji

Korzystając z bibliotek klienta Cloud Firestore, możesz grupować wiele operacji w jedną transakcję. Transakcje są przydatne, gdy chcesz zaktualizować wartość pola na podstawie jego bieżącej wartości lub wartości innego pola.

Transakcja składa się z dowolnej liczby operacji get(), po których następuje dowolna liczba operacji zapisu, takich jak set(), update() lub delete(). W przypadku równoczesnej zmiany Cloud Firestore uruchamia ponownie całą transakcję. Jeśli na przykład transakcja odczytuje dokumenty, a inny klient je zmodyfikuje, transakcja Cloud Firestore zostanie ponownie wykonana. Ta funkcja zapewnia, że transakcja jest przeprowadzana na podstawie aktualnych i spójnych danych.

Transakcje nigdy nie wprowadzają częściowych zapisów. Wszystkie operacje zapisu są wykonywane po zakończeniu udanej transakcji.

Podczas korzystania z transakcji pamiętaj o tych kwestiach:

  • Operacje odczytu muszą być wykonywane przed operacjami zapisu.
  • Funkcja wywołująca transakcję (funkcję transakcji) może zostać uruchomiona więcej niż raz, jeśli równoczesna zmiana wpływa na dokument czytany przez transakcję.
  • Funkcje transakcji nie powinny bezpośrednio modyfikować stanu aplikacji.
  • Transakcje nie będą realizowane, gdy klient jest offline.

Poniższy przykład pokazuje, jak utworzyć i wykonywać transakcję:

Web

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

// 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
Uwaga: ta usługa nie jest dostępna na urządzeniach z systemem watchOS i nie można jej używać w przypadku 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
Uwaga: ta usługa nie jest dostępna na urządzeniach z systemem watchOS i nie można jej używać w przypadku 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

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

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

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

Przekazywanie informacji z transakcji

Nie modyfikuj stanu aplikacji w ramach funkcji transakcji. Spowoduje to problemy z jednoczesnością, ponieważ funkcje transakcji mogą być wykonywane wielokrotnie i nie ma gwarancji, że będą działać w wątku interfejsu użytkownika. Zamiast tego przekaż informacje potrzebne do wykonania transakcji z funkcji transakcji. Poniższy przykład ilustruje sposób przekazywania informacji z transakcji:

Web

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

// 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
Uwaga: ta usługa nie jest dostępna na urządzeniach z systemem watchOS i nie można jej używać w przypadku 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
Uwaga: ta usługa nie jest dostępna na urządzeniach z systemem watchOS i nie można jej używać w przypadku 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

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

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

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

Nieudana transakcja

Transakcja może się nie udać z tych powodów:

  • Transakcja zawiera operacje odczytu po operacjach zapisu. Operacje odczytu muszą zawsze poprzedzać operacje zapisu.
  • Transakcja odczytuje dokument, który został zmodyfikowany poza transakcją. W takim przypadku transakcja zostanie automatycznie powtórzona. Transakcja jest powtarzana skończoną liczbę razy.
  • Transakcja przekroczyła maksymalny rozmiar żądania wynoszący 10 MiB.

    Rozmiar transakcji zależy od rozmiarów dokumentów i modyfikowanych przez nią wpisów w indeksie. W przypadku operacji usuwania obejmuje to rozmiar dokumentu docelowego i rozmiary wpisów indeksu usuniętych w odpowiedzi na tę operację.

Nieudana transakcja zwraca błąd i nie zapisuje niczego w bazie danych. Nie musisz cofać transakcji, ponieważ Cloud Firestorerobi to automatycznie.

Zapisy zbiorcze

Jeśli nie musisz odczytywać żadnych dokumentów w zbiorze operacji, możesz wykonać wiele operacji zapisu jako pojedynczą partię zawierającą dowolną kombinację operacji set(), update() lub delete(). Każda operacja w zbiorze jest zliczana osobno do wykorzystania usługi Cloud Firestore. Przetwarzanie zbiorczego zapisu jest wykonywane w sposób atomowy i może obejmować wiele dokumentów. Z tego przykładu dowiesz się, jak utworzyć i zatwierdzić partię zapisu:

Web

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

// 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
Uwaga: ta usługa nie jest dostępna na urządzeniach z systemem watchOS i nie można jej używać w przypadku 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
Uwaga: ta usługa nie jest dostępna na urządzeniach z systemem watchOS i nie można jej używać w przypadku 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

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

// 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

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

Podobnie jak transakcje, zapisy wsadowe są niepodzielne. W odróżnieniu od transakcji zbiorcze zapisy nie muszą zapewniać, aby odczytane dokumenty pozostawały niezmodyfikowane, co prowadzi do mniejszej liczby przypadków niepowodzenia. Nie są podatne na ponowne próby, ani na błędy wynikające ze zbyt wielu ponownych prób. Zapisy wsadowe są wykonywane nawet wtedy, gdy urządzenie użytkownika jest offline.

Zapis wsadowy obejmujący setki dokumentów może wymagać wielu aktualizacji indeksu i może przekroczyć limit rozmiaru transakcji. W takim przypadku zmniejsz liczbę dokumentów na partię. Aby zapisać dużą liczbę dokumentów, możesz użyć narzędzia do zapisu zbiorczego lub równoległego zapisu poszczególnych dokumentów.

Sprawdzanie poprawności danych w przypadku operacji atomowych

W przypadku bibliotek klienta mobilnego/internetowego możesz sprawdzać dane za pomocą narzędzia Cloud Firestore Security Rules. Możesz mieć pewność, że powiązane dokumenty są zawsze aktualizowane w sposób atomowy i zawsze w ramach transakcji lub zbiorczego zapisu. Za pomocą funkcji reguł zabezpieczeń getAfter() możesz uzyskiwać dostęp do dokumentu i weryfikować jego stan po zakończeniu zbioru operacji, ale przed ich zatwierdzeniem przez Cloud Firestore.

Załóżmy na przykład, że baza danych z przykładu cities zawiera też kolekcję countries. Każdy dokument country używa pola last_updated do śledzenia ostatniej aktualizacji miasta powiązanego z danym krajem. Zgodnie z tymi zasadami zabezpieczeń aktualizacja dokumentu city musi również zmienić powiązane pole last_updated w kraju:

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;
    }
  }
}

Limity reguł zabezpieczeń

W regułach bezpieczeństwa dotyczących transakcji lub zbiorczych operacji zapisu obowiązuje limit 20 wywołań dostępu do dokumentu dla całej operacji atomowej, oprócz normalnego limitu 10 wywołań dla każdej operacji na pojedynczym dokumencie w zbiorze.

Oto przykładowe reguły dla aplikacji do czatu:

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));
    }
  }
}

Fragmenty poniżej ilustrują liczbę wywołań dostępu do dokumentu używanych w przypadku kilku wzorców dostępu do danych:

// 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();

Więcej informacji o rozwiązywaniu problemów z opóźnieniami spowodowanymi przez duże i zbiorowe zapisy, błędy wynikające z konfliktów w przypadku nakładających się transakcji oraz inne problemy znajdziesz na stronie dotyczącej rozwiązywania problemów.