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:有效觸發條件,監控單一文件/users/marie。users/{username}:有效觸發條件,監控所有使用者文件。萬用字元則用來監控集合中的所有文件。users/{username}/addresses:無效觸發條件,指向子集合addresses,而不是文件。users/{username}/addresses/home:有效觸發條件,監控所有使用者的住家地址文件。users/{username}/addresses/{addressId}:有效觸發條件。監控所有地址文件。
使用萬用字元與參數
如果不知道要監控哪一個特定文件,請使用 {wildcard} 而不是文件 ID:
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 物件都包含一或多個 Value 物件。如需類型參照,請參閱 Value 說明文件。若您使用型別語言 (例如 Go) 編寫函式,這項功能就特別實用。
程式碼範例
下列範例 Cloud 函式會顯示觸發 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 函式名稱,可為原始碼中的函式名稱,或是任意字串。如果 FUNCTION_NAME 是任意字串,必須加入 --entry-point 旗標。 |
--entry-point ENTRY_POINT |
原始碼中函式或類別的名稱。這是選填欄位,但若您未使用 FUNCTION_NAME 在原始碼中指定要在部署期間執行的函式,就必須使用 --entry-point 提供可執行函式的名稱。 |
--runtime RUNTIME |
您使用的執行階段名稱。如需完整清單,請參閱 gcloud 參考資料。 |
--set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID |
專案的專屬 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 原生模式下既有的「(default)」資料庫,不支援 Firestore 的具名資料庫或 Datastore 模式,在後者的情況下,請改用 Cloud Run functions (第 2 代) 設定事件。
- 目前不支援跨專案設定 Cloud Run functions 和 Firestore 觸發條件。如要設定 Firestore 觸發條件,Cloud Run functions 必須位於同一個專案中。
- 函式的叫用並無一定順序。在快速變更的情形下,可能會以非預期的順序觸發函式叫用。
- 事件至少會傳送一次,但單一事件可能會導致多次函式叫用。請避免依賴「恰好一次」的機制,並務必編寫冪等函式。
- Datastore 模式下的 Firestore 需要 Cloud Run functions (第 2 代)。Cloud Run functions (第 1 代) 不支援 Datastore 模式。
- 每項觸發條件僅能關聯至單一資料庫,無法建立同時比對多個資料庫的觸發條件。
- 刪除資料庫時,相關觸發條件並不自動隨之刪除,僅會停止傳送事件,並持續保留到手動刪除為止。
- 如果比對符合事件超過要求大小上限,事件可能無法傳送至 Cloud Run functions (第 1 代)。
- 如果事件因要求大小而未能傳送,這些事件會記錄於平台記錄檔,並計入專案的記錄用量。
- 這些記錄位於 Logs Explorer 中,訊息為「Event cannot deliver to Cloud function due to size exceeding the limit for 1st gen...」(因事件大小超出第一代限制,無法傳遞至 Cloud 函式),嚴重程度為
error。函式名稱位於functionName欄位下方。如果receiveTimestamp欄位的時間戳記仍在一小時內,則可讀取該時間點前後的相關文件快照,推斷實際的事件內容。 - 如要避免此情況,建議方法如下:
- 遷移並升級至 Cloud Run functions (第 2 代)
- 縮減文件大小
- 刪除受影響的 Cloud Run functions
- 您可以使用排除條件關閉記錄功能,但請注意,超出限制的事件仍不會傳送。