Ricevi aggiornamenti in tempo reale da SQL Connect

Il codice client può iscriversi alle query per ricevere aggiornamenti in tempo reale quando il risultato della query cambia.

Prima di iniziare

  • Configura la generazione dell'SDK per il tuo progetto come descritto nella documentazione per web, piattaforme Apple e Flutter.

    • Devi attivare la memorizzazione nella cache lato client per tutti gli SDK generati. Nello specifico, ogni configurazione dell'SDK deve contenere una dichiarazione come la seguente:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • I client della tua app devono utilizzare una versione recente dell'SDK core SQL Connect:

    • Apple: SDK per Swift versione 11.12.0 o successiveFirebase SQL Connect
    • Web: SDK JavaScript versione 12.12.0 o successive
    • Flutter: firebase_data_connect versione 0.3.0 o successive
  • Rigenera gli SDK client utilizzando la versione 15.14.0 o successive dell'interfaccia a riga di comando di Firebase.

Iscrizione ai risultati delle query

Puoi iscriverti a una query per rispondere alle modifiche nel risultato della query. Ad esempio, supponiamo di avere lo schema e le operazioni seguenti definiti nel progetto:

# dataconnect/schema/schema.gql

type Movie @table(key: "id") {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int
  genre: String
  description: String
  averageRating: Int
}
# dataconnect/connector/operations.gql

query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id
    title
    releaseYear
    genre
    description
  }
}

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      description: $description
    })
}

Per iscriverti alle modifiche nel risultato dell'esecuzione di GetMovieById:

Web

import { subscribe, DataConnectError, QueryResult } from 'firebase/data-connect';
import { getMovieByIdRef, GetMovieByIdData, GetMovieByIdVariables } from '@dataconnect/generated';

const queryRef = getMovieByIdRef({ id: "<MOVIE_ID>" });

// Called when receiving an update.
const onNext = (result: QueryResult<GetMovieByIdData, GetMovieByIdVariables>) => {
  console.log("Movie <MOVIE_ID> updated", result);
}

const onError = (err?: DataConnectError) => {
  console.error("received error", err);
}

// Called when unsubscribing or when the subscription is automatically released.
const onComplete = () => {
  console.log("subscription complete!");
}

const unsubscribe = subscribe(queryRef, onNext, onError, onComplete);

Web (React)

import { subscribe, QueryResult } from 'firebase/data-connect';
import { getMovieByIdRef, GetMovieByIdData, GetMovieByIdVariables } from '@dataconnect/generated';
import { useState, useEffect } from "react";

export const MovieInfo = ({ id: movieId }: { id: string }) => {
  const [movieInfo, setMovieInfo] = useState<GetMovieByIdData>();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const queryRef = getMovieByIdRef({ id: movieId });

    function updateUi(result: QueryResult<GetMovieByIdData, GetMovieByIdVariables>): void {
      setMovieInfo(result.data);
      setLoading(false);
    }

    const unsubscribe = subscribe(
      queryRef,
      updateUi,
      (err) => {
        setError(err ?? new Error("Unknown error occurred"));
        setLoading(false);
      }
    );

    return () => unsubscribe();
  }, [movieId]);

  if (loading)
    return <div>Loading movie details...</div>;
  if (error || !movieInfo || !movieInfo.movie)
    return <div>Error loading movie details: {error?.message}</div>;

  return (
    <div>
      <h2>{movieInfo.movie.title} ({movieInfo.movie.releaseYear})</h2>
      <ul>
        <li>Genre: {movieInfo.movie.genre}</li>
        <li>Description: {movieInfo.movie.description}</li>
      </ul>
    </div>
  );
};

SQL Connect supporta anche la memorizzazione nella cache e le sottoscrizioni in tempo reale utilizzando TanStack. Quando specifichi react: true o angular: true nel file connector.yaml, SQL Connect genera binding per React o Angular utilizzando TanStack.

Questi binding possono funzionare insieme al supporto integrato in tempo reale di SQL Connect, ma solo con qualche difficoltà. Ti consigliamo di utilizzare i binding basati su TanStack o il supporto in tempo reale integrato di SQL Connect, ma non entrambi.

Tieni presente che l'implementazione in tempo reale di SQL Connect presenta alcuni vantaggi rispetto ai binding TanStack:

  • Memorizzazione nella cache normalizzata: SQL Connect implementa la memorizzazione nella cache normalizzata, che migliora la coerenza dei dati, nonché l'efficienza della memoria e della rete rispetto alla memorizzazione nella cache a livello di query. Con la memorizzazione nella cache normalizzata, se un'entità viene aggiornata in un'area dell'app, verrà aggiornata anche in altre aree che utilizzano l'entità.
  • Invalidazione remota: SQL Connect può invalidare da remoto le entità memorizzate nella cache su tutti i dispositivi abbonati.

