Ricevi aggiornamenti in tempo reale da SQL Connect

Il codice client può sottoscrivere le 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. In particolare, ogni configurazione dell'SDK deve contenere una dichiarazione simile alla seguente:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • I client delle app devono utilizzare una versione recente dell' SQL Connect SDK principale:

    • Apple: Firebase SQL Connect SDK per Swift versione 11.12.0 o successive
    • Web: JavaScript SDK 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 dell'interfaccia a riga di comando di Firebase o successive.

Sottoscrizione ai risultati delle query

Puoi sottoscrivere una query per rispondere alle modifiche del 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 sottoscrivere le modifiche al 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 associazioni per React o Angular utilizzando TanStack.

Queste associazioni possono funzionare insieme al supporto in tempo reale integrato di SQL Connect, ma solo con alcune difficoltà. Ti consigliamo di utilizzare le associazioni basate su TanStack o SQL Connect's il supporto in tempo reale integrato, ma non entrambi.

Tieni presente che l'implementazione in tempo reale di SQL Connect's presenta alcuni vantaggi rispetto alle associazioni 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 sottoscritti.

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

iOS

struct MovieDetailsView: View {
    // QueryRef has the @Observable annotation, so its properties will
    // automatically trigger updates on changes.
    // Realtime subscriptions will keep the query results updated with changes.

    // Define the ref variable.
    // If parameters are known before hand, refs can be initialized here directly
    // else they can be initialized in the init for the view, like here
    @State private var queryRef: GetMovieByIdQuery.Ref

    // Store the handle to unsubscribe from query updates.
    // QueryRef can be used in multiple views.
    // Each view can separately subscribe / unsubscribe to updates
    // When there are no more subscribers to a QueryRef,
    // it will cancel automatic updates for that QueryRef.
    @State private var querySub: AnyCancellable?

    init(movieId: String) {
      // initialize the ref with the movieId
      queryRef = DataConnect.moviesConnector.getMovieByIdQuery.ref(movieId: movieId)
    }

    var body: some View {
        VStack {
            // Use the query results in a View.
            if let movie = queryRef.data?.movie {
              Text(movie.title)
              Text(mpvie.description)
              // other details
            } else {
              // if last fetch/update resulted in an error
              if error = queryRef.lastError {
                Text("Error loading movie")
              } else {
                Text("Loading movie ...")
              }
            }
        }
        .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 {
          // Calling cancel will unsubscribe from receiving updates.
          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 subscription.cancel().

Una volta sottoscritta la 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 sottoscritto, riceverai un aggiornamento.

Segnali di aggiornamento impliciti delle query

Nell'esempio precedente, hai potuto sottoscrivere una query e ricevere aggiornamenti in tempo reale senza ulteriori modifiche alle operazioni. In particolare, non è stato 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 impliciti vengono inviati tra un sottoinsieme delle query e delle mutazioni che potresti scrivere:

Se la query esegue una singola ricerca di 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 segnali 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 segnali di aggiornamento impliciti quando si utilizza 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ù complesse, dovrai specificare le condizioni che richiedono un aggiornamento della query. Continua alla sezione successiva per scoprire come.

Segnali di aggiornamento espliciti 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 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 di utenti molto attivi possa comportare l'aggiornamento della valutazione cumulativa di un film molte volte al minuto, in particolare dopo l'uscita di un film. 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 per 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.

È vivamente consigliato. Più precisa è la condizione, meno risorse di database non necessarie verranno consumate e più reattiva sarà l'app.

Ad esempio, supponiamo di avere una query che elenca i film solo in un genere specificato. 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
  }
}

Associazioni CEL nelle condizioni @refresh

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

request

Lo stato della query a cui è sottoscritta.

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 del 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 di autenticazione Firebase 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 la direttiva @refresh più volte in una query per attivare un aggiornamento ogni volta che viene soddisfatto uno dei criteri specificati da una delle direttive @refresh.

Ad esempio, la query seguente 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.