הרחבת Cloud Firestore באמצעות Cloud Functions

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

לדוגמאות של תרחישים לדוגמה, ראו What Can I Do with Cloud Functions? או במאגר GitHub של Functions Samples.

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

ה-SDK של Cloud Functions for Firebase מייצא אובייקט functions.firestore שמאפשר ליצור מנהלים שמקושרים לאירועי Cloud Firestore ספציפיים.

סוג האירוע הפעלה
onCreate האירוע מופעל כשכותבים למסמך בפעם הראשונה.
onUpdate האירוע מופעל כשמסמך כבר קיים ויש בו שינוי כלשהו בערך.
onDelete מופעל כשמסמך עם נתונים נמחק.
onWrite מופעל כשהתכונות onCreate, onUpdate או onDelete מופעלות.

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

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

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

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

Node.js

const functions = require('firebase-functions');

exports.myFunction = functions.firestore
  .document('my-collection/{docId}')
  .onWrite((change, context) => { /* ... */ });

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

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

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

Node.js

// Listen for any change on document `marie` in collection `users`
exports.myFunctionName = functions.firestore
    .document('users/marie').onWrite((change, context) => {
      // ... Your code here
    });

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

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

Node.js

// Listen for changes in all documents in the 'users' collection
exports.useWildcard = functions.firestore
    .document('users/{userId}')
    .onWrite((change, context) => {
      // If we set `/users/marie` to {name: "Marie"} then
      // context.params.userId == "marie"
      // ... and ...
      // change.after.data() == {name: "Marie"}
    });

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

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

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

Node.js

// Listen for changes in all documents in the 'users' collection and all subcollections
exports.useMultipleWildcards = functions.firestore
    .document('users/{userId}/{messageCollectionId}/{messageId}')
    .onWrite((change, context) => {
      // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
      // context.params.userId == "marie";
      // context.params.messageCollectionId == "incoming_messages";
      // context.params.messageId == "134";
      // ... and ...
      // change.after.data() == {body: "Hello"}
    });

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

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

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

Node.js

exports.createUser = functions.firestore
    .document('users/{userId}')
    .onCreate((snap, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = snap.data();

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

      // perform desired operations ...
    });

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

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

Node.js

exports.updateUser = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();

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

      // perform desired operations ...
    });

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

אפשר גם להפעיל פונקציה כשמסמך נמחק באמצעות הפונקציה onDelete() עם תו כללי לחיפוש. הפונקציה לדוגמה קורא ל-deleteUser כשמשתמש מחק את פרופיל המשתמש שלו:

Node.js

exports.deleteUser = functions.firestore
    .document('users/{userID}')
    .onDelete((snap, context) => {
      // Get an object representing the document prior to deletion
      // e.g. {'name': 'Marie', 'age': 66}
      const deletedValue = snap.data();

      // perform desired operations ...
    });

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

אם לא אכפת לכם מהסוג של האירוע שמופעל, אתם יכולים להאזין לכל השינויים במסמך Cloud Firestore באמצעות הפונקציה onWrite() עם תו כללי. הפונקציה לדוגמה הזו מפעילה את הפונקציה modifyUser אם משתמש נוצר, מעודכן או נמחק:

Node.js

exports.modifyUser = functions.firestore
    .document('users/{userID}')
    .onWrite((change, context) => {
      // Get an object with the current document value.
      // If the document does not exist, it has been deleted.
      const document = change.after.exists ? change.after.data() : null;

      // Get an object with the previous document value (for update or delete)
      const oldDocument = change.before.data();

      // perform desired operations ...
    });

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

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

נתוני אירוע

נתוני קריאה

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

Node.js

exports.updateUser2 = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the current document
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();
    });

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

Node.js

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

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

כתיבת נתונים

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

הערך של DocumentReference מגיע מ-Cloud Firestore Node.js SDK, וכולל שיטות כמו update(),‏ set() ו-remove(), שמאפשרות לשנות בקלות את המסמך שהפעיל את הפונקציה.

Node.js

// Listen for updates to any `user` document.
exports.countNameChanges = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Retrieve the current and previous value
      const data = change.after.data();
      const previousData = change.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 change.after.ref.set({
        name_change_count: count + 1
      }, {merge: true});
    });

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

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

Node.js

const admin = require('firebase-admin');
admin.initializeApp();

const db = admin.firestore();

exports.writeToFirestore = functions.firestore
  .document('some/doc')
  .onWrite((change, context) => {
    db.doc('some/otherdoc').set({ ... });
  });

מגבלות

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

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