トランザクションと一括書き込み

Cloud Firestore は、データを読み書きするアトミック オペレーションをサポートしています。一連のアトミック オペレーションでは、すべてのオペレーションが正常に完了するか、またはどのオペレーションも適用されないかのいずれかです。Cloud Firestore には 2 種類のアトミック オペレーションがあります。

  • トランザクション: トランザクションは、1 つ以上のドキュメントに対して読み書きを行う一連のオペレーションです。
  • 一括書き込み: 一括書き込みは、1 つ以上のドキュメントに対して書き込みを行う一連のオペレーションです。

1 回のトランザクションまたは一括書き込みでは、最大 500 のドキュメントに書き込みを行うことができます。書き込みに関連するその他の制限については、割り当てと制限をご覧ください。

トランザクションでデータを更新する

Cloud Firestore クライアント ライブラリを使用して、複数のオペレーションを 1 つのトランザクションにまとめることができます。フィールドの値を、その現行値またはその他のフィールドの値に基づいて更新する場合には、トランザクションが便利です。カウンタを増分する場合には、カウンタの現行値を読み取り、カウンタを増分して、新しい値を Cloud Firestore に書き込むトランザクションを作成できます。

トランザクションは、任意の数の get() オペレーションと、その後に続く任意の数の書き込みオペレーション(set()update()delete() など)で構成されます。同時編集の場合、Cloud Firestore はトランザクション全体を再実行します。たとえば、トランザクションがドキュメントを読み取り、別のクライアントがそれらのドキュメントを変更すると、Cloud Firestore はトランザクションを再試行します。この機能により、常に整合性のある最新データに対してトランザクションが実行されます。

トランザクションでは、書き込みが部分的に適用されることはありません。成功したトランザクションの完了時にすべての書き込みが実行されます。

トランザクションを使用する場合は、次の点に注意してください。

  • 読み取りオペレーションは書き込みオペレーションの前に実行する必要があります。
  • トランザクションが読み取るドキュメントに対して同時編集が影響する場合は、トランザクションを呼び出す関数(トランザクション関数)が複数回実行されることがあります。
  • トランザクション関数はアプリケーションの状態を直接変更してはなりません。
  • クライアントがオフラインの場合、トランザクションは失敗します。

次の例は、トランザクションを作成して実行する方法を示します。

ウェブ
// 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(function(transaction) {
    // This code may get re-run multiple times if there are conflicts.
    return transaction.get(sfDocRef).then(function(sfDoc) {
        if (!sfDoc.exists) {
            throw "Document does not exist!";
        }

        var newPopulation = sfDoc.data().population + 1;
        transaction.update(sfDocRef, { population: newPopulation });
    });
}).then(function() {
    console.log("Transaction successfully committed!");
}).catch(function(error) {
    console.log("Transaction failed: ", error);
});
Swift
let sfReference = db.collection("cities").document("SF")

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
    }

    transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
    return nil
}) { (object, error) in
    if let error = error {
        print("Transaction failed: \(error)")
    } else {
        print("Transaction successfully committed!")
    }
}
Objective-C
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];

  [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!");
  }
}];
  
Android
final DocumentReference sfDocRef = db.collection("cities").document("SF");

db.runTransaction(new Transaction.Function<Void>() {
    @Override
    public Void apply(Transaction transaction) throws FirebaseFirestoreException {
        DocumentSnapshot snapshot = transaction.get(sfDocRef);
        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);
    }
});
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> transaction =
    db.runTransaction(
        new Transaction.Function<Void>() {
          @Override
          public Void updateCallback(Transaction transaction) throws Exception {
            // 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(u'cities').document(u'SF')

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

update_in_transaction(transaction, city_ref)
Node.js
// Initialize document
var cityRef = db.collection('cities').doc('SF');
var setCity = cityRef.set({
  name: 'San Francisco',
  state: 'CA',
  country: 'USA',
  capital: false,
  population: 860000
});

var transaction = db.runTransaction(t => {
  return t.get(cityRef)
      .then(doc => {
        // Add one person to the city population
        var newPopulation = doc.data().population + 1;
        t.update(cityRef, { population: newPopulation });
      });
}).then(result => {
  console.log('Transaction success!');
}).catch(err => {
  console.log('Transaction failure:', err);
});
Go
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 {
	return err
}
PHP
$cityRef = $db->collection('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]
    ]);
});

トランザクションから情報を渡す

トランザクション関数の内部でアプリケーション状態を変更しないでください。トランザクション関数は複数回実行でき、UI スレッドに対して実行される保証はないため、このように変更すると同時実行の問題が発生します。代わりに、トランザクション関数から必要な情報を渡します。次の例は、前の例に基づいて、トランザクションから情報を渡す方法を示します。

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

db.runTransaction(function(transaction) {
    return transaction.get(sfDocRef).then(function(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(function(newPopulation) {
    console.log("Population increased to ", newPopulation);
}).catch(function(err) {
    // This will be an "population is too big" error.
    console.error(err);
});
Swift
let sfReference = db.collection("cities").document("SF")

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
    }

    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
}) { (object, error) in
    if let error = error {
        print("Error updating population: \(error)")
    } else {
        print("Population increased to \(object!)")
    }
}
Objective-C
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);
  }
}];
  
