טריגרים של Google Cloud Firestore (דור ראשון)
פונקציות Cloud Run יכולות לטפל באירועים ב-Firestore באותו פרויקט שבו נמצאת הפונקציה. Google Cloud אפשר לקרוא או לעדכן את 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}
במקום במזהה המסמך:
-
users/{username}מאזין לשינויים בכל המסמכים של המשתמשים.
בדוגמה הזו, כשמשנים שדה כלשהו במסמך כלשהו ב-users, הוא תואם לתו כל כללי שנקרא {username}.
אם במסמך ב-users יש אוספי משנה, ושדה באחד מהמסמכים של אוספי המשנה האלה משתנה, התו הכללי {username} לא מופעל.
התאמות של תווים כלליים לחיפוש מחולצות מנתיבי מסמכים. אפשר להגדיר כמה תווים כלליים שרוצים כדי להחליף מזהים מפורשים של אוספים או מסמכים.
מבנה האירוע
הטריגר הזה מפעיל את הפונקציה עם אירוע שדומה לזה שמוצג בהמשך:
{ "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 Function לדוגמה שבהמשך מדפיסה את השדות של אירוע 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} הוא פרמטר wildcard שמתואר למעלה
בקטע ציון נתיב המסמך.
|
מגבלות
שימו לב למגבלות הבאות לגבי טריגרים של Firestore לפונקציות Cloud Run:
- פונקציות Cloud Run (דור ראשון) דורשות מסד נתונים קיים '(default)' במצב Firestore Native. הוא לא תומך במסדי נתונים בעלי שם ב-Firestore או במצב Datastore. במקרים כאלה, צריך להשתמש בפונקציות Cloud Run (דור שני) כדי להגדיר אירועים.
- הגדרה של פרויקטים שונים באמצעות פונקציות Cloud Run וטריגר של Firestore היא מגבלה. כדי להגדיר פונקציות Cloud Run להפעלה על ידי Firestore, הן צריכות להיות באותו פרויקט.
- אנחנו לא מתחייבים לגבי סדר ההזמנה. שינויים מהירים יכולים להפעיל קריאות לפונקציות בסדר לא צפוי.
- האירועים מועברים לפחות פעם אחת, אבל אירוע יחיד עשוי להוביל להפעלות מרובות של פונקציות. מומלץ להימנע מהסתמכות על מנגנונים של 'פעם אחת בדיוק', ולכתוב פונקציות אידמפוטנטיות.
- Firestore במצב Datastore דורש פונקציות Cloud Run (דור שני). פונקציות Cloud Run (דור ראשון) לא תומכות במצב Datastore.
- טריגר משויך למסד נתונים יחיד. אי אפשר ליצור טריגר שתואם לכמה מסדי נתונים.
- מחיקה של מסד נתונים לא תגרום למחיקה אוטומטית של טריגרים במסד הנתונים הזה. הטריגר מפסיק להעביר אירועים אבל ממשיך להתקיים עד שמוחקים את הטריגר.
- אם אירוע תואם חורג מהגודל המקסימלי של הבקשה, יכול להיות שהאירוע לא יועבר לפונקציות Cloud Run (דור ראשון).
- אירועים שלא נמסרו בגלל גודל הבקשה מתועדים ביומני הפלטפורמה ונספרים בשימוש ביומן של הפרויקט.
- אפשר למצוא את היומנים האלה בכלי Logs Explorer עם ההודעה "לא ניתן למסור את האירוע אל
פונקציית Cloud בגלל שהגודל חורג מהמגבלה של דור ראשון..." ברמת החומרה
error. שם הפונקציה מופיע בשדהfunctionName. אם השדהreceiveTimestampעדיין נמצא בטווח של שעה מהזמן הנוכחי, אפשר להסיק את תוכן האירוע בפועל על ידי קריאת המסמך המדובר עם תמונת מצב לפני ואחרי חותמת הזמן. - כדי להימנע מקצב צעדים כזה, אתם יכולים:
- העברה ושדרוג לפונקציות Cloud Run (דור שני)
- הקטנת המסמך
- מחיקת פונקציות Cloud Run הרלוונטיות
- אפשר להשבית את הרישום עצמו באמצעות החרגות, אבל חשוב לזכור שהאירועים הבעייתיים עדיין לא יועברו.