Se scegli di non utilizzare TanStack, devi rimuovere le impostazioni react: true e angular: true dal file connector.yaml.

iOS

struct ListMovieView: View {
    // QueryRef has the Observable attribute, so its properties will
    // automatically trigger updates on changes.
    private var queryRef = connector.listMoviesByGenreQuery.ref(genre: "Sci-Fi")

    // Store the handle to unsubscribe from query updates.
    @State private var querySub: AnyCancellable?

    var body: some View {
        VStack {
            // Use the query results in a View.
            ForEach(queryRef.data?.movies ?? [], id: \.self.id) { movie in
                    Text(movie.title)
                }
        }
        .onAppear {
            // Subscribe to the query for updates using the Observable macro.
            Task {
                do {
                    querySub = try await queryRef.subscribe().sink { _ in }
                } catch {
                    print("Error subscribing to query: \(error)")
                }
            }
        }
        .onDisappear {
          querySub?.cancel()
        }
    }
}

Flutter

Importa l'SDK generato del progetto:

import 'package:flutter_app/dataconnect_generated/generated.dart';

Quindi chiama il metodo subscribe() su un riferimento alla query:

final queryRef = MovieConnector.instance.getMovieById(id: "<MOVIE_ID>").ref();
final subscription = queryRef.subscribe().listen((result) {
  final movie = result.data.movie;
  if (movie != null) {
    // Execute your logic to update the UI with the refreshed movie information.
    updateUi(movie.title);
  }
});

Per interrompere gli aggiornamenti, puoi chiamare il numero subscription.cancel().

Una volta eseguita la sottoscrizione alla query come nell'esempio precedente, riceverai aggiornamenti ogni volta che il risultato della query specifica cambia. Ad esempio, se un altro client esegue la mutazione UpdateMovie sullo stesso ID a cui hai eseguito la sottoscrizione, riceverai un aggiornamento.

Indicatori di aggiornamento implicito delle query

Nell'esempio precedente, hai potuto abbonarti a una query e ricevere aggiornamenti in tempo reale senza ulteriori modifiche alle tue operazioni. In particolare, non è necessario specificare che la mutazione UpdateMovie può influire sul risultato della query GetMovieById.

Ciò è possibile perché la query GetMovieById riceve implicitamente un segnale di aggiornamento dalla mutazione UpdateMovie. I segnali di aggiornamento implicito vengono inviati tra un sottoinsieme delle query e delle mutazioni che potresti scrivere:

Se la tua query esegue una ricerca di una singola entità per chiave primaria, qualsiasi mutazione che scrive nella stessa entità, identificata anche dalla sua chiave primaria, attiverà implicitamente un segnale di aggiornamento.

  • _insert e _insertMany
  • _upsert e _upsertMany
  • _update
  • _delete

_deleteMany e _updateMany non inviano indicatori di aggiornamento.

Nell'esempio precedente, la query GetMovieById cerca un singolo film per ID (movie(id: $id)) e la mutazione UpdateMovie aggiorna un singolo film, specificato dall'ID (movie_update(id: $id, ...)), quindi la query può sfruttare l'aggiornamento implicito.

Le operazioni di inserimento e upsert possono attivare indicatori di aggiornamento impliciti quando utilizzi un valore noto, ad esempio l'UID di un utente Firebase Authentication.

Ad esempio, considera una query come la seguente:

query GetExtendedProfileByUser @auth(level: USER) {
  profile(key: { id_expr: "auth.uid" }) {
    id
    status
    photoUrl
    socialLink
  }
}

La query riceverebbe implicitamente un segnale di aggiornamento da una mutazione come la seguente:

mutation UpsertExtendedProfile($status: String, $photoUrl: String, $socialLink: String) @auth(level: USER) {
  profile_upsert(
    data: {
      id_expr: "auth.uid"
      status: $status
      photoUrl: $photoUrl
      socialLink: $socialLink
    }
  ) {
    id
    status
    photoUrl
    socialLink
  }
}

Quando le query o le mutazioni sono più complicate, devi specificare le condizioni che richiedono un aggiornamento della query. Continua alla sezione successiva per scoprire come fare.

Indicatori di aggiornamento esplicito delle query

