Estendere Realtime Database con Cloud Functions


Con Cloud Functions, puoi gestire gli eventi in Firebase Realtime Database senza dover aggiornare il codice client. Cloud Functions ti consente di eseguire operazioni Realtime Database con privilegi amministrativi completi e garantisce che ogni modifica a Realtime Database venga elaborata singolarmente. Puoi apportare modifiche ai Firebase Realtime Database tramite lo snapshot dei dati o tramite l'SDK Admin.

In un ciclo di vita tipico, una funzione Firebase Realtime Database esegue le seguenti operazioni:

  1. Attende le modifiche a un determinato percorso Realtime Database.
  2. Si attiva quando si verifica un evento ed esegue le relative attività.
  3. Riceve un oggetto dati contenente uno snapshot dei dati archiviati in quel percorso.

Puoi attivare una funzione in risposta alla scrittura, alla creazione, all'aggiornamento o all'eliminazione di nodi del database in Firebase Realtime Database. Per controllare quando viene attivata la funzione, specifica uno dei gestori di eventi e il percorso Realtime Database in cui verranno ascoltati gli eventi.

Impostazione della posizione della funzione

La distanza tra la posizione di un'istanza Realtime Database e la posizione della funzione può creare una latenza di rete significativa. Inoltre, una mancata corrispondenza tra le regioni può causare un errore di deployment. Per evitare queste situazioni, specifica la posizione della funzione in modo che corrisponda alla posizione dell'istanza del database.

Gestione degli eventi Realtime Database

Le funzioni ti consentono di gestire gli eventi Realtime Database a due livelli di specificità: puoi ascoltare specificamente solo gli eventi di scrittura, creazione, aggiornamento o cancellazione oppure puoi ascoltare qualsiasi modifica di qualsiasi tipo a un riferimento.

Sono disponibili i seguenti gestori per rispondere agli eventi Realtime Database:

  • onValueWritten() Viene attivato quando i dati vengono creati, aggiornati o eliminati in Realtime Database.
  • onValueCreated() Viene attivato solo quando i dati vengono creati in Realtime Database.
  • onValueUpdated() Viene attivato solo quando i dati vengono aggiornati in Realtime Database.
  • onValueDeleted() Viene attivato solo quando i dati vengono eliminati in Realtime Database.
  • on_value_written() Viene attivato quando i dati vengono creati, aggiornati o eliminati in Realtime Database.
  • on_value_created() Viene attivato solo quando i dati vengono creati in Realtime Database.
  • on_value_updated() Viene attivato solo quando i dati vengono aggiornati in Realtime Database.
  • on_value_deleted() Viene attivato solo quando i dati vengono eliminati in Realtime Database.

Importa i moduli richiesti

Nell'origine della funzione devi importare i moduli SDK che vuoi utilizzare. Per questo esempio, è necessario importare i moduli HTTP e Realtime Database insieme al modulo Firebase Admin SDK per scrivere in Realtime Database.

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

// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require("firebase-admin");
admin.initializeApp();
# 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()

Specifica l'istanza e il percorso

Per controllare quando e dove deve essere attivata la funzione, configurala con un percorso e, facoltativamente, un'istanza Realtime Database. Se non specifichi un'istanza, la funzione ascolta tutte le istanze Realtime Database nella regione della funzione. Puoi anche specificare un Realtime Database pattern di istanze per eseguire il deployment in un sottoinsieme selettivo di istanze nella stessa regione.

Ad esempio:

// 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) => {
    // …
  }
);
# 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

Questi parametri indicano alla funzione di gestire le scritture in un determinato percorso all'interno dell'istanza Realtime Database.

Le specifiche del percorso corrispondono a tutte le scritture che toccano un percorso, incluse quelle che si verificano in qualsiasi punto sottostante. Se imposti il percorso per la funzione come /foo/bar, corrisponde agli eventi in entrambe le posizioni:

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

