Cloud Functions(第 2 世代)による Cloud Firestore の拡張

Cloud Functions で、コードをデプロイして、Cloud Firestore データベースの変更によってトリガーされるイベントを処理できます。これにより、独自のサーバーを運用することなく、サーバーサイドの機能を簡単にアプリに追加できます。

Cloud Functions(第 2 世代)

Cloud RunEventarc を活用した Cloud Functions for Firebase(第 2 世代)を利用すれば、インフラストラクチャのさらなる強化、パフォーマンスとスケーラビリティの高度な管理、関数のランタイムのより詳細な管理を実現できます。第 2 世代の詳細については、Cloud Functions for Firebase(第 2 世代)をご覧ください。そうではなく、第 1 世代の詳細を確認するには、Cloud Functions による Cloud Firestore の拡張をご覧ください。

Cloud Firestore 関数トリガー

Cloud Functions for Firebase SDK で次の Cloud Firestore イベント トリガーをエクスポートすると、特定の Cloud Firestore イベントに結び付けたハンドラを作成できます。

Node.js

イベントタイプ トリガー
onDocumentCreated ドキュメントが最初に書き込まれたときにトリガーされます。
onDocumentUpdated すでに存在するドキュメントの値が変更されたときにトリガーされます。
onDocumentDeleted ドキュメントが削除されたときにトリガーされます。
onDocumentWritten onDocumentCreatedonDocumentUpdated または onDocumentDeleted がトリガーされたときにトリガーされます。
onDocumentCreatedWithAuthContext onDocumentCreated(追加の認証情報を含む)
onDocumentWrittenWithAuthContext onDocumentWritten(追加の認証情報を含む)
onDocumentDeletedWithAuthContext onDocumentDeleted(追加の認証情報を含む)
onDocumentUpdatedWithAuthContext onDocumentUpdated(追加の認証情報を含む)

Python(プレビュー)

イベントタイプ トリガー
on_document_created ドキュメントが最初に書き込まれたときにトリガーされます。
on_document_updated すでに存在するドキュメントの値が変更されたときにトリガーされます。
on_document_deleted ドキュメントが削除されたときにトリガーされます。
on_document_written on_document_createdon_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(第 2 世代)を使ってみるを参照して、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:

ワイルドカードを使用してドキュメントのグループを指定する

特定のコレクション内の任意のドキュメントなど、ドキュメントのグループにトリガーを関連付ける場合は、ドキュメント ID の代わりに {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 に保存されます。明示的なコレクションまたはドキュメント ID に置き換えるワイルドカードは、必要な数だけ定義できます。次に例を示します。

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"}

ワイルドカードを使用する場合でも、トリガーは常にドキュメントを指している必要があります。たとえば、{messageCollectionId} はコレクションであるため、users/{userId}/{messageCollectionId} は無効になります。その一方で、{messageId} は常にドキュメントを指すため、users/{userId}/{messageCollectionId}/{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 ...

データの読み取りと書き込み

関数がトリガーされると、イベントに関連するデータのスナップショットが提供されます。このスナップショットを使用してイベントをトリガーしたドキュメントの読み書きをしたり、Firebase Admin SDK を使用してデータベースの他の部分にアクセスしたりできます。

イベントデータ

データの読み取り

関数がトリガーされた後、更新済みのドキュメントのデータを取得したり、更新前のデータを取得したりする必要が生じる場合があります。更新前のデータは、更新前のドキュメントのスナップショットを含む 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 データベースの特定のドキュメントに関連付けられています。関数に返されたスナップショット内のドキュメントにアクセスできます。

ドキュメントの参照には 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 FunctionsCloud Firestore トリガーには、次の制限事項があることに留意してください。

  • Cloud Functions(第 1 世代)では、Firestore ネイティブ モードで既存の「(デフォルト)」データベースがあることが前提となります。Cloud Firestore の名前付きデータベースや Datastore モードはサポートされていません。このような場合にイベントを構成するには、Cloud Functions(第 2 世代)を使用してください。
  • 順序は保証されません。短時間に複数の変更を行うと、予期しない順序で関数の呼び出しがトリガーされることがあります。
  • イベントは必ず 1 回以上処理されますが、1 つのイベントで関数が複数回呼び出される場合があります。「正確に 1 回」のメカニズムに依存することは避け、べき等性がある関数を記述してください。
  • Datastore モードの Cloud Firestore には、Cloud Functions(第 2 世代)が必要です。Cloud Functions(第 1 世代)では、Datastore モードはサポートされていません。
  • トリガーは、単一のデータベースに関連付けられます。複数のデータベースに一致するトリガーは作成できません。
  • データベースを削除しても、そのデータベースのトリガーは自動的に削除されません。トリガーはイベントの配信を停止しますが、トリガーを削除するまで存在し続けます。
  • 一致したイベントが最大リクエスト サイズを超えると、イベントが Cloud Functions(第 1 世代)に配信されない場合があります。
    • リクエスト サイズが原因で配信されなかったイベントは、プラットフォーム ログに記録され、プロジェクトのログ使用量にカウントされます。
    • これらのログは、ログ エクスプローラで「サイズが第 1 世代の上限を超えているため、イベントを Cloud Functions に配信できません...」という error 重大度メッセージとともに表示されます。関数名は functionName フィールドで確認できます。receiveTimestamp フィールドが現在から 1 時間以内であれば、タイムスタンプの前後のスナップショットで問題のドキュメントを読み取ることで、実際のイベントの内容を推測できます。
    • このようなケイデンスを回避するには、次のようにします。
      • Cloud Functions(第 2 世代)に移行してアップグレードする
      • ドキュメントのサイズを縮小する
      • 該当する Cloud Functions を削除する
    • 除外を使用してロギング自体を無効にすることもできますが、問題のあるイベントは引き続き配信されないことに注意してください。