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_connectversione 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.
_inserte_insertMany_upserte_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.