Android
final DocumentReference sfDocRef = db.collection("cities").document("SF");

db.runTransaction(new Transaction.Function<Double>() {
    @Override
    public Double apply(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);
    }
});
Java
final DocumentReference docRef = db.collection("cities").document("SF");
ApiFuture<String> transaction =
    db.runTransaction(
        new Transaction.Function<String>() {
          @Override
          public String updateCallback(Transaction transaction) throws Exception {
            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(transaction.get());
Python
transaction = db.transaction()
city_ref = db.collection(u'cities').document(u'SF')

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

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

result = update_in_transaction(transaction, city_ref)
if result:
    print(u'Population updated')
else:
    print(u'Sorry! Population is too big.')
Node.js
var cityRef = db.collection('cities').doc('SF');
var transaction = db.runTransaction(t => {
  return t.get(cityRef)
      .then(doc => {
        var newPopulation = doc.data().population + 1;
        if (newPopulation <= 1000000) {
          t.update(cityRef, { population: newPopulation });
          return Promise.resolve('Population increased to ' + newPopulation);
        } else {
          return Promise.reject('Sorry! Population is too big.');
        }
      });
}).then(result => {
  console.log('Transaction success', result);
}).catch(err => {
  console.log('Transaction failure:', err);
});
Go
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 {
		return tx.Set(ref, map[string]interface{}{
			"population": pop.(int64) + 1,
		}, firestore.MergeAll)
	}
	return errors.New("population is too big")
})
if err != nil {
	return err
}
PHP
$cityRef = $db->collection('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);
}

トランザクションの失敗

トランザクションが失敗する可能性のある原因を次に示します。

  • トランザクションで書き込みオペレーションの後に読み取りオペレーションが実行される。読み取りオペレーションは常に、書き込みオペレーションの前に実行する必要があります。
  • トランザクションが、トランザクション外部で変更されたドキュメントを読み取る。この場合、トランザクションは自動的に再実行されます。トランザクションは一定の回数で再試行されます。

トランザクションが失敗するとエラーが返され、データベースには何も書き込まれません。トランザクションをロールバックする必要はありません。これは Cloud Firestore により自動的に実行されます。

一括書き込み

オペレーション セットでドキュメントを読み取る必要がない場合は、複数の書き込みオペレーションを 1 つのバッチとして実行できます。このバッチには、set()update()delete() オペレーションを自由に組み合わせて含めることができます。バッチの書き込みはアトミックに実行され、また複数のドキュメントに対する書き込みを実行できます。

一括書き込みは、大規模なデータセットを Cloud Firestore に移行する場合にも便利です。一括書き込みには最大 500 のオペレーションを含めることができ、一括オペレーションとともに使うことで接続のオーバーヘッドが軽減され、データ移行をより迅速に行うことができます。

一括書き込みではトランザクションよりも失敗が少なく、よりシンプルなコードが使用されます。一括書き込みは、整合性のあるドキュメントの読み取りに依存しないため、競合の問題の影響を受けません。一括書き込みは、ユーザーの端末がオフラインの場合でも実行できます。次の例は、書き込みのバッチをビルドして commit する方法を示します。

ウェブ
// 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(function () {
    // ...
});
Swift
// 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
batch.commit() { err in
    if let err = err {
        print("Error writing batch \(err)")
    } else {
        print("Batch write succeeded.")
    }
}
Objective-C
// 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.");
  }
}];
  
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) {
        // ...
    }
});
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(u'cities').document(u'NYC')
batch.set(nyc_ref, {u'name': u'New York City'})

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

# Delete LA
la_ref = db.collection(u'cities').document(u'LA')
batch.delete(la_ref)

# Commit the batch
batch.commit()
Node.js
// 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
return batch.commit().then(function () {
  // ...
});
Go
// 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 {
	return err
}
PHP
$batch = $db->batch();

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

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

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

# Commit the batch
$batch->commit();

アトミック オペレーションに関するデータの検証

モバイル / ウェブ クライアント ライブラリの場合は、Cloud Firestore セキュリティ ルールを使用してデータを検証できます。関連ドキュメントが常にアトミックに更新され、また常にトランザクションまたは一括書き込みの一環として更新されるようにすることができます。一連のオペレーションが完了した後の、Cloud Firestore がオペレーションを commit する前の時点で、ドキュメントの状態にアクセスして検証するには、getAfter() セキュリティ ルール関数を使用します。

たとえば、cities の例のデータベースに countries コレクションも含まれている場合を考えます。それぞれの country ドキュメントで、last_updated フィールドを使用して、その国に関連する都市が最後に更新された時刻を追跡します。次のセキュリティ ルールでは、city ドキュメントを更新する場合は、関連する国の last_updated フィールドもアトミックに更新することを要求しています。

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.uid != null &&
        getAfter(
          /databases/$(database)/documents/countries/$(request.resource.data.country)
        ).data.last_updated == request.time;
    }

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

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。