Google Cloud Firestore 觸發條件 (第 1 代)
Cloud Run 函式可處理函式所在的 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 函式的 Firestore 觸發條件有下列限制:
- Cloud Run 函式 (第 1 代) 的先決條件為在 Firestore 原生模式中使用現有的「(預設)」資料庫。不支援 Firestore 命名資料庫或 Datastore 模式。在這種情況下,請使用 Cloud Run 函式 (第 2 代) 設定事件。
- 我們不保證排序。快速變更可能會以非預期的順序觸發函式叫用。
- 事件至少會傳送一次,但單一事件可能會導致多次函式叫用。避免依賴「一次一律」機制,並編寫冪等函式。
- Firestore (Datastore 模式) 需要 Cloud Run 函式 (第 2 代)。Cloud Run 函式 (第 1 代) 不支援 Datastore 模式。
- 觸發條件會與單一資料庫相關聯。您無法建立可比對多個資料庫的觸發事件。
- 刪除資料庫時,系統不會自動刪除該資料庫的任何觸發事件。觸發事件會停止傳送事件,但會持續存在,直到您刪除觸發事件為止。
- 如果相符的事件超過最大要求大小,事件可能無法傳送至 Cloud Run 函式 (第 1 代)。
- 由於要求大小而未送達的事件會記錄在平台記錄中,並計入專案的記錄用量。
- 您可以在「Logs Explorer」中找到這些記錄,其中的訊息為「Event cannot deliver to Cloud function due to size exceeding the limit for 1st gen...」(由於大小超出第 1 代限制,事件無法傳送至 Cloud 函式),嚴重性為
error
。您可以在functionName
欄位下方找到函式名稱。如果receiveTimestamp
欄位仍在現在起一小時內,您可以透過時間戳記前後的快照,讀取相關文件,進而推斷實際事件內容。 - 如要避免這種節奏,您可以採取下列做法:
- 遷移至 Cloud Run 函式 (第 2 代) 並升級
- 縮小文件大小
- 刪除問題的 Cloud Run 函式
- 您可以使用排除項目關閉記錄功能,但請注意,系統仍不會傳送違規事件。