Google Cloud Firestore トリガー

Cloud Functions では、クライアント コードを更新することなく、Cloud Firestore 内のイベントを処理できます。Firestore API とクライアント ライブラリを使用すると、これらのイベントの発生時に Cloud Firestore の読み取りや更新を行うことができます。

一般的なライフサイクルの場合、Cloud Firestore の関数は次のように動作します。

  1. 特定のドキュメントに変更が加えられるのを待ちます。

  2. イベントが発生するとトリガーされ、そのタスクを実行します。

  3. 影響を受けるドキュメントのスナップショットを含むデータ オブジェクトを受信します。write または update イベントの場合、トリガー イベントの前後のドキュメントの状態を表すスナップショットがデータ オブジェクトに含まれます。

イベントタイプ

Cloud Firestore は、createupdatedeletewrite イベントをサポートします。write イベントには、ドキュメントに対するすべての変更が含まれます。

イベントタイプ トリガー
providers/cloud.firestore/eventTypes/document.create ドキュメントが最初に書き込まれたときにトリガーされます。
providers/cloud.firestore/eventTypes/document.update すでに存在するドキュメントの値が変更されたときにトリガーされます。
providers/cloud.firestore/eventTypes/document.delete データを含むドキュメントが削除されたときにトリガーされます。
providers/cloud.firestore/eventTypes/document.write ドキュメントが作成、更新、削除されたときにトリガーされます。

ワイルドカードは、次のように中括弧で記述します。 /projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

ドキュメント パスの指定

関数をトリガーするには、リッスンするドキュメント パスを指定します。関数はドキュメントの変更にのみ対応します。特定のフィールドまたはコレクションをモニタリングすることはできません。有効なドキュメント パスは次のとおりです。

  • users/marie: 有効なトリガー。1 つのドキュメント(/users/marie)をモニタリングします。

  • users/{username}: 有効なトリガー。すべてのユーザー ドキュメントをモニタリングします。コレクション内のすべてのドキュメントをモニタリングする場合は、ワイルドカードを使用します。

  • users/{username}/addresses: 無効なトリガー。ドキュメントではなく、サブコレクション addresses を参照します。

  • users/{username}/addresses/home: 有効なトリガー。すべてのユーザーの自宅住所のドキュメントをモニタリングします。

  • users/{username}/addresses/{addressId}: 有効なトリガー。すべての住所ドキュメントをモニタリングします。

ワイルドカードとパラメータの使用

モニタリングするドキュメントがわからない場合は、ドキュメント ID の代わりに {wildcard} を使用します。

  • users/{username} は、すべてのユーザー ドキュメントに対する変更をリッスンします。

この例では、users にあるドキュメントの任意のフィールドが変更されると、{username} というワイルドカードと照合されます。

users のドキュメントにサブコレクションがあり、サブコレクションのいずれかのドキュメントのフィールドが変更された場合、{username} ワイルドカードはトリガーされません。

ワイルドカードと一致した部分がドキュメント パスから抽出され、event.params に格納されます。明示的なコレクションまたはドキュメント ID に置き換えるワイルドカードは、必要な数だけ定義できます。

イベントの構造

このトリガーは、次のようなイベントで関数を呼び出します。

{
    "oldValue": { // Update and Delete operations only
        A Document object containing a pre-operation document snapshot
    },
    "updateMask": { // Update operations only
        A DocumentMask object that lists changed fields.
    },
    "value": {
        // A Document object containing a post-operation document snapshot
    }
}

それぞれの Document オブジェクトに 1 つまたは複数の Value オブジェクトが含まれます。型の詳細については、Value ドキュメントをご覧ください。これは、Go などの型言語を使用して関数を記述する場合に便利です。

コードサンプル

以下の Cloud Functions の関数のサンプルでは、トリガーする Cloud Firestore イベントのフィールドを出力します。

Node.js

/**
 * Triggered by a change to a Firestore document.
 *
 * @param {!Object} event The Cloud Functions event.
 */
exports.helloFirestore = (event) => {
  const triggerResource = event.resource;

  console.log(`Function triggered by event on: ${triggerResource}`);
  console.log(`Event type: ${event.eventType}`);

  if (event.data.oldValue && Object.keys(event.data.oldValue).length) {
    console.log(`\nOld value:`);
    console.log(JSON.stringify(event.data.oldValue, null, 2));
  }

  if (event.data.value && Object.keys(event.data.value).length) {
    console.log(`\nNew value:`);
    console.log(JSON.stringify(event.data.value, null, 2));
  }
};

Python

import json
def hello_firestore(data, context):
    """ Triggered by a change to a Firestore document.
    Args:
        data (dict): The event payload.
        context (google.cloud.functions.Context): Metadata for the event.
    """
    trigger_resource = context.resource

    print('Function triggered by change to: %s' % trigger_resource)

    print('\nOld value:')
    print(json.dumps(data["oldValue"]))

    print('\nNew value:')
    print(json.dumps(data["value"]))

