Google Cloud Firestore-Trigger

Cloud Functions kann Ereignisse im Cloud Firestore im selben Cloud-Projekt wie die Funktion verarbeiten. Sie haben dabei die Möglichkeit, Cloud Firestore als Antwort auf diese Ereignisse mithilfe der Firestore APIs und Clientbibliotheken zu lesen und/oder zu aktualisieren.

Der typische Lebenszyklus einer Cloud Firestore-Funktion sieht so aus:

  1. Sie wartet auf Änderungen an einem bestimmten Dokument.

  2. Sie wird ausgelöst, wenn ein Ereignis eintritt, und führt dessen Aufgaben aus.

  3. Sie empfängt ein Datenobjekt mit einem Snapshot des betreffenden Dokuments. Für write- oder update-Ereignisse enthält das Datenobjekt Snapshots, die den Dokumentstatus vor und nach dem auslösenden Ereignis darstellen.

Ereignistypen

Cloud Firestore unterstützt Ereignisse vom Typ create, update, delete und write. Das write-Ereignis umfasst alle Änderungen eines Dokuments.

Ereignistyp Trigger
providers/cloud.firestore/eventTypes/document.create (Standard) Wird ausgelöst, wenn ein Dokument zum ersten Mal beschrieben wird.
providers/cloud.firestore/eventTypes/document.update Wird ausgelöst, wenn ein Dokument bereits existiert und sich ein Wert geändert hat.
providers/cloud.firestore/eventTypes/document.delete Wird ausgelöst, wenn ein Dokument mit Daten gelöscht wird.
providers/cloud.firestore/eventTypes/document.write Wird ausgelöst, wenn ein Dokument erstellt, aktualisiert oder gelöscht wird.

Platzhalter werden in Triggern in geschweiften Klammern dargestellt: "/projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}".

Dokumentpfad angeben

Wenn Sie eine Funktion auslösen möchten, müssen Sie den Dokumentpfad angeben, der überwacht werden soll. Funktionen reagieren nur auf Dokumentänderungen und können keine einzelnen Felder oder Sammlungen überwachen. Hier sehen Sie ein paar Beispiele gültiger Dokumentpfade:

  • users/marie: Gültiger Trigger. Überwacht ein einzelnes Dokument, /users/marie.

  • users/{username}: Gültiger Trigger. Überwacht alle Nutzerdokumente. Bei Angabe von Platzhaltern werden alle Dokumente in der Sammlung überwacht.

  • users/{username}/addresses: Ungültiger Trigger. Bezieht sich auf die untergeordnete Sammlung addresses und nicht auf ein Dokument.

  • users/{username}/addresses/home: Gültiger Trigger. Überwacht das Privatadressdokument für alle Nutzer.

  • users/{username}/addresses/{addressId}: Gültiger Trigger. Überwacht alle Adressdokumente.

Platzhalter und Parameter verwenden

Wenn Sie das Dokument, das überwacht werden soll, nicht kennen, verwenden Sie {wildcard} anstelle der Dokument-ID:

  • users/{username} wartet auf Änderungen für alle Nutzerdokumente.

Wenn in diesem Beispiel ein Feld in einem Dokument im Verzeichnis users geändert wird, entspricht es einem Platzhalter namens {username}.

Wenn ein Dokument in users untergeordnete Sammlungen enthält und ein Feld in einem Dokument dieser Sammlungen geändert wird, wird der Platzhalter {username} nicht ausgelöst.

Platzhalterübereinstimmungen werden aus Dokumentpfaden extrahiert. Sie können beliebig viele Platzhalter für explizite Sammlungs- oder Dokument-IDs festzulegen.

Ereignisstruktur

Dieser Trigger löst die Funktion mit einem Ereignis wie etwa dem folgenden aus:

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

Jedes Document-Objekt enthält ein oder mehrere Value-Objekte. Informationen zu Typreferenzen finden Sie in der Dokumentation zu Value. Diese sind besonders hilfreich, wenn Sie eine Programmiersprache wie Go zum Schreiben Ihrer Funktionen verwenden.

Codebeispiel

Diese beispielhafte Cloud Functions-Funktion gibt die Felder eines auslösenden Cloud Firestore-Ereignisses aus:

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.oldValue && Object.keys(event.oldValue).length) {
    console.log('\nOld value:');
    console.log(JSON.stringify(event.oldValue, null, 2));
  }

  if (event.value && Object.keys(event.value).length) {
    console.log('\nNew value:');
    console.log(JSON.stringify(event.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());
    }
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FirebaseFirestore
{
    public class Function : ICloudEventFunction<DocumentEventData>
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
        {
            _logger.LogInformation("Function triggered by event on {subject}", cloudEvent.Subject);
            _logger.LogInformation("Event type: {type}", cloudEvent.Type);
            MaybeLogDocument("Old value", data.OldValue);
            MaybeLogDocument("New value", data.Value);

            // In this example, we don't need to perform any asynchronous operations, so the
            // method doesn't need to be declared async.
            return Task.CompletedTask;
        }

        /// <summary>
        /// Logs the names and values of the fields in a document in a very simplistic way.
        /// </summary>
        private void MaybeLogDocument(string message, Document document)
        {
            if (document is null)
            {
                return;
            }

            // ConvertFields converts the Firestore representation into a .NET-friendly
            // representation.
            IReadOnlyDictionary<string, object> fields = document.ConvertFields();
            var fieldNamesAndTypes = fields
                .OrderBy(pair => pair.Key)
                .Select(pair => $"{pair.Key}: {pair.Value}");
            _logger.LogInformation(message + ": {fields}", string.Join(", ", fieldNamesAndTypes));
        }
    }
}

