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

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

Cloud Functions (דור שני)

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

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

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

Node.js

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

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

סוג אירוע Trigger
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 באמצעות טריגר האירוע 'כתוב מסמך'. הפונקציה לדוגמה הזו מופעלת אם המשתמש נוצר, מעודכן או נמחק:

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 ...

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

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

נתוני אירוע

קריאת נתונים

כשפונקציה מופעלת, יכול להיות שתרצו לקבל נתונים ממסמך שעודכן, או לקבל את הנתונים לפני העדכון. אפשר לקבל את הנתונים הקודמים באמצעות event.data.before, שמכיל את תמונת המצב של המסמך לפני העדכון. באופן דומה, 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 Firestore במצב Datastore יש צורך ב-Cloud Functions (דור שני). ב-Cloud Functions (דור ראשון) אין תמיכה במצב Datastore.
  • Cloud Functions (דור ראשון) פועל רק עם מסד הנתונים "(ברירת מחדל)" ולא תומך במסדי נתונים בעלי שם של Cloud Firestore. כדי להגדיר אירועים למסדי נתונים בעלי שם, צריך להשתמש ב-Cloud Functions (דור שני).
  • טריגר משויך למסד נתונים יחיד. לא ניתן ליצור גורם מפעיל שתואם למספר מסדי נתונים.
  • מחיקה של מסד נתונים לא מוחקת באופן אוטומטי את הטריגרים של אותו מסד נתונים. הטריגר מפסיק להעביר אירועים אבל ממשיך להתקיים עד שמוחקים את הטריגר.
  • אם אירוע תואם חורג מגודל הבקשה המקסימלי, ייתכן שהאירוע לא יועבר ל-Cloud Functions (דור ראשון).
    • אירועים שלא נמסרו בגלל גודל הבקשה נרשמים ביומני הפלטפורמה ונכללים בחישוב השימוש ביומן בפרויקט.
    • היומנים האלה מופיעים ב-Logs Explorer עם ההודעה "Event לא יכול לספק לפונקציה Cloud Functions בגלל שהגודל חורג מהמגבלה של דור ראשון..." בחומרה של error. השם של הפונקציה מופיע מתחת לשדה functionName. אם השדה receiveTimestamp עדיין נמצא בטווח של שעה מעכשיו, אפשר להסיק את תוכן האירוע בפועל על ידי קריאת המסמך הרלוונטי עם תמונת מצב לפני ואחרי חותמת הזמן.
    • כדי להימנע מקצב כזה, אפשר:
      • העברה ושדרוג ל-Cloud Functions (דור שני)
      • הקטנת המסמך
      • מחיקת הפונקציות הנדונות של Cloud Functions
    • תוכלו להשבית את הרישום ביומן עצמו באמצעות החרגות, אבל שימו לב שהאירועים הבעייתיים עדיין לא יועברו.