Acionadores do Google Cloud Firestore (1.ª geração)
As funções do Cloud Run podem processar eventos no Firestore no mesmo Google Cloud projeto que a função. Pode ler ou atualizar o Firestore em resposta a estes eventos através das APIs e bibliotecas cliente do Firestore.
Num ciclo de vida típico, uma função do Firestore faz o seguinte:
Aguarda alterações a um documento específico.
É acionado quando ocorre um evento e executa as respetivas tarefas.
Recebe um objeto de dados com um instantâneo do documento afetado. Para eventos
write
ouupdate
, o objeto de dados contém instantâneos que representam o estado do documento antes e depois do evento de acionamento.
Tipos de eventos
O Firestore suporta eventos create
, update
, delete
e write
. O evento
write
abrange todas as modificações a um documento.
Tipo de evento | Acionador |
---|---|
providers/cloud.firestore/eventTypes/document.create (predefinição) |
Acionado quando um documento é escrito pela primeira vez. |
providers/cloud.firestore/eventTypes/document.update |
Acionado quando já existe um documento e qualquer valor é alterado. |
providers/cloud.firestore/eventTypes/document.delete |
Acionado quando um documento com dados é eliminado. |
providers/cloud.firestore/eventTypes/document.write |
Acionado quando um documento é criado, atualizado ou eliminado. |
Os carateres universais são escritos em acionadores com chavetas, da seguinte forma:
"projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}"
Especificar o caminho do documento
Para acionar a função, especifique um caminho do documento a ouvir. As funções só respondem a alterações de documentos e não podem monitorizar campos específicos nem coleções. Seguem-se alguns exemplos de caminhos de documentos válidos:
users/marie
: acionador válido. Monitoriza um único documento,/users/marie
.users/{username}
: acionador válido. Monitoriza todos os documentos do utilizador. Os carateres universais são usados para monitorizar todos os documentos na coleção.users/{username}/addresses
: acionador inválido. Refere-se à subcoleçãoaddresses
e não a um documento.users/{username}/addresses/home
: acionador válido. Monitoriza o documento de morada para todos os utilizadores.users/{username}/addresses/{addressId}
: acionador válido. Monitoriza todos os documentos de morada.
Usar carateres universais e parâmetros
Se não souber o documento específico que quer monitorizar, use {wildcard}
em vez do ID do documento:
users/{username}
ouve as alterações a todos os documentos do utilizador.
Neste exemplo, quando qualquer campo em qualquer documento em users
é alterado, corresponde a um caráter universal denominado {username}
.
Se um documento em users
tiver
subcoleções e um
campo num dos documentos dessas subcoleções for alterado, o {username}
caráter universal não é acionado.
As correspondências com carateres universais são extraídas dos caminhos dos documentos. Pode definir quantos carateres universais quiser para substituir IDs de documentos ou de recolha explícitos.
Estrutura de eventos
Este acionador invoca a sua função com um evento semelhante ao apresentado abaixo:
{ "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 } }
Cada objeto Document
contém um ou mais objetos Value
. Consulte a documentação do Value
para referências de tipos. Isto é especialmente útil se estiver a usar uma linguagem tipada (como Go) para escrever as suas funções.
Exemplo de código
A função do Google Cloud abaixo imprime os campos de um evento do Cloud Firestore de acionamento:
Node.js
Python
Go
Java
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
PHP
use Google\CloudFunctions\CloudEvent; function firebaseFirestore(CloudEvent $cloudevent) { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); fwrite($log, 'Event: ' . $cloudevent->getId() . PHP_EOL); fwrite($log, 'Event Type: ' . $cloudevent->getType() . PHP_EOL); $data = $cloudevent->getData(); $resource = $data['resource']; fwrite($log, 'Function triggered by event on: ' . $resource . PHP_EOL); if (isset($data['oldValue'])) { fwrite($log, 'Old value: ' . json_encode($data['oldValue']) . PHP_EOL); } if (isset($data['value'])) { fwrite($log, 'New value: ' . json_encode($data['value']) . PHP_EOL); } }
O exemplo abaixo obtém o valor adicionado pelo utilizador, converte a string nessa localização em letras maiúsculas e substitui o valor pela string em letras maiúsculas:
Node.js
Python
Go
Java
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, cancellationToken: cancellationToken); } }
Ruby
PHP
use Google\Cloud\Firestore\FirestoreClient; use Google\CloudFunctions\CloudEvent; function firebaseReactive(CloudEvent $cloudevent) { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); $data = $cloudevent->getData(); $resource = $data['value']['name']; $db = new FirestoreClient(); $docPath = explode('/documents/', $resource)[1]; $affectedDoc = $db->document($docPath); $curValue = $data['value']['fields']['original']['stringValue']; $newValue = strtoupper($curValue); if ($curValue !== $newValue) { fwrite($log, 'Replacing value: ' . $curValue . ' --> ' . $newValue . PHP_EOL); $affectedDoc->set(['original' => $newValue]); } else { // Value is already upper-case // Don't perform another write (it might cause an infinite loop) fwrite($log, 'Value is already upper-case.' . PHP_EOL); } }
Implementar a sua função
O seguinte comando gcloud
implementa uma função acionada por eventos de escrita no documento /messages/{pushId}
:
gcloud functions deploy FUNCTION_NAME \ --no-gen2 \ --entry-point ENTRY_POINT \ --runtime RUNTIME \ --set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID \ --trigger-event "providers/cloud.firestore/eventTypes/document.write" \ --trigger-resource "projects/YOUR_PROJECT_ID/databases/(default)/documents/messages/{pushId}"
Argumento | Descrição |
---|---|
FUNCTION_NAME |
O nome registado da função do Cloud que está a implementar.
Pode ser o nome de uma função no seu código-fonte ou uma string arbitrária. Se FUNCTION_NAME for uma string arbitrária, tem de incluir a flag --entry-point .
|
--entry-point ENTRY_POINT |
O nome de uma função ou classe no seu código-fonte. Opcional, a menos que não tenha usado FUNCTION_NAME
para especificar a função no código fonte a ser executada durante a implementação. Nesse caso, tem de usar --entry-point para indicar o nome da função executável.
|
--runtime RUNTIME |
O nome do tempo de execução que está a usar. Para ver uma lista completa, consulte a
gcloud referência.
|
--set-env-vars GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID |
O identificador exclusivo do projeto como uma variável do ambiente de tempo de execução. |
--trigger-event NAME |
O tipo de evento que a função vai monitorizar (um dos seguintes:
write , create , update ou
delete ).
|
--trigger-resource NAME |
O caminho da base de dados totalmente qualificado que a função vai ouvir.
Tem de estar em conformidade com o seguinte formato:
"projects/YOUR_PROJECT_ID/databases/(default)/documents/PATH"
O texto {pushId} é um parâmetro de carateres universais descrito acima
em Especificar o caminho do documento.
|
Limitações
Tenha em atenção as seguintes limitações para acionadores do Firestore para funções do Cloud Run:
- As funções do Cloud Run (1.ª geração) requerem uma base de dados "(default)" existente no modo nativo do Firestore. Não suporta bases de dados com nome do Firestore nem o modo Datastore. Use as funções do Cloud Run (2.ª geração) para configurar eventos nesses casos.
- A configuração entre projetos com funções do Cloud Run e o acionador do Firestore é uma limitação. Para configurar funções do Cloud Run com acionadores do Firestore, estas têm de estar no mesmo projeto.
- A ordenação não é garantida. As alterações rápidas podem acionar invocações de funções numa ordem inesperada.
- Os eventos são entregues, pelo menos, uma vez, mas um único evento pode resultar em várias invocações de funções. Evite depender de mecanismos de execução única e escreva funções idempotentes.
- O Firestore no modo Datastore requer funções do Cloud Run (2.ª geração). As funções do Cloud Run (1.ª geração) não suportam o modo Datastore.
- Um acionador está associado a uma única base de dados. Não pode criar um acionador que corresponda a várias bases de dados.
- A eliminação de uma base de dados não elimina automaticamente os acionadores dessa base de dados. O acionador deixa de enviar eventos, mas continua a existir até eliminar o acionador.
- Se um evento correspondente exceder o tamanho máximo do pedido, o evento pode não ser entregue às funções do Cloud Run (1.ª geração).
- Os eventos não entregues devido ao tamanho do pedido são registados nos registos da plataforma e contam para a utilização de registos do projeto.
- Pode encontrar estes registos no Explorador de registos com a mensagem "Event cannot deliver to
Cloud function due to size exceeding the limit for 1st gen..." de
error
gravidade. Pode encontrar o nome da função no campofunctionName
. Se o camporeceiveTimestamp
ainda estiver dentro de uma hora a partir de agora, pode inferir o conteúdo real do evento lendo o documento em questão com uma captura de ecrã antes e depois da data/hora. - Para evitar esta cadência, pode:
- Migre e atualize para as funções do Cloud Run (2.ª geração)
- Reduza o tamanho do documento
- Elimine as funções do Cloud Run em questão
- Pode desativar o próprio registo através de exclusões, mas tenha em atenção que os eventos ofensivos continuam a não ser enviados.