Go


// Package hello contains a Cloud Function triggered by a Firestore event.
package hello

import (
	"context"
	"fmt"
	"log"
	"time"

	"cloud.google.com/go/functions/metadata"
)

// FirestoreEvent is the payload of a Firestore event.
type FirestoreEvent struct {
	OldValue   FirestoreValue `json:"oldValue"`
	Value      FirestoreValue `json:"value"`
	UpdateMask struct {
		FieldPaths []string `json:"fieldPaths"`
	} `json:"updateMask"`
}

// FirestoreValue holds Firestore fields.
type FirestoreValue struct {
	CreateTime time.Time `json:"createTime"`
	// Fields is the data for this value. The type depends on the format of your
	// database. Log the interface{} value and inspect the result to see a JSON
	// representation of your database fields.
	Fields     interface{} `json:"fields"`
	Name       string      `json:"name"`
	UpdateTime time.Time   `json:"updateTime"`
}

// HelloFirestore is triggered by a change to a Firestore document.
func HelloFirestore(ctx context.Context, e FirestoreEvent) error {
	meta, err := metadata.FromContext(ctx)
	if err != nil {
		return fmt.Errorf("metadata.FromContext: %v", err)
	}
	log.Printf("Function triggered by change to: %v", meta.Resource)
	log.Printf("Old value: %+v", e.OldValue)
	log.Printf("New value: %+v", e.Value)
	return nil
}

Java

import com.google.cloud.functions.Context;
import com.google.cloud.functions.RawBackgroundFunction;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.logging.Logger;

public class FirebaseFirestore implements RawBackgroundFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestore.class.getName());

  // Use GSON (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  @Override
  public void accept(String json, Context context) {
    JsonObject body = gson.fromJson(json, JsonObject.class);
    logger.info("Function triggered by event on: " + context.resource());
    logger.info("Event type: " + context.eventType());

    if (body != null && body.has("oldValue")) {
      logger.info("Old value:");
      logger.info(body.get("oldValue").getAsString());
    }

    if (body != null && body.has("value")) {
      logger.info("New value:");
      logger.info(body.get("value").getAsString());
    }
  }
}

次の例では、ユーザーが追加した値を取得し、その場所にある文字列を大文字に変換して、大文字の文字列で置き換えます。

Node.js

const Firestore = require('@google-cloud/firestore');

const firestore = new Firestore({
  projectId: process.env.GCP_PROJECT,
});

// Converts strings added to /messages/{pushId}/original to uppercase
exports.makeUpperCase = (event) => {
  const {resource} = event;
  const affectedDoc = firestore.doc(resource.split('/documents/')[1]);

  const curValue = event.data.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();
  console.log(`Replacing value: ${curValue} --> ${newValue}`);

  return affectedDoc.set({
    original: newValue,
  });
};

Python

from google.cloud import firestore
client = firestore.Client()

# Converts strings added to /messages/{pushId}/original to uppercase
def make_upper_case(data, context):
    path_parts = context.resource.split('/documents/')[1].split('/')
    collection_path = path_parts[0]
    document_path = '/'.join(path_parts[1:])

    affected_doc = client.collection(collection_path).document(document_path)

    cur_value = data["value"]["fields"]["original"]["stringValue"]
    new_value = cur_value.upper()
    print(f'Replacing value: {cur_value} --> {new_value}')

    affected_doc.set({
        u'original': new_value
    })

Go


// Package upper contains a Firestore Cloud Function.
package upper

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go"
)

// FirestoreEvent is the payload of a Firestore event.
type FirestoreEvent struct {
	OldValue   FirestoreValue `json:"oldValue"`
	Value      FirestoreValue `json:"value"`
	UpdateMask struct {
		FieldPaths []string `json:"fieldPaths"`
	} `json:"updateMask"`
}

// FirestoreValue holds Firestore fields.
type FirestoreValue struct {
	CreateTime time.Time `json:"createTime"`
	// Fields is the data for this value. The type depends on the format of your
	// database. Log an interface{} value and inspect the result to see a JSON
	// representation of your database fields.
	Fields     MyData    `json:"fields"`
	Name       string    `json:"name"`
	UpdateTime time.Time `json:"updateTime"`
}

// MyData represents a value from Firestore. The type definition depends on the
// format of your database.
type MyData struct {
	Original struct {
		StringValue string `json:"stringValue"`
	} `json:"original"`
}

// GOOGLE_CLOUD_PROJECT is automatically set by the Cloud Functions runtime.
var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")

// client is a Firestore client, reused between function invocations.
var client *firestore.Client