Ruby

require "functions_framework"

# Triggered by a change to a Firestore document.
FunctionsFramework.cloud_event "hello_firestore" do |event|
  # The event parameter is a CloudEvents::Event::V1 object.
  # See https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event/V1.html
  payload = event.data

  logger.info "Function triggered by change to: #{event.source}"
  logger.info "Old value: #{payload['oldValue']}"
  logger.info "New value: #{payload['value']}"
end

Im folgenden Beispiel wird der vom Nutzer hinzugefügte Wert abgerufen, der String an dieser Stelle in Großbuchstaben umgewandelt und der Wert durch den String in Großbuchstaben ersetzt:

Node.js

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

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

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

  const curValue = event.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();

  if (curValue !== newValue) {
    console.log(`Replacing value: ${curValue} --> ${newValue}`);

    return affectedDoc.set({
      original: newValue,
    });
  } else {
    // Value is already upper-case
    // Don't perform a(nother) write to avoid infinite loops
    console.log('Value is already upper-case.');
  }
};

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()

    if cur_value != new_value:
        print(f'Replacing value: {cur_value} --> {new_value}')
        affected_doc.set({
            u'original': new_value
        })
    else:
        # Value is already upper-case
        # Don't perform a second write (which can trigger an infinite loop)
        print('Value is already upper-case.')

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/v4"
)

// 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("\"", "");

    if (!currentValue.equals(newValue)) {
      // The stored value needs to be updated
      // Write the upper-cased value to Firestore
      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);
      }
    } else {
      // The stored value is already upper-case, and doesn't need updating.
      // (Don't perform a "second" write, since that could trigger an infinite loop.)
      logger.info(String.format("Value is already upper-case."));
    }
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Firestore;
using Google.Cloud.Functions.Framework;
using Google.Cloud.Functions.Hosting;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace FirestoreReactive
{
    public class Startup : FunctionsStartup
    {
        public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) =>
            services.AddSingleton(FirestoreDb.Create());
    }

    // Register the startup class to provide the Firestore dependency.
    [FunctionsStartup(typeof(Startup))]
    public class Function : ICloudEventFunction<DocumentEventData>
    {
        private readonly ILogger _logger;
        private readonly FirestoreDb _firestoreDb;

        public Function(ILogger<Function> logger, FirestoreDb firestoreDb) =>
            (_logger, _firestoreDb) = (logger, firestoreDb);

        public async Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
        {
            // Get the recently-written value. This expression will result in a null value
            // if any of the following is true:
            // - The event doesn't contain a "new" document
            // - The value doesn't contain a field called "original"
            // - The "original" field isn't a string
            string currentValue = data.Value?.ConvertFields().GetValueOrDefault("original") as string;
            if (currentValue is null)
            {
                _logger.LogWarning($"Event did not contain a suitable document");
                return;
            }

            string newValue = currentValue.ToUpperInvariant();
            if (newValue == currentValue)
            {
                _logger.LogInformation("Value is already upper-cased; no replacement necessary");
                return;
            }

            // The CloudEvent subject is "documents/x/y/...".
            // The Firestore SDK FirestoreDb.Document method expects a reference relative to
            // "documents" (so just the "x/y/..." part). This may be simplified over time.
            if (cloudEvent.Subject is null || !cloudEvent.Subject.StartsWith("documents/"))
            {
                _logger.LogWarning("CloudEvent subject is not a document reference.");
                return;
            }
            string documentPath = cloudEvent.Subject.Substring("documents/".Length);

            _logger.LogInformation("Replacing '{current}' with '{new}' in '{path}'", currentValue, newValue, documentPath);
            await _firestoreDb.Document(documentPath).UpdateAsync("original", newValue);
        }
    }
}

Funktion bereitstellen

Mit dem folgenden gcloud-Befehl wird eine Funktion bereitgestellt, die durch Schreibereignisse im Dokument /messages/{pushId} ausgelöst wird:

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}"
Argument Beschreibung
--runtime RUNTIME Der Name der Laufzeit, die Sie verwenden. Eine vollständige Liste finden Sie in der gcloud-Referenz.
--trigger-event NAME Der Ereignistyp, den die Funktion überwacht, entweder write, create, update oder delete.
--trigger-resource NAME Der voll qualifizierte Datenbankpfad, den die Funktion überwacht. Dieser muss im Format "projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH" angegeben werden. Der Text {pushId} ist ein Platzhalterparameter, der oben unter Dokumentpfad festlegen beschrieben wird.

Einschränkungen und Garantien

Firestore-Trigger für Cloud Functions sind ein Beta-Feature mit einigen bekannten Einschränkungen:

  • Es kann bis zu zehn Sekunden dauern, bis eine Funktion auf Änderungen in Firestore reagiert.
  • Die Reihenfolge ist nicht garantiert. Schnelle Änderungen können Funktionsaufrufe in einer unvorhergesehenen Reihenfolge auslösen.
  • Ereignisse werden mindestens einmal übergeben. Ein einzelnes Ereignis kann aber zu mehreren Funktionsaufrufen führen. Vermeiden Sie die Abhängigkeit von genau einmal vorkommenden Verfahren und schreiben Sie idempotente Funktionen.
  • Firestore-Trigger für Cloud Functions sind nur für Firestore im nativen Modus verfügbar. Sie stehen nicht für Firestore im Datastore-Modus zur Verfügung.