Oltre ai segnali di aggiornamento inviati implicitamente dalle mutazioni alle query, puoi anche specificare in modo esplicito quando una query deve ricevere un segnale di aggiornamento. Per farlo, annota le query con la direttiva @refresh.

L'utilizzo della direttiva @refresh è obbligatorio ogni volta che le query non soddisfano i criteri specifici (vedi sopra) per l'aggiornamento automatico. Alcuni esempi di query che devono includere questa direttiva sono:

  • Query che recuperano elenchi di entità
  • Query che eseguono join su altre tabelle
  • Query di aggregazione
  • Query che utilizzano l'SQL nativo
  • Query che utilizzano resolver personalizzati

Puoi specificare una policy di aggiornamento in due modi:

Intervalli basati sul tempo

Aggiorna la query a un intervallo di tempo fisso.

Ad esempio, supponiamo che la tua base utenti molto attivi possa comportare l'aggiornamento molte volte al minuto della valutazione cumulativa di un film, in particolare dopo la sua uscita. Anziché aggiornare la query ogni volta che la valutazione cambia, potresti aggiornarla ogni pochi secondi per ricevere aggiornamenti che riflettono il risultato cumulativo di potenzialmente diverse mutazioni.

# dataconnect/connector/operations.gql

query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
  movie(id: $id) {
    id
    averageRating
  }
}

Esecuzione della mutazione

Aggiorna la query quando viene eseguita una mutazione specifica. Questo approccio rende esplicito quali mutazioni hanno il potenziale di modificare il risultato della query.

Ad esempio, supponiamo di avere una query che recupera informazioni su più film anziché su uno specifico. Questa query deve essere aggiornata ogni volta che una mutazione ha aggiornato uno dei record dei film.

query ListMovies($offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.")
    @refresh(onMutationExecuted: { operation: "UpdateMovie" }) {
  movies(limit: 10, offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}

Puoi anche specificare una condizione di espressione CEL che deve essere soddisfatta affinché la mutazione attivi un aggiornamento della query.

Ti consigliamo vivamente di farlo. Più precisa è la condizione specificata, meno risorse di database inutili verranno utilizzate e più reattiva sarà la tua app.

Ad esempio, supponiamo di avere una query che elenca i film solo di un genere specifico. Questa query deve essere aggiornata solo quando una mutazione aggiorna un film dello stesso genere:

query ListMoviesByGenre($genre: String, $offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list movies.")
    @refresh(onMutationExecuted: {
      operation: "UpdateMovie",
      condition: "request.variables.genre == mutation.variables.genre"
    }) {
  movies(
      where: { genre: { eq: $genre } },
      limit: 10,
      offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}

Binding CEL nelle condizioni @refresh

L'espressione condition in onMutationExecuted ha accesso a due contesti:

request

Lo stato della query a cui è stata eseguita la sottoscrizione.

Associazione Descrizione
request.variables Variabili passate alla query (ad esempio, request.variables.id)
request.auth.uid Firebase Authentication UID dell'utente che ha eseguito la query
request.auth.token Dizionario delle rivendicazioni dei token Firebase Authentication per l'utente che ha eseguito la query
mutation

Lo stato della mutazione eseguita.

Associazione Descrizione
mutation.variables Variabili passate alla mutazione (ad es. mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID dell'utente che ha eseguito la mutazione
mutation.auth.token Dizionario delle rivendicazioni del token Firebase Authentication per l'utente che ha eseguito la mutazione
Pattern comuni
# Refresh only when the mutation targets the same entity
"request.variables.id == mutation.variables.id"

# Refresh only when the same user who subscribed makes a change
"request.auth.uid == mutation.auth.uid"

# Refresh when a specific field value matches a condition
"request.auth.uid == mutation.auth.uid && mutation.variables.status == 'PUBLISHED'"

# Refresh when a specific flag is set in the mutation
"mutation.variables.isPublic == true"

Più direttive @refresh

Puoi specificare più volte la direttiva @refresh in una query per attivare un aggiornamento ogni volta che uno dei criteri specificati da una delle direttive @refresh viene soddisfatto.

Ad esempio, la seguente query verrà aggiornata ogni 30 secondi e ogni volta che viene eseguita una delle mutazioni specificate:

query ListMovies($offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.")
    @refresh(every: {seconds: 30})
    @refresh(onMutationExecuted: { operation: "UpdateMovie" })
    @refresh(onMutationExecuted: { operation: "BulkUpdateMovies" }) {
  movies(limit: 10, offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}

Riferimento

Per altri esempi, consulta il riferimento alla direttiva @refresh.