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