Google Cloud Firestore トリガー(第 1 世代)
Cloud Run functions の関数は、その関数と同じ Google Cloud プロジェクトの Firestore のイベントを処理できます。これらのイベントの発生時に、Firestore API とクライアント ライブラリを使用して Firestore の読み取りや更新を行うことができます。
一般的なライフサイクルの場合、Firestore の関数は次のように動作します。
特定のドキュメントに変更が加えられるのを待ちます。
イベントが発生するとトリガーされ、そのタスクを実行します。
影響を受けるドキュメントのスナップショットを含むデータ オブジェクトを受信します。
writeまたはupdateイベントの場合、トリガー イベントの前後のドキュメントの状態を表すスナップショットがデータ オブジェクトに含まれます。
イベントタイプ
Firestore は、create、update、delete、write イベントをサポートしています。write イベントには、ドキュメントに対するすべての変更が含まれます。
| イベントタイプ | トリガー |
|---|---|
providers/cloud.firestore/eventTypes/document.create(デフォルト) |
ドキュメントが最初に書き込まれたときにトリガーされます。 |
providers/cloud.firestore/eventTypes/document.update |
すでに存在するドキュメントの値が変更されたときにトリガーされます。 |
providers/cloud.firestore/eventTypes/document.delete |
データを含むドキュメントが削除されたときにトリガーされます。 |
providers/cloud.firestore/eventTypes/document.write |
ドキュメントが作成、更新、削除されたときにトリガーされます。 |
ワイルドカードは、次のように中括弧で記述します。
"projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}"
ドキュメント パスの指定
関数をトリガーするには、リッスンするドキュメント パスを指定します。関数はドキュメントの変更にのみ対応します。特定のフィールドまたはコレクションをモニタリングすることはできません。有効なドキュメント パスは次のとおりです。
users/marie: 有効なトリガー。1 つのドキュメント(/users/marie)をモニタリングします。users/{username}: 有効なトリガー。すべてのユーザー ドキュメントをモニタリングします。コレクション内のすべてのドキュメントをモニタリングする場合は、ワイルドカードを使用します。users/{username}/addresses: 無効なトリガー。ドキュメントではなく、サブコレクションaddressesを参照します。users/{username}/addresses/home: 有効なトリガー。すべてのユーザーの自宅住所のドキュメントをモニタリングします。users/{username}/addresses/{addressId}: 有効なトリガー。すべての住所ドキュメントをモニタリングします。
ワイルドカードとパラメータの使用
モニタリングするドキュメントがわからない場合は、ドキュメント ID の代わりに {wildcard} を使用します。
users/{username}は、すべてのユーザー ドキュメントに対する変更をリッスンします。
この例では、users にあるドキュメントの任意のフィールドが変更されると、{username} というワイルドカードと照合されます。
users に含まれるドキュメントにサブコレクションがある場合、サブコレクションのいずれかに含まれるドキュメントのフィールドが変更されても、{username} ワイルドカードはトリガーされません。
ワイルドカードに一致した部分が、ドキュメント パスから抽出されます。明示的なコレクションまたはドキュメント ID に置き換えるワイルドカードは、必要な数だけ定義できます。
イベントの構造
このトリガーは、次のようなイベントで関数を呼び出します。
{ "oldValue": { // Update and Delete operations only A Document object containing a pre-operation document snapshot }, "updateMask": { // Update operations only A DocumentMask object that lists changed fields. }, "value": { // A Document object containing a post-operation document snapshot } }
それぞれの Document オブジェクトに 1 つまたは複数の Value オブジェクトが含まれます。型の詳細については、Value ドキュメントをご覧ください。これは、Go などの型言語を使用して関数を記述する場合に便利です。
コードサンプル
以下の Cloud Functions の関数のサンプルでは、トリガーする Cloud Firestore イベントのフィールドを出力します。
Node.js
Python
Go
Java
C#
using CloudNative.CloudEvents; using Google.Cloud.Functions.Framework; using Google.Events.Protobuf.Cloud.Firestore.V1; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace FirebaseFirestore; public class Function : ICloudEventFunction<DocumentEventData> { private readonly ILogger _logger; public Function(ILogger<Function> logger) => _logger = logger; public Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken) { _logger.LogInformation("Function triggered by event on {subject}", cloudEvent.Subject); _logger.LogInformation("Event type: {type}", cloudEvent.Type); MaybeLogDocument("Old value", data.OldValue); MaybeLogDocument("New value", data.Value); // In this example, we don't need to perform any asynchronous operations, so the // method doesn't need to be declared async. return Task.CompletedTask; } /// <summary> /// Logs the names and values of the fields in a document in a very simplistic way. /// </summary> private void MaybeLogDocument(string message, Document document) { if (document is null) { return; } // ConvertFields converts the Firestore representation into a .NET-friendly // representation. IReadOnlyDictionary<string, object> fields = document.ConvertFields(); var fieldNamesAndTypes = fields .OrderBy(pair => pair.Key) .Select(pair => $"{pair.Key}: {pair.Value}"); _logger.LogInformation(message + ": {fields}", string.Join(", ", fieldNamesAndTypes)); } }
Ruby
PHP
use Google\CloudFunctions\CloudEvent; function firebaseFirestore(CloudEvent $cloudevent) { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); fwrite($log, 'Event: ' . $cloudevent->getId() . PHP_EOL); fwrite($log, 'Event Type: ' . $cloudevent->getType() . PHP_EOL); $data = $cloudevent->getData(); $resource = $data['resource']; fwrite($log, 'Function triggered by event on: ' . $resource . PHP_EOL); if (isset($data['oldValue'])) { fwrite($log, 'Old value: ' . json_encode($data['oldValue']) . PHP_EOL); } if (isset($data['value'])) { fwrite($log, 'New value: ' . json_encode($data['value']) . PHP_EOL); } }
次の例では、ユーザーが追加した値を取得し、その場所にある文字列を大文字に変換して、値を大文字の文字列に置き換えています。
Node.js
Python
Go
Java
C#
using CloudNative.CloudEvents; using Google.Cloud.Firestore; using Google.Cloud.Functions.Framework; using Google.Cloud.Functions.Hosting; using Google.Events.Protobuf.Cloud.Firestore.V1; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace FirestoreReactive; public class Startup : FunctionsStartup { public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) => services.AddSingleton(FirestoreDb.Create()); } // Register the startup class to provide the Firestore dependency. [FunctionsStartup(typeof(Startup))] public class Function : ICloudEventFunction<DocumentEventData> { private readonly ILogger _logger; private readonly FirestoreDb _firestoreDb; public Function(ILogger<Function> logger, FirestoreDb firestoreDb) => (_logger, _firestoreDb) = (logger, firestoreDb); public async Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken) { // Get the recently-written value. This expression will result in a null value // if any of the following is true: // - The event doesn't contain a "new" document // - The value doesn't contain a field called "original" // - The "original" field isn't a string string currentValue = data.Value?.ConvertFields().GetValueOrDefault("original") as string; if (currentValue is null) { _logger.LogWarning($"Event did not contain a suitable document"); return; } string newValue = currentValue.ToUpperInvariant(); if (newValue == currentValue) { _logger.LogInformation("Value is already upper-cased; no replacement necessary"); return; } // The CloudEvent subject is "documents/x/y/...". // The Firestore SDK FirestoreDb.Document method expects a reference relative to // "documents" (so just the "x/y/..." part). This may be simplified over time. if (cloudEvent.Subject is null || !cloudEvent.Subject.StartsWith("documents/")) { _logger.LogWarning("CloudEvent subject is not a document reference."); return; } string documentPath = cloudEvent.Subject.Substring("documents/".Length); _logger.LogInformation("Replacing '{current}' with '{new}' in '{path}'", currentValue, newValue, documentPath); await _firestoreDb.Document(documentPath).UpdateAsync("original", newValue, cancellationToken: cancellationToken); } }
Ruby
PHP
use Google\Cloud\Firestore\FirestoreClient; use Google\CloudFunctions\CloudEvent; function firebaseReactive(CloudEvent $cloudevent) { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); $data = $cloudevent->getData(); $resource = $data['value']['name']; $db = new FirestoreClient(); $docPath = explode('/documents/', $resource)[1]; $affectedDoc = $db->document($docPath); $curValue = $data['value']['fields']['original']['stringValue']; $newValue = strtoupper($curValue); if ($curValue !== $newValue) { fwrite($log, 'Replacing value: ' . $curValue . ' --> ' . $newValue . PHP_EOL); $affectedDoc->set(['original' => $newValue]); } else { // Value is already upper-case // Don't perform another write (it might cause an infinite loop) fwrite($log, 'Value is already upper-case.' . PHP_EOL); } }
関数のデプロイ
次の gcloud コマンドは、ドキュメント /messages/{pushId} に対する書き込みイベントによってトリガーされる関数をデプロイします。
gcloud functions deploy FUNCTION_NAME \ --no-gen2 \ --entry-point ENTRY_POINT \ --runtime RUNTIME \ --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID \ --trigger-event "providers/cloud.firestore/eventTypes/document.write" \ --trigger-resource "projects/YOUR_PROJECT_ID/databases/(default)/documents/messages/{pushId}"
| 引数 | 説明 |
|---|---|
FUNCTION_NAME |
デプロイする Cloud Functions の関数の登録名。ソースコード内の関数の名前にすることも、任意の文字列にすることもできます。FUNCTION_NAME が任意の文字列の場合は、--entry-point フラグを含める必要があります。 |
--entry-point ENTRY_POINT |
ソースコード内の関数またはクラスの名前。FUNCTION_NAME を使用して、デプロイ時に実行する関数をソースコードに指定していない場合は省略できます。その場合は、--entry-point を使用して実行可能関数の名前を指定する必要があります。 |
--runtime RUNTIME |
使用しているランタイムの名前。網羅的なリストについては、gcloud リファレンスをご覧ください。 |
--set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID |
ランタイム環境変数としてのプロジェクトの固有識別子。 |
--trigger-event NAME |
関数がモニタリングするイベントタイプ(write、create、update または delete)。 |
--trigger-resource NAME |
関数がリッスンするデータベース パスの完全修飾名。次の形式に従う必要があります。
"projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH"
{pushId} テキストは、ドキュメント パスの指定で説明したワイルドカード パラメータです。 |
制限事項
Cloud Run functions の Firestore トリガーには、次の制限事項があります。
- Cloud Run functions(第 1 世代)では、Firestore ネイティブ モードの既存の「(デフォルト)」データベースがあることが前提となります。Firestore の名前付きデータベースや Datastore モードはサポートされていません。これらを使用している場合にイベントを構成するには、Cloud Run functions(第 2 世代)を使用してください。
- Cloud Run functions と Firestore トリガーを使用したクロス プロジェクト設定は制限事項です。Firestore トリガーを設定するには、Cloud Run functions が同じプロジェクトに存在する必要があります。
- 順序は保証されません。短時間に複数の変更を行うと、予期しない順序で関数の呼び出しがトリガーされることがあります。
- イベントは必ず 1 回以上処理されますが、1 つのイベントで関数が複数回呼び出される場合があります。「正確に 1 回」のメカニズムに依存することは避け、べき等になるように関数を記述してください。
- Datastore モードの Firestore には、Cloud Run functions(第 2 世代)が必要です。Cloud Run functions(第 1 世代)では、Datastore モードはサポートされていません。
- トリガーは、単一のデータベースに関連付けられます。複数のデータベースに一致するトリガーは作成できません。
- データベースを削除しても、そのデータベースのトリガーは自動的に削除されません。トリガーはイベントの配信を停止しますが、トリガーを削除するまで存在し続けます。
- 一致したイベントが最大リクエスト サイズを超えると、Cloud Run functions(第 1 世代)に配信されない可能性があります。
- リクエスト サイズが原因で配信されなかったイベントは、プラットフォーム ログに記録され、プロジェクトのログ使用量にカウントされます。
- これらのログは、ログ エクスプローラで「サイズが第 1 世代の上限を超えているため、イベントを Cloud Functions に配信できません...」という
error重大度メッセージとともに表示されます。関数名はfunctionNameフィールドで確認できます。receiveTimestampフィールドが現在から 1 時間以内であれば、タイムスタンプの前後のスナップショットで問題のドキュメントを読み取ることで、実際のイベントの内容を推測できます。 - このようなケイデンスを回避するには、次のようにします。
- Cloud Run functions(第 2 世代)への移行とアップグレードを行う
- ドキュメントのサイズを縮小する
- 問題の Cloud Run 関数を削除する
- 除外を使用してロギング自体を無効にすることもできますが、問題のあるイベントは引き続き配信されないことに注意してください。