ทริกเกอร์ Realtime Database

ด้วย Cloud Functions คุณสามารถจัดการเหตุการณ์ใน Firebase Realtime Database ได้โดยไม่ต้องอัปเดตโค้ดฝั่งไคลเอ็นต์ Cloud Functions ช่วยให้คุณเรียกใช้การดำเนินการ Realtime Database ด้วยสิทธิ์ระดับผู้ดูแลระบบเต็มรูปแบบ และช่วยให้มั่นใจได้ว่าการเปลี่ยนแปลงแต่ละรายการใน Realtime Database จะได้รับการประมวลผลแยกกัน คุณสามารถทำการเปลี่ยนแปลง Firebase Realtime Database ผ่านข้อมูลสแนปช็อต หรือผ่าน Admin SDK

ในวงจรชีวิตโดยทั่วไป ฟังก์ชัน Firebase Realtime Database จะดำเนินการดังนี้

  1. รอการเปลี่ยนแปลงในเส้นทาง Realtime Database ที่เฉพาะเจาะจง
  2. ทริกเกอร์เมื่อเกิดเหตุการณ์และทำงานที่กำหนด
  3. รับออบเจ็กต์ข้อมูลที่มีสแนปช็อตของข้อมูลที่จัดเก็บไว้ในเส้นทางนั้น

คุณสามารถทริกเกอร์ฟังก์ชันเพื่อตอบสนองต่อการเขียน สร้าง อัปเดต หรือ ลบโหนดฐานข้อมูลใน Firebase Realtime Database หากต้องการ ควบคุมเวลาที่ฟังก์ชันทริกเกอร์ ให้ระบุตัวแฮนเดิลเหตุการณ์รายการใดรายการหนึ่ง และ ระบุเส้นทาง Realtime Database ที่จะใช้ฟังเหตุการณ์

การตั้งค่าตำแหน่งฟังก์ชัน

ระยะห่างระหว่างตำแหน่งของอินสแตนซ์ Realtime Database กับตำแหน่งของฟังก์ชัน อาจทำให้เกิดเวลาในการตอบสนองของเครือข่ายที่สำคัญ นอกจากนี้ การที่ภูมิภาคไม่ตรงกันอาจทำให้การติดตั้งใช้งานล้มเหลว หากต้องการหลีกเลี่ยงสถานการณ์เหล่านี้ ให้ระบุ ตำแหน่งฟังก์ชันเพื่อให้ ตรงกับ ตำแหน่งอินสแตนซ์ฐานข้อมูล

การจัดการเหตุการณ์ Realtime Database

ฟังก์ชันช่วยให้คุณจัดการเหตุการณ์ Realtime Database ได้ 2 ระดับความเฉพาะเจาะจง โดยคุณสามารถฟังเหตุการณ์การเขียน การสร้าง การอัปเดต หรือการลบโดยเฉพาะ หรือจะฟังการเปลี่ยนแปลงทุกประเภทในการอ้างอิงก็ได้

ตัวแฮนเดิลต่อไปนี้พร้อมให้คุณใช้เพื่อตอบสนองต่อเหตุการณ์ Realtime Database

Node.js

  • onValueWritten() ทริกเกอร์เมื่อมีการสร้าง อัปเดต หรือลบข้อมูลใน Realtime Database
  • onValueCreated() ทริกเกอร์เมื่อมีการสร้างข้อมูลใน Realtime Database เท่านั้น
  • onValueUpdated() ทริกเกอร์เมื่อมีการอัปเดตข้อมูลใน Realtime Database เท่านั้น
  • onValueDeleted() ทริกเกอร์เมื่อมีการลบข้อมูลใน Realtime Database เท่านั้น

Python

  • on_value_written() ทริกเกอร์เมื่อมีการสร้าง อัปเดต หรือลบข้อมูลใน Realtime Database
  • on_value_created() ทริกเกอร์เมื่อมีการสร้างข้อมูลใน Realtime Database เท่านั้น
  • on_value_updated() ทริกเกอร์เมื่อมีการอัปเดตข้อมูลใน Realtime Database เท่านั้น
  • on_value_deleted() ทริกเกอร์เมื่อมีการลบข้อมูลใน Realtime Database เท่านั้น

นำเข้าโมดูลที่จำเป็น

ในแหล่งที่มาของฟังก์ชัน คุณต้องนำเข้าโมดูล SDK ที่ต้องการใช้ สำหรับ ตัวอย่างนี้ คุณต้องนำเข้าโมดูล HTTP และ Realtime Database พร้อมกับ โมดูล Firebase Admin SDK สำหรับการเขียนไปยัง Realtime Database

