הרחבת Cloud Firestore באמצעות Cloud Functions (דור שני)

בעזרת Cloud Functions, אפשר לפרוס קוד לטיפול באירועים שמופעלים על ידי שינויים במסד הנתונים Cloud Firestore. כך תוכלו להוסיף בקלות פונקציונליות בצד השרת לאפליקציה, בלי להפעיל שרתים משלכם.

Cloud Functions (דור שני)

Cloud Functions for Firebase (דור שני) מבוסס על Cloud Run ו-Eventarc, ומספק תשתית חזקה יותר, שליטה מתקדמת יותר בביצועים וביכולת ההתאמה לעומס ושליטה רבה יותר בסביבת זמן הריצה של הפונקציות. למידע נוסף על דור שני, ראו Cloud Functions for Firebase (דור שני). למידע נוסף על Cloud Firestore מדור ראשון, תוכלו לקרוא את המאמר הרחבת Cloud Firestore באמצעות Cloud Functions.

טריגרים של פונקציית Cloud Firestore

ערכת ה-SDK של Cloud Functions for Firebase מייצאת את טריגרי האירועים הבאים של Cloud Firestore כדי לאפשר לכם ליצור מנהלים שמקושרים לאירועי Cloud Firestore ספציפיים:

Node.js

סוג האירוע הפעלה
onDocumentCreated האירוע מופעל כשכותבים למסמך בפעם הראשונה.
onDocumentUpdated האירוע מופעל כשמסמך כבר קיים ויש בו שינוי כלשהו בערך.
onDocumentDeleted האירוע מופעל כשמסירים מסמך.
onDocumentWritten מופעל כשהאירוע onDocumentCreated, ‏ onDocumentUpdated או onDocumentDeleted מופעל.
onDocumentCreatedWithAuthContext onDocumentCreated עם פרטי אימות נוספים
onDocumentWrittenWithAuthContext onDocumentWritten עם פרטי אימות נוספים
onDocumentDeletedWithAuthContext onDocumentDeleted עם פרטי אימות נוספים
onDocumentUpdatedWithAuthContext onDocumentUpdated עם פרטי אימות נוספים

Python (תצוגה מקדימה)

סוג האירוע הפעלה
on_document_created האירוע מופעל כשכותבים למסמך בפעם הראשונה.
on_document_updated האירוע מופעל כשמסמך כבר קיים ויש בו שינוי כלשהו בערך.
on_document_deleted האירוע מופעל כשמסירים מסמך.
on_document_written מופעל כשהאירוע on_document_created, ‏ on_document_updated או on_document_deleted מופעל.
on_document_created_with_auth_context on_document_created עם פרטי אימות נוספים
on_document_updated_with_auth_context on_document_updated עם פרטי אימות נוספים
on_document_deleted_with_auth_context on_document_deleted עם פרטי אימות נוספים
on_document_written_with_auth_context on_document_written עם פרטי אימות נוספים

אירועי Cloud Firestore מופעלים רק במקרה של שינויים במסמך. עדכון של מסמך Cloud Firestore שבו הנתונים לא השתנו (כתיבה ללא פעולה) לא יוצר אירוע עדכון או כתיבה. אי אפשר להוסיף אירועים לשדות ספציפיים.

אם עדיין לא הפעלתם פרויקט ב-Cloud Functions for Firebase, תוכלו לקרוא את המאמר תחילת העבודה עם Cloud Functions for Firebase (דור שני) כדי להגדיר את הפרויקט ב-Cloud Functions for Firebase.

כתיבת פונקציות שמופעל בהן טריגר Cloud Firestore

הגדרת טריגר לפונקציה

כדי להגדיר טריגר Cloud Firestore, מציינים נתיב למסמך וסוג אירוע:

Node.js