In entrambi i casi, Firebase interpreta che l'evento si verifica in /foo/bar e che i dati dell'evento includono i dati vecchi e nuovi in /foo/bar. Se i dati degli eventi potrebbero essere di grandi dimensioni, valuta la possibilità di utilizzare più funzioni in percorsi più profondi anziché una singola funzione vicino alla radice del database. Per ottenere il rendimento migliore, richiedi i dati solo al livello più granulare possibile.

Caratteri jolly e acquisizione

Puoi utilizzare {key}, {key=*}, {key=prefix*}, {key=*suffix} per acquisire. *, prefix*, *suffix per i caratteri jolly a un segmento. Nota: ** rappresenta i caratteri jolly con più segmenti, che Realtime Database non supporta. Consulta Informazioni sui pattern di percorso.

Sostituzioni generiche dei percorsi. Puoi specificare un componente del percorso come carattere jolly:

  • Utilizzo dell'asterisco, *. Ad esempio, foo/* corrisponde a qualsiasi elemento secondario a un livello della gerarchia del nodo sotto foo/.
  • Utilizzo di un segmento contenente esattamente un asterisco, *. Ad esempio, foo/app*-us corrisponde a qualsiasi segmento secondario sotto foo/ con prefisso app e suffisso -us.

I percorsi con caratteri jolly possono corrispondere a più eventi, ad esempio da una singola scrittura. Un inserto di

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

corrisponde al percorso "/foo/*" due volte: una volta con "hello": "world" e di nuovo con "firebase": "functions".

Acquisizione del percorso. Puoi acquisire le corrispondenze dei percorsi in variabili denominate da utilizzare nel codice della funzione (ad es. /user/{uid}, /user/{uid=*-us}).

I valori delle variabili di acquisizione sono disponibili nell'oggetto database.DatabaseEvent.params della funzione.

Sostituzioni generiche delle istanze. Puoi anche specificare un componente dell'istanza utilizzando i caratteri jolly. Un carattere jolly istanza può avere un prefisso, un suffisso o entrambi (ad es. my-app-*-prod).

Riferimento ai caratteri jolly e all'acquisizione

Con Cloud Functions (2ª gen.) e Realtime Database, è possibile utilizzare un pattern per specificare ref e instance. Ogni interfaccia di attivazione avrà le seguenti opzioni per definire l'ambito di una funzione:

Specifica di ref Specifica di instance Comportamento
Singolo (/foo/bar) Senza specificare Applica l'ambito all'handler a tutte le istanze nella regione della funzione.
Singolo (/foo/bar) Singolo (‘my-new-db') Associa l'handler degli ambiti all'istanza specifica nella regione della funzione.
Singolo (/foo/bar) Pattern (‘inst-prefix*') Applica l'ambito all'handler a tutte le istanze che corrispondono al pattern nella regione della funzione.
Pattern (/foo/{bar}) Senza specificare Applica l'ambito all'handler a tutte le istanze nella regione della funzione.
Pattern (/foo/{bar}) Singolo (‘my-new-db') Associa l'handler degli ambiti all'istanza specifica nella regione della funzione.
Pattern (/foo/{bar}) Pattern (‘inst-prefix*') Applica l'ambito all'handler a tutte le istanze che corrispondono al pattern nella regione della funzione.

Gestire i dati sugli eventi

Quando viene attivato un evento Realtime Database, viene passato un oggetto Event alla funzione di gestore. Questo oggetto ha una proprietà data che, per gli eventi di creazione ed eliminazione, contiene uno snapshot dei dati creati o eliminati.

In questo esempio, la funzione recupera i dati per il percorso a cui si fa riferimento, converte la stringa in quella posizione in maiuscolo e scrive la stringa modificata nel database:

// 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);
    },
);
@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)

Lettura del valore precedente

Per gli eventi write o update, la proprietà data è un oggetto Change contenente due istantanee che rappresentano lo stato dei dati prima e dopo l'evento di attivazione. L'oggetto Change ha una proprietà before che consente di controllare cosa è stato memorizzato in Realtime Database prima dell'evento e una proprietà after che rappresenta lo stato dei dati dopo l'evento.

Ad esempio, la proprietà before può essere utilizzata per assicurarsi che la funzione metta in maiuscolo il testo solo al momento della prima creazione:

  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);
      });
@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)