func init() {
	// Use the application default credentials.
	conf := &firebase.Config{ProjectID: projectID}

	// Use context.Background() because the app/client should persist across
	// invocations.
	ctx := context.Background()

	app, err := firebase.NewApp(ctx, conf)
	if err != nil {
		log.Fatalf("firebase.NewApp: %v", err)
	}

	client, err = app.Firestore(ctx)
	if err != nil {
		log.Fatalf("app.Firestore: %v", err)
	}
}

// MakeUpperCase is triggered by a change to a Firestore document. It updates
// the `original` value of the document to upper case.
func MakeUpperCase(ctx context.Context, e FirestoreEvent) error {
	fullPath := strings.Split(e.Value.Name, "/documents/")[1]
	pathParts := strings.Split(fullPath, "/")
	collection := pathParts[0]
	doc := strings.Join(pathParts[1:], "/")

	curValue := e.Value.Fields.Original.StringValue
	newValue := strings.ToUpper(curValue)
	if curValue == newValue {
		log.Printf("%q is already upper case: skipping", curValue)
		return nil
	}
	log.Printf("Replacing value: %q -> %q", curValue, newValue)

	data := map[string]string{"original": newValue}
	_, err := client.Collection(collection).Doc(doc).Set(ctx, data)
	if err != nil {
		return fmt.Errorf("Set: %v", err)
	}
	return nil
}

Java


import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.SetOptions;
import com.google.cloud.functions.Context;
import com.google.cloud.functions.RawBackgroundFunction;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class FirebaseFirestoreReactive implements RawBackgroundFunction {

  // Use GSON (https://github.com/google/gson) to parse JSON content.
  private static final Gson gson = new Gson();

  private static final Logger logger = Logger.getLogger(FirebaseFirestoreReactive.class.getName());
  private static final Firestore FIRESTORE = FirestoreOptions.getDefaultInstance().getService();

  private final Firestore firestore;

  public FirebaseFirestoreReactive() {
    this(FIRESTORE);
  }

  FirebaseFirestoreReactive(Firestore firestore) {
    this.firestore = firestore;
  }

  @Override
  public void accept(String json, Context context) {
    // Get the recently-written value
    JsonObject body = gson.fromJson(json, JsonObject.class);
    JsonObject tempJson = body.getAsJsonObject("value");

    // Verify that value.fields.original.stringValue exists
    String currentValue = null;
    if (tempJson != null) {
      tempJson = tempJson.getAsJsonObject("fields");
    }
    if (tempJson != null) {
      tempJson = tempJson.getAsJsonObject("original");
    }
    if (tempJson != null && tempJson.has("stringValue")) {
      currentValue = tempJson.get("stringValue").getAsString();
    }
    if (currentValue == null) {
      throw new IllegalArgumentException("Malformed JSON: " + json);
    }

    // Convert recently-written value to ALL CAPS
    String newValue = currentValue.toUpperCase(Locale.getDefault());

    // Update Firestore DB with ALL CAPS value
    Map<String, String> newFields = Map.of("original", newValue);

    String affectedDoc = context.resource().split("/documents/")[1].replace("\"", "");

    logger.info(String.format("Replacing value: %s --> %s", currentValue, newValue));
    try {
      FIRESTORE.document(affectedDoc).set(newFields, SetOptions.merge()).get();
    } catch (ExecutionException | InterruptedException e) {
      logger.log(Level.SEVERE, "Error updating Firestore document: " + e.getMessage(), e);
    }
  }
}

関数のデプロイ

次の gcloud コマンドは、ドキュメント /messages/{pushId} に対する書き込みイベントによってトリガーされる関数をデプロイします。

gcloud functions deploy FUNCTION_NAME \
  --runtime RUNTIME \
  --trigger-event providers/cloud.firestore/eventTypes/document.write \
  --trigger-resource projects/YOUR_PROJECT_ID/databases/(default)/documents/messages/{pushId}
引数 説明
--runtime RUNTIME 使用しているランタイムの名前。網羅的なリストについては、gcloud リファレンスをご覧ください。
--trigger-event NAME 関数がモニタリングするイベントタイプ(writecreateupdate または delete)。
--trigger-resource NAME 関数がリッスンするデータベース パスの完全修飾名。 次の形式に従う必要があります。 projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH {pushId} テキストは、ドキュメント パスの指定で説明したワイルドカード パラメータです。

制約と保証

Cloud Functions 用 Firestore トリガーはベータ版の機能であり、いくつかの既知の制限があります。

  • 関数が Firestore の変更に応答するまで最大 10 秒かかることがあります。
  • 順序は保証されません。短時間に複数の変更を行うと、予期しない順序で関数がトリガーされることがあります。
  • イベントは必ず 1 回以上処理されますが、1 つのイベントで関数が複数回呼び出される場合があります。「正確に 1 回」のメカニズムに依存することは避け、べき等になるように関数を記述してください。
  • Cloud Functions 用 Firestore トリガーは、ネイティブ モードの Firestore でのみ使用できます。Datastore モードの Firestore では使用できません。