import {
  onDocumentWritten,
  onDocumentCreated,
  onDocumentUpdated,
  onDocumentDeleted,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("my-collection/{docId}", (event) => {
   /* ... */ 
});

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_created,
  on_document_deleted,
  on_document_updated,
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_created(document="users/{userId}")
def myfunction(event: Event[DocumentSnapshot]) -> None:

נתיבי מסמכים יכולים להפנות למסמך ספציפי או לתבנית של תו כללי לחיפוש.

ציון מסמך יחיד

אם רוצים להפעיל אירוע לכל שינוי במסמך ספציפי, אפשר להשתמש בפונקציה הבאה.

Node.js

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("users/marie", (event) => {
  // Your code here
});

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/marie")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:

ציון קבוצה של מסמכים באמצעות תווים כלליים לחיפוש

אם רוצים לצרף טריגר לקבוצת מסמכים, למשל כל מסמך בקולקציה מסוימת, צריך להשתמש ב-{wildcard} במקום במזהה המסמך:

Node.js

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("users/{userId}", (event) => {
  // If we set `/users/marie` to {name: "Marie"} then
  // event.params.userId == "marie"
  // ... and ...
  // event.data.after.data() == {name: "Marie"}
});

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # If we set `/users/marie` to {name: "Marie"} then
  event.params["userId"] == "marie"  # True
  # ... and ...
  event.data.after.to_dict() == {"name": "Marie"}  # True

בדוגמה הזו, כששדה כלשהו במסמך כלשהו ב-users משתנה, הוא תואם למשתנה אסימון 'כל תו' שנקרא userId.

אם למסמך ב-users יש אוספי משנה ושדה באחד מהמסמכים של אוספי המשנה האלה משתנה, תו המשתנה הלא ידוע userId לא מופעל.

התאמות של תווים כלליים לחיפוש מופקות מנתיב המסמך ונשמרות ב-event.params. אפשר להגדיר כמה תווים כלליים לחיפוש שרוצים כדי להחליף מזהי אוספים או מסמכים מפורשים, לדוגמה:

Node.js

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.myfunction = onDocumentWritten("users/{userId}/{messageCollectionId}/{messageId}", (event) => {
    // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
    // event.params.userId == "marie";
    // event.params.messageCollectionId == "incoming_messages";
    // event.params.messageId == "134";
    // ... and ...
    // event.data.after.data() == {body: "Hello"}
});

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/{userId}/{messageCollectionId}/{messageId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
  event.params["userId"] == "marie"  # True
  event.params["messageCollectionId"] == "incoming_messages"  # True
  event.params["messageId"] == "134"  # True
  # ... and ...
  event.data.after.to_dict() == {"body": "Hello"}

הטריגר חייב תמיד להפנות למסמך, גם אם משתמשים בתו כללי לחיפוש. לדוגמה, הערך users/{userId}/{messageCollectionId} לא תקין כי {messageCollectionId} הוא אוסף. עם זאת, הערך users/{userId}/{messageCollectionId}/{messageId} תקף כי הערך {messageId} תמיד יצביע על מסמך.

טריגרים של אירועים

הפעלת פונקציה כשיוצרים מסמך חדש

אפשר להגדיר פונקציה שתופעל בכל פעם שייווצר מסמך חדש באוסף. הפונקציה לדוגמה הזו מופעלת בכל פעם שנוסף פרופיל משתמש חדש:

Node.js

import {
  onDocumentCreated,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.createuser = onDocumentCreated("users/{userId}", (event) => {
    // Get an object representing the document
    // e.g. {'name': 'Marie', 'age': 66}
    const snapshot = event.data;
    if (!snapshot) {
        console.log("No data associated with the event");
        return;
    }
    const data = snapshot.data();

    // access a particular field as you would any JS property
    const name = data.name;

    // perform more operations ...
});

אפשר להשתמש ב-onDocumentCreatedWithAuthContext כדי לקבל מידע נוסף על האימות.

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_created,
  Event,
  DocumentSnapshot,
)

@on_document_created(document="users/{userId}")
def myfunction(event: Event[DocumentSnapshot]) -> None:
  # Get a dictionary representing the document
  # e.g. {'name': 'Marie', 'age': 66}
  new_value = event.data.to_dict()

  # Access a particular field as you would any dictionary
  name = new_value["name"]

  # Perform more operations ...

הפעלת פונקציה כשמסמך מתעדכן

אפשר גם להפעיל פונקציה שתופעל כשמסמך יתעדכן. הפונקציה לדוגמה הזו מופעלת אם משתמש משנה את הפרופיל שלו:

Node.js

import {
  onDocumentUpdated,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.updateuser = onDocumentUpdated("users/{userId}", (event) => {
    // Get an object representing the document
    // e.g. {'name': 'Marie', 'age': 66}
    const newValue = event.data.after.data();

    // access a particular field as you would any JS property
    const name = newValue.name;

    // perform more operations ...
});

אפשר להשתמש ב-onDocumentUpdatedWithAuthContext כדי לקבל מידע נוסף על האימות.

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_updated,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_updated(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # Get a dictionary representing the document
  # e.g. {'name': 'Marie', 'age': 66}
  new_value = event.data.after.to_dict()

  # Access a particular field as you would any dictionary
  name = new_value["name"]

  # Perform more operations ...

הפעלת פונקציה כשמסמך נמחק

אפשר גם להפעיל פונקציה כשמסמך נמחק. הפונקציה לדוגמה הזו מופעלת כשמשתמש מוחק את פרופיל המשתמש שלו:

Node.js

import {
  onDocumentDeleted,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.deleteuser = onDocumentDeleted("users/{userId}", (event) => {
    // Get an object representing the document
    // e.g. {'name': 'Marie', 'age': 66}
    const snap =  event.data;
    const data =  snap.data();

    // perform more operations ...
});

אפשר להשתמש ב-onDocumentDeletedWithAuthContext כדי לקבל מידע נוסף על האימות.

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_deleted,
  Event,
  DocumentSnapshot,
)

@on_document_deleted(document="users/{userId}")
def myfunction(event: Event[DocumentSnapshot|None]) -> None:
  # Perform more operations ...

הפעלת פונקציה לכל השינויים במסמך

אם לא אכפת לכם מהסוג של האירוע שמופעל, תוכלו להאזין לכל השינויים במסמך Cloud Firestore באמצעות הטריגר 'document written'. הפונקציה לדוגמה הזו מופעלת אם משתמש נוצר, מתעדכן או נמחק:

Node.js

import {
  onDocumentWritten,
  Change,
  FirestoreEvent
} from "firebase-functions/v2/firestore";

exports.modifyuser = onDocumentWritten("users/{userId}", (event) => {
    // Get an object with the current document values.
    // If the document does not exist, it was deleted
    const document =  event.data.after.data();

    // Get an object with the previous document values
    const previousValues =  event.data.before.data();

    // perform more operations ...
});

אפשר להשתמש ב-onDocumentWrittenWithAuthContext כדי לקבל מידע נוסף על האימות.

Python (תצוגה מקדימה)

from firebase_functions.firestore_fn import (
  on_document_written,
  Event,
  Change,
  DocumentSnapshot,
)

@on_document_written(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot | None]]) -> None:
  # Get an object with the current document values.
  # If the document does not exist, it was deleted.
  document = (event.data.after.to_dict()
              if event.data.after is not None else None)

  # Get an object with the previous document values.
  # If the document does not exist, it was newly created.
  previous_values = (event.data.before.to_dict()
                     if event.data.before is not None else None)

  # Perform more operations ...

קריאה וכתיבה של נתונים

כשפונקציה מופעלת, היא מספקת תמונת מצב של הנתונים שקשורים לאירוע. אפשר להשתמש בתמונת המצב הזו כדי לקרוא מהמסמך שהפעיל את האירוע או לכתוב בו, או להשתמש ב-Firebase Admin SDK כדי לגשת לחלקים אחרים של מסד הנתונים.

נתוני אירוע

קריאת נתונים

כשפונקציה מופעלת, יכול להיות שתרצו לקבל נתונים ממסמך שעודכן, או לקבל את הנתונים לפני העדכון. אפשר לקבל את הנתונים הקודמים באמצעות event.data.before, שמכיל את קובץ snapshot של המסמך לפני העדכון. באופן דומה, המשתנה event.data.after מכיל את המצב של תמונת המצב של המסמך אחרי העדכון.

Node.js

exports.updateuser2 = onDocumentUpdated("users/{userId}", (event) => {
    // Get an object with the current document values.
    // If the document does not exist, it was deleted
    const newValues =  event.data.after.data();

    // Get an object with the previous document values
    const previousValues =  event.data.before.data();
});

Python (תצוגה מקדימה)

@on_document_updated(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # Get an object with the current document values.
  new_value = event.data.after.to_dict()

  # Get an object with the previous document values.
  prev_value = event.data.before.to_dict()

אפשר לגשת למאפיינים כמו בכל אובייקט אחר. לחלופין, אפשר להשתמש בפונקציה get כדי לגשת לשדות ספציפיים:

Node.js

// Fetch data using standard accessors
const age = event.data.after.data().age;
const name = event.data.after.data()['name'];

// Fetch data using built in accessor
const experience = event.data.after.data.get('experience');

Python (תצוגה מקדימה)

# Get the value of a single document field.
age = event.data.after.get("age")

# Convert the document to a dictionary.
age = event.data.after.to_dict()["age"]

כתיבת נתונים

כל קריאה לפונקציה משויכת למסמך ספציפי במסד הנתונים Cloud Firestore. תוכלו לגשת למסמך הזה בתמונת המצב (snapshot) שמוחזרת לפונקציה.

ההפניה למסמך כוללת שיטות כמו update(), ‏ set() ו-remove(), כדי שתוכלו לשנות את המסמך שהפעיל את הפונקציה.

Node.js

import { onDocumentUpdated } from "firebase-functions/v2/firestore";

exports.countnamechanges = onDocumentUpdated('users/{userId}', (event) => {
  // Retrieve the current and previous value
  const data = event.data.after.data();
  const previousData = event.data.before.data();

  // We'll only update if the name has changed.
  // This is crucial to prevent infinite loops.
  if (data.name == previousData.name) {
    return null;
  }

  // Retrieve the current count of name changes
  let count = data.name_change_count;
  if (!count) {
    count = 0;
  }

  // Then return a promise of a set operation to update the count
  return data.after.ref.set({
    name_change_count: count + 1
  }, {merge: true});

});

Python (תצוגה מקדימה)

@on_document_updated(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:
  # Get the current and previous document values.
  new_value = event.data.after
  prev_value = event.data.before

  # We'll only update if the name has changed.
  # This is crucial to prevent infinite loops.
  if new_value.get("name") == prev_value.get("name"):
      return

  # Retrieve the current count of name changes
  count = new_value.to_dict().get("name_change_count", 0)

  # Update the count
  new_value.reference.update({"name_change_count": count + 1})

גישה לפרטי אימות המשתמשים

אם משתמשים באחד מסוגי האירועים הבאים, אפשר לגשת למידע על אימות המשתמש של חשבון המשתמש שהפעיל את האירוע. המידע הזה מתווסף למידע שמוחזר באירוע הבסיסי.

Node.js

  • onDocumentCreatedWithAuthContext
  • onDocumentWrittenWithAuthContext
  • onDocumentDeletedWithAuthContext
  • onDocumentUpdatedWithAuthContext

Python (תצוגה מקדימה)

  • on_document_created_with_auth_context
  • on_document_updated_with_auth_context
  • on_document_deleted_with_auth_context
  • on_document_written_with_auth_context

מידע על הנתונים שזמינים בהקשר האימות זמין במאמר הקשר האימות. הדוגמה הבאה ממחישה איך לאחזר את פרטי האימות:

Node.js

import { onDocumentWrittenWithAuthContext } from "firebase-functions/v2/firestore"

exports.syncUser = onDocumentWrittenWithAuthContext("users/{userId}", (event) => {
    const snapshot = event.data.after;
    if (!snapshot) {
        console.log("No data associated with the event");
        return;
    }
    const data = snapshot.data();

    // retrieve auth context from event
    const { authType, authId } = event;

    let verified = false;
    if (authType === "system") {
      // system-generated users are automatically verified
      verified = true;
    } else if (authType === "unknown" || authType === "unauthenticated") {
      // admin users from a specific domain are verified
      if (authId.endsWith("@example.com")) {
        verified = true;
      }
    }

    return data.after.ref.set({
        created_by: authId,
        verified,
    }, {merge: true}); 
}); 

Python (תצוגה מקדימה)

@on_document_updated_with_auth_context(document="users/{userId}")
def myfunction(event: Event[Change[DocumentSnapshot]]) -> None:

  # Get the current and previous document values.
  new_value = event.data.after
  prev_value = event.data.before

  # Get the auth context from the event
  user_auth_type = event.auth_type
  user_auth_id = event.auth_id

נתונים מחוץ לאירוע ההפעלה

Cloud Functions להריץ בסביבה מהימנה. הוא מורשה כחשבון שירות בפרויקט, ותוכלו לבצע פעולות קריאה וכתיבה באמצעות Firebase Admin SDK:

Node.js

const { initializeApp } = require('firebase-admin/app');
const { getFirestore, Timestamp, FieldValue } = require('firebase-admin/firestore');

initializeApp();
const db = getFirestore();

exports.writetofirestore = onDocumentWritten("some/doc", (event) => {
    db.doc('some/otherdoc').set({ ... });
  });

  exports.writetofirestore = onDocumentWritten('users/{userId}', (event) => {
    db.doc('some/otherdoc').set({
      // Update otherdoc
    });
  });

Python (תצוגה מקדימה)

from firebase_admin import firestore, initialize_app
import google.cloud.firestore

initialize_app()

@on_document_written(document="some/doc")
def myfunction(event: Event[Change[DocumentSnapshot | None]]) -> None:
  firestore_client: google.cloud.firestore.Client = firestore.client()
  firestore_client.document("another/doc").set({
      # ...
  })

מגבלות

חשוב לשים לב למגבלות הבאות לגבי טריגרים של Cloud Firestore ל-Cloud Functions:

  • Cloud Functions (דור ראשון) מחייב מסד נתונים קיים מסוג '(default)' במצב מקורי של Firestore. הוא לא תומך במסדי נתונים Cloud Firestore עם שם או במצב Datastore. במקרים כאלה, צריך להשתמש ב-Cloud Functions (דור שני) כדי להגדיר אירועים.
  • לא מובטח שההזמנה תבוצע. שינויים מהירים יכולים להפעיל קריאות לפונקציות בסדר לא צפוי.
  • האירועים מועברים לפחות פעם אחת, אבל אירוע אחד יכול לגרום להפעלה של כמה פונקציות. הימנעו מהסתמכות על מנגנונים של 'פעם אחת בלבד', וכתבו פונקציות חד-ערכיות (idempotent).
  • כדי להשתמש ב-Cloud Firestore במצב Datastore נדרש Cloud Functions (דור שני). ב-Cloud Functions (דור ראשון) אין תמיכה במצב Datastore.
  • כל טריגר משויך למסד נתונים אחד. לא ניתן ליצור טריגר שתואמת למספר מסדי נתונים.
  • מחיקת מסד נתונים לא מוחקת באופן אוטומטי את הטריגרים של אותו מסד נתונים. הטריגר יפסיק לשלוח אירועים, אבל הוא ימשיך להתקיים עד שתמחקו אותו.
  • אם אירוע תואם חורג מגודל הבקשה המקסימלי, יכול להיות שהאירוע לא יישלח אל Cloud Functions (דור ראשון).
    • אירועים שלא נשלחו בגלל גודל הבקשה מתועדים ביומני הפלטפורמה ומספרם נספר בשימוש היומן של הפרויקט.
    • היומנים האלה מופיעים ב-Logs Explorer עם ההודעה "Event cannot deliver to Cloud function due to size exceeding the limit for 1st gen..." ברמת חומרה error. שם הפונקציה מופיע בשדה functionName. אם השדה receiveTimestamp עדיין מתייחס לשעה הקרובה, תוכלו להסיק את תוכן האירוע בפועל על ידי קריאת המסמך הרלוונטי באמצעות קובץ snapshot לפני ואחרי חותמת הזמן.
    • כדי להימנע מקצב כזה, אתם יכולים:
      • העברה ושדרוג ל-Cloud Functions (דור שני)
      • צמצום המסמך
      • מוחקים את ה-Cloud Functions הרלוונטי
    • אפשר להשבית את הרישום ביומן באמצעות החרגות, אבל חשוב לזכור שהאירועים הבעייתיים עדיין לא יישלחו.