Node.js

// The Cloud Functions for Firebase SDK to setup triggers and logging.
const {onRequest} = require("firebase-functions/https");
const {onValueCreated} = require("firebase-functions/database");
const {logger} = require("firebase-functions");

// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require("firebase-admin");
admin.initializeApp();

Python

# The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers.
from firebase_functions import db_fn, https_fn

# The Firebase Admin SDK to access the Firebase Realtime Database.
from firebase_admin import initialize_app, db

app = initialize_app()

ระบุอินสแตนซ์และเส้นทาง

หากต้องการควบคุมเวลาและตำแหน่งที่ฟังก์ชันควรทริกเกอร์ ให้กำหนดค่าฟังก์ชัน ด้วยเส้นทางและอินสแตนซ์ Realtime Database (ไม่บังคับ) หากคุณไม่ได้ ระบุอินสแตนซ์ ฟังก์ชันจะฟังอินสแตนซ์ Realtime Database ทั้งหมด ในภูมิภาคของฟังก์ชัน นอกจากนี้ คุณยังระบุรูปแบบอินสแตนซ์ Realtime Database เพื่อทำให้ใช้งานได้กับอินสแตนซ์ย่อยที่เลือกในภูมิภาคเดียวกันได้ด้วย

ตัวอย่างเช่น

Node.js

// All Realtime Database instances in default function region us-central1 at path "/user/{uid}"
// There must be at least one Realtime Database present in us-central1.
const onWrittenFunctionDefault = onValueWritten("/user/{uid}", (event) => {
  // …
});

// Instance named "my-app-db-2", at path "/user/{uid}".
// The "my-app-db-2" instance must exist in this region.
const OnWrittenFunctionInstance = onValueWritten(
  {
    ref: "/user/{uid}",
    instance: "my-app-db-2"
    // This example assumes us-central1, but to set location:
    // region: "europe-west1"
  },
  (event) => {
    // …
  }
);

// Instance with "my-app-db-" prefix, at path "/user/{uid}", where uid ends with @gmail.com.
// There must be at least one Realtime Database with "my-app-db-*" prefix in this region.
const onWrittenFunctionInstance = onValueWritten(
  {
    ref: "/user/{uid=*@gmail.com}",
    instance: "my-app-db-*"
    // This example assumes us-central1, but to set location:
    // region: "europe-west1"
  },
  (event) => {
    // …
  }
);

Python

# All Realtime Database instances in default function region us-central1 at path "/user/{uid}"
# There must be at least one Realtime Database present in us-central1.
@db_fn.on_value_written(r"/user/{uid}")
def onwrittenfunctiondefault(event: db_fn.Event[db_fn.Change]):
    # ...
    pass

# Instance named "my-app-db-2", at path "/user/{uid}".
# The "my-app-db-2" instance must exist in this region.
@db_fn.on_value_written(
    reference=r"/user/{uid}",
    instance="my-app-db-2",
    # This example assumes us-central1, but to set location:
    # region="europe-west1",
)
def on_written_function_instance(event: db_fn.Event[db_fn.Change]):
    # ...
    pass

# Instance with "my-app-db-" prefix, at path "/user/{uid}", where uid ends with @gmail.com.
# There must be at least one Realtime Database with "my-app-db-*" prefix in this region.
@db_fn.on_value_written(
    reference=r"/user/{uid=*@gmail.com}",
    instance="my-app-db-*",
    # This example assumes us-central1, but to set location:
    # region="europe-west1",
)
def on_written_function_instance(event: db_fn.Event[db_fn.Change]):
    # ...
    pass

พารามิเตอร์เหล่านี้จะสั่งให้ฟังก์ชันจัดการการเขียนในเส้นทางที่แน่นอนภายใน อินสแตนซ์ Realtime Database

การระบุเส้นทางจะตรงกับการเขียน ทั้งหมด ที่แตะเส้นทาง รวมถึงการเขียนที่เกิดขึ้นในตำแหน่งใดก็ตามที่อยู่ใต้เส้นทางนั้น หากคุณตั้งค่าเส้นทางสำหรับฟังก์ชันเป็น /foo/bar ฟังก์ชันจะตรงกับเหตุการณ์ในตำแหน่งต่อไปนี้

 /foo/bar
 /foo/bar/baz/really/deep/path

ไม่ว่าในกรณีใดก็ตาม Firebase จะตีความว่าเหตุการณ์เกิดขึ้นที่ /foo/bar และข้อมูลเหตุการณ์จะมีข้อมูลเก่าและข้อมูลใหม่ที่ /foo/bar หากข้อมูลเหตุการณ์มีขนาดใหญ่ ให้พิจารณาใช้ฟังก์ชันหลายรายการในเส้นทางที่ลึกลงไปแทนที่จะใช้ฟังก์ชันเดียวที่อยู่ใกล้กับรูทของฐานข้อมูล ขอข้อมูลเฉพาะในระดับที่ลึกที่สุดเท่าที่จะเป็นไปได้เพื่อให้ได้ประสิทธิภาพสูงสุด

การใช้ไวลด์การ์ดและการจับ

คุณสามารถใช้ {key}, {key=*}, {key=prefix*}, {key=*suffix} เพื่อจับ *, prefix*, *suffix สำหรับการใช้ไวลด์การ์ดแบบส่วนเดียว หมายเหตุ: ** แสดงถึงการใช้ไวลด์การ์ดแบบหลายส่วน ซึ่ง Realtime Database ไม่รองรับ ดูทำความเข้าใจรูปแบบเส้นทาง

การใช้ไวลด์การ์ดเส้นทาง คุณสามารถระบุคอมโพเนนต์เส้นทางเป็นไวลด์การ์ดได้ดังนี้

  • ใช้เครื่องหมายดอกจัน * เช่น foo/* จะตรงกับโหนดย่อยระดับเดียวใน ลำดับชั้นโหนดที่อยู่ใต้ foo/
  • ใช้ส่วนที่มีเครื่องหมายดอกจัน * เท่านั้น เช่น foo/app*-us จะตรงกับส่วนย่อยที่อยู่ใต้ foo/ ที่มีคำนำหน้า app และคำต่อท้าย -us

เส้นทางที่มีไวลด์การ์ดสามารถตรงกับเหตุการณ์หลายรายการได้ เช่น จากการเขียนครั้งเดียว การแทรก

{
  "foo": {
    "hello": "world",
    "firebase": "functions"
  }
}

จะตรงกับเส้นทาง "/foo/*" 2 ครั้ง ได้แก่ ครั้งหนึ่งกับ "hello": "world" และอีกครั้งกับ "firebase": "functions"

การจับเส้นทาง คุณสามารถจับการจับคู่เส้นทางลงในตัวแปรที่มีชื่อเพื่อใช้ในโค้ดฟังก์ชันได้ (เช่น /user/{uid}, /user/{uid=*-us})

ค่าของตัวแปรการจับจะอยู่ใน ออบเจ็กต์ database.DatabaseEvent.params ของฟังก์ชัน

การใช้ไวลด์การ์ดอินสแตนซ์ นอกจากนี้ คุณยังระบุคอมโพเนนต์อินสแตนซ์โดยใช้ไวลด์การ์ดได้ด้วย ไวลด์การ์ดอินสแตนซ์อาจมีคำนำหน้า คำต่อท้าย หรือทั้ง 2 อย่าง (เช่น my-app-*-prod)

ข้อมูลอ้างอิงไวลด์การ์ดและการจับ

Cloud Functions (รุ่นที่ 2) และ Realtime Database อนุญาตให้ใช้รูปแบบเมื่อ ระบุ ref และ instance อินเทอร์เฟซทริกเกอร์แต่ละรายการจะมีตัวเลือกต่อไปนี้สำหรับการกำหนดขอบเขตฟังก์ชัน

การระบุ ref การระบุ instance พฤติกรรม
รายการเดียว (/foo/bar) ไม่ได้ระบุ กำหนดขอบเขตตัวแฮนเดิลเป็นอินสแตนซ์ทั้งหมดในภูมิภาคของฟังก์ชัน
รายการเดียว (/foo/bar) รายการเดียว (‘my-new-db') กำหนดขอบเขตตัวแฮนเดิลเป็นอินสแตนซ์ที่เฉพาะเจาะจงในภูมิภาคของฟังก์ชัน
รายการเดียว (/foo/bar) รูปแบบ (‘inst-prefix*') กำหนดขอบเขตตัวแฮนเดิลเป็นอินสแตนซ์ทั้งหมดที่ตรงกับรูปแบบในภูมิภาคของฟังก์ชัน
รูปแบบ (/foo/{bar}) ไม่ได้ระบุ กำหนดขอบเขตตัวแฮนเดิลเป็นอินสแตนซ์ทั้งหมดในภูมิภาคของฟังก์ชัน
รูปแบบ (/foo/{bar}) รายการเดียว (‘my-new-db') กำหนดขอบเขตตัวแฮนเดิลเป็นอินสแตนซ์ที่เฉพาะเจาะจงในภูมิภาคของฟังก์ชัน
รูปแบบ (/foo/{bar}) รูปแบบ (‘inst-prefix*') กำหนดขอบเขตตัวแฮนเดิลเป็นอินสแตนซ์ทั้งหมดที่ตรงกับรูปแบบในภูมิภาคของฟังก์ชัน

จัดการข้อมูลเหตุการณ์

เมื่อเหตุการณ์ Realtime Database ทริกเกอร์ ระบบจะส่งออบเจ็กต์ Event ไปยังฟังก์ชันตัวแฮนเดิล ออบเจ็กต์นี้มีพร็อพเพอร์ตี้ data ซึ่งสำหรับเหตุการณ์การสร้างและการลบ จะมีสแนปช็อตของข้อมูลที่สร้างหรือลบ

ในตัวอย่างนี้ ฟังก์ชันจะดึงข้อมูลสำหรับเส้นทางที่อ้างอิง แปลงสตริงในตำแหน่งนั้นเป็นตัวพิมพ์ใหญ่ และเขียนสตริงที่แก้ไขแล้วลงในฐานข้อมูล

Node.js

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
// for all databases in 'us-central1'
exports.makeuppercase = onValueCreated(
    "/messages/{pushId}/original",
    (event) => {
    // Grab the current value of what was written to the Realtime Database.
      const original = event.data.val();
      logger.log("Uppercasing", event.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing
      // asynchronous tasks inside a function, such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the
      // Realtime Database returns a Promise.
      return event.data.ref.parent.child("uppercase").set(uppercase);
    },
);

Python

@db_fn.on_value_created(reference="/messages/{pushId}/original")
def makeuppercase(event: db_fn.Event[Any]) -> None:
    """Listens for new messages added to /messages/{pushId}/original and
    creates an uppercase version of the message to /messages/{pushId}/uppercase
    """

    # Grab the value that was written to the Realtime Database.
    original = event.data
    if not isinstance(original, str):
        print(f"Not a string: {event.reference}")
        return

    # Use the Admin SDK to set an "uppercase" sibling.
    print(f"Uppercasing {event.params['pushId']}: {original}")
    upper = original.upper()
    parent = db.reference(event.reference).parent
    if parent is None:
        print("Message can't be root node.")
        return
    parent.child("uppercase").set(upper)

การอ่านค่าก่อนหน้า

สำหรับเหตุการณ์ write หรือ update พร็อพเพอร์ตี้ data จะเป็นออบเจ็กต์ Change ที่มีสแนปช็อต 2 รายการซึ่งแสดงสถานะข้อมูลก่อนและหลังเหตุการณ์ทริกเกอร์ ออบเจ็กต์ Change มีพร็อพเพอร์ตี้ before ที่ช่วยให้คุณตรวจสอบสิ่งที่บันทึกไว้ใน Realtime Database ก่อน เหตุการณ์ และพร็อพเพอร์ตี้ after ที่แสดงสถานะของข้อมูล หลังจาก เกิดเหตุการณ์

ตัวอย่างเช่น คุณสามารถใช้พร็อพเพอร์ตี้ before เพื่อให้แน่ใจว่าฟังก์ชันจะแปลงข้อความเป็นตัวพิมพ์ใหญ่เมื่อมีการสร้างข้อความเป็นครั้งแรกเท่านั้น

Node.js

  exports makeUppercase = onValueWritten("/messages/{pushId}/original", (event) => {
        // Only edit data when it is first created.
        if (event.data.before.exists()) {
          return null;
        }
        // Exit when the data is deleted.
        if (!event.data.after.exists()) {
          return null;
        }
        // Grab the current value of what was written to the Realtime Database.
        const original = event.data.after.val();
        console.log('Uppercasing', event.params.pushId, original);
        const uppercase = original.toUpperCase();
        // You must return a Promise when performing asynchronous tasks inside a Functions such as
        // writing to the Firebase Realtime Database.
        // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
        return event.data.after.ref.parent.child('uppercase').set(uppercase);
      });

Python

@db_fn.on_value_written(reference="/messages/{pushId}/original")
def makeuppercase2(event: db_fn.Event[db_fn.Change]) -> None:
    """Listens for new messages added to /messages/{pushId}/original and
    creates an uppercase version of the message to /messages/{pushId}/uppercase
    """

    # Only edit data when it is first created.
    if event.data.before is not None:
        return

    # Exit when the data is deleted.
    if event.data.after is None:
        return

    # Grab the value that was written to the Realtime Database.
    original = event.data.after
    if not hasattr(original, "upper"):
        print(f"Not a string: {event.reference}")
        return

    # Use the Admin SDK to set an "uppercase" sibling.
    print(f"Uppercasing {event.params['pushId']}: {original}")
    upper = original.upper()
    parent = db.reference(event.reference).parent
    if parent is None:
        print("Message can't be root node.")
        return
    parent.child("uppercase").set(upper)

การเข้าถึงบริบทการตรวจสอบสิทธิ์

สำหรับฟังก์ชันที่ทริกเกอร์โดยเหตุการณ์ RTDB Eventarc บริบทการตรวจสอบสิทธิ์จะรวมอยู่ในเพย์โหลดเหตุการณ์

  • authtype: ประเภทของพรินซิเพิลที่ทริกเกอร์เหตุการณ์ ค่าที่เป็นไปได้มีดังนี้
    • app_user: ผู้ใช้ปลายทางของแอปพลิเคชันของนักพัฒนาแอป
    • admin: บัญชีบริการ
    • unauthenticated: ผู้ใช้ที่ไม่ได้รับการตรวจสอบสิทธิ์
    • unknown: ค่าเริ่มต้นเมื่อไม่มีข้อมูลการตรวจสอบสิทธิ์
  • authid: ตัวระบุที่ไม่ซ้ำกันของพรินซิเพิล
    • หาก authtype คือ app_user ค่านี้จะเป็น UID ของผู้ใช้
    • หาก authtype คือ admin ค่านี้จะเป็นอีเมลของบัญชีบริการหรือผู้ใช้ IAM

โค้ดนี้จะแปลงข้อความของข้อความเป็นตัวพิมพ์ใหญ่ก็ต่อเมื่อผู้ใช้ที่ทริกเกอร์ฟังก์ชันไม่ใช่ผู้ดูแลระบบ นอกจากนี้ โค้ดยังตรวจสอบด้วยว่าผู้ใช้ที่ทริกเกอร์ข้อความเป็นผู้ส่งข้อความจริงหรือไม่

Node.js

// The Cloud Functions for Firebase SDK to setup triggers and logging.
const {onValueWritten} = require("firebase-functions/v2/database");
const {logger} = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp();

exports.dbtrigger = onValueWritten("/messages/{pushId}/original", async (event) => {
  // 1. Check whether authtype is admin. If it is, skip this operation.
  if (event.authType === "admin") {
    logger.log("Modification by admin detected. Skipping uppercase conversion.");
    return null;
  }

  // 2. Retrieve the userID of the sender (assumed sibling node 'senderId')
  const snapshot = await event.data.after.ref.parent.child("senderId").get();
  const senderId = snapshot.val();

  // 3. Check if userID of sender of message = event.authid
  if (senderId !== event.authId) {
    logger.error(`Unauthorized write: senderId (${senderId}) does not match authId (${event.authId})`);
    return null;
  }

  // Grab the value that was written to the Realtime Database.
  const original = event.data.after.val();
  logger.log("Uppercasing", event.params.pushId, original);
  const uppercase = original.toUpperCase();

  // Return the promise to set the "uppercase" sibling node.
  return event.data.after.ref.parent.child("uppercase").set(uppercase);
});

Python

from firebase_functions import db_fn
from firebase_admin import initialize_app, db

initialize_app()

@db_fn.on_value_written(reference="/messages/{pushId}/original")
def makeuppercase(event: db_fn.Event[db_fn.Change]) -> None:
    # 1. Check whether authtype is admin. If it is, skip this operation.
    if event.auth_type == "admin":
        print("Admin user detected. Skipping.")
        return

    # 2. Retrieve the userID of the sender (assumed sibling node: 'senderId')
    parent_ref = db.reference(event.reference).parent
    sender_id = parent_ref.child("senderId").get()

    # 3. Check if userID of sender = event.auth_id
    if sender_id != event.auth_id:
        print(f"Unauthorized: sender_id {sender_id} != auth_id {event.auth_id}")
        return

    # Exit when the data is deleted.
    if event.data.after is None:
        return

    # Grab the value and uppercase it
    original = event.data.after
    if not isinstance(original, str):
        return

    print(f"Uppercasing {event.params['pushId']}: {original}")
    upper = original.upper()
    parent_ref.child("uppercase").set(upper)