Kod klienta może subskrybować zapytania, aby otrzymywać aktualizacje w czasie rzeczywistym, gdy zmieni się wynik zapytania.
Zanim zaczniesz
Skonfiguruj generowanie pakietu SDK dla swojego projektu zgodnie z opisem w dokumentacji dotyczącej sieci, platform Apple i Fluttera.
- Musisz włączyć buforowanie po stronie klienta we wszystkich wygenerowanych pakietach SDK. Każda konfiguracja pakietu SDK musi zawierać deklarację podobną do tej:
clientCache: maxAge: 5s storage: ... # Optional.Klienci aplikacji muszą używać najnowszej wersji SQL Connect podstawowego pakietu SDK:
- Apple: pakiet Firebase SQL Connect SDK dla Swift w wersji 11.12.0 lub nowszej
- Sieć: pakiet JavaScript SDK w wersji 12.12.0 lub nowszej
- Flutter:
firebase_data_connectw wersji 0.3.0 lub nowszej
Wygeneruj ponownie pakiety SDK klienta za pomocą interfejsu wiersza poleceń Firebase w wersji 15.14.0 lub nowszej.
Subskrybowanie wyników zapytań
Możesz subskrybować zapytanie, aby reagować na zmiany w jego wynikach. Załóżmy na przykład, że w projekcie masz zdefiniowane te schematy i operacje:
# 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
})
}
Aby subskrybować zmiany w wyniku uruchomienia GetMovieById:
Sieć
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);
Sieć (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 obsługuje też buforowanie i subskrypcje w czasie rzeczywistym za pomocą
TanStack. Gdy w pliku
connector.yaml określisz react: true lub angular: true, SQL Connect wygeneruje
powiązania dla Reacta lub Angulara za pomocą TanStack.
Te powiązania mogą działać razem z SQL Connect's wbudowaną obsługą w czasie rzeczywistym, ale tylko z pewnymi trudnościami. Zalecamy używanie powiązań opartych na TanStack lub SQL Connect's wbudowanej obsługi w czasie rzeczywistym, ale nie obu tych rozwiązań naraz.
Pamiętaj, że własne wdrożenie w czasie rzeczywistym w SQL Connect's ma pewne zalety w porównaniu z powiązaniami TanStack:
- Znormalizowane buforowanie: SQL Connect implementuje znormalizowane buforowanie, które poprawia spójność danych oraz wydajność pamięci i sieci w porównaniu z buforowaniem na poziomie zapytań. Dzięki znormalizowanemu buforowaniu, jeśli encja zostanie zaktualizowana w jednym obszarze aplikacji, zostanie też zaktualizowana w innych obszarach, które jej używają.
- Zdalne unieważnianie: SQL Connect może zdalnie unieważnić buforowane encje na wszystkich subskrybowanych urządzeniach.
Jeśli nie chcesz używać TanStack, usuń ustawienia react: true i angular: true z pliku 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
Zaimportuj wygenerowany pakiet SDK projektu:
import 'package:flutter_app/dataconnect_generated/generated.dart';
Następnie wywołaj metodę subscribe() w odniesieniu do zapytania:
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);
}
});
Aby zatrzymać aktualizacje, możesz wywołać subscription.cancel().
Gdy zasubskrybujesz zapytanie, jak w poprzednim przykładzie, będziesz otrzymywać aktualizacje za każdym razem, gdy zmieni się wynik konkretnego zapytania. Jeśli na przykład inny klient wykona mutację UpdateMovie na tym samym identyfikatorze, który subskrybujesz, otrzymasz aktualizację.
Sygnały niejawnego odświeżania zapytań
W poprzednim przykładzie udało Ci się zasubskrybować zapytanie i otrzymywać aktualizacje w czasie rzeczywistym bez wprowadzania dodatkowych modyfikacji w operacjach. W szczególności nie trzeba było określać, że mutacja UpdateMovie może wpływać na wynik zapytania GetMovieById.
Jest to możliwe, ponieważ zapytanie GetMovieById niejawnie otrzymuje sygnał odświeżenia z mutacji UpdateMovie. Niejawne sygnały odświeżenia są wysyłane między podzbiorem zapytań i mutacji, które możesz napisać:
Jeśli zapytanie wykonuje wyszukiwanie pojedynczej encji według klucza podstawowego, każda mutacja, która zapisuje tę samą encję, również identyfikowaną przez jej klucz podstawowy, niejawnie wywoła sygnał odświeżenia.
_inserti_insertMany_upserti_upsertMany_update_delete
_deleteMany i _updateMany nie wysyłają sygnałów odświeżenia.
W poprzednim przykładzie zapytanie GetMovieById wyszukuje pojedynczy film według identyfikatora (movie(id: $id)), a mutacja UpdateMovie aktualizuje pojedynczy film określony przez identyfikator (movie_update(id: $id, ...)), więc zapytanie może korzystać z niejawnego odświeżania.
Operacje wstawiania i aktualizowania mogą wywoływać niejawne sygnały odświeżenia, gdy używasz znanego klucza, np. identyfikatora UID użytkownika Firebase Authentication.
Rozważmy na przykład takie zapytanie:
query GetExtendedProfileByUser @auth(level: USER) {
profile(key: { id_expr: "auth.uid" }) {
id
status
photoUrl
socialLink
}
}
Zapytanie niejawnie otrzyma sygnał odświeżenia z mutacji takiej jak ta:
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
}
}
Gdy zapytania lub mutacje są bardziej skomplikowane, musisz określić warunki, które wymagają odświeżenia zapytania. Aby dowiedzieć się, jak to zrobić, przejdź do następnej sekcji.
Jawne sygnały odświeżania zapytań
Oprócz sygnałów odświeżenia, które są niejawnie wysyłane przez mutacje do zapytań, możesz też jawnie określić, kiedy zapytanie ma otrzymywać sygnał odświeżenia. Aby to zrobić, dodaj do zapytań adnotację z dyrektywą @refresh.
Użycie dyrektywy @refresh jest wymagane, gdy zapytania nie spełniają określonych kryteriów (patrz wyżej) automatycznego odświeżania. Oto kilka przykładów zapytań, które muszą zawierać tę dyrektywę:
- Zapytania, które pobierają listy encji
- Zapytania, które wykonują łączenia z innymi tabelami
- Zapytania agregujące
- Zapytania używające natywnego SQL
- Zapytania używające niestandardowych resolverów
Zasady odświeżania możesz określić na 2 sposoby:
Przedziały czasowe
Odśwież zapytanie w stałych odstępach czasu.
Załóżmy na przykład, że Twoja bardzo aktywna baza użytkowników może powodować, że łączna ocena filmu będzie aktualizowana wiele razy na minutę, zwłaszcza po premierze. Zamiast odświeżać zapytanie za każdym razem, gdy zmieni się ocena, możesz odświeżać je co kilka sekund, aby otrzymywać aktualizacje odzwierciedlające łączny wynik potencjalnie kilku mutacji.
# dataconnect/connector/operations.gql
query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
movie(id: $id) {
id
averageRating
}
}
Wykonywanie mutacji
Odśwież zapytanie, gdy zostanie wykonana określona mutacja. To podejście jasno określa, które mutacje mogą zmienić wynik zapytania.
Załóżmy na przykład, że masz zapytanie, które pobiera informacje o wielu filmach, a nie o konkretnym. To zapytanie powinno się odświeżać za każdym razem, gdy mutacja zaktualizuje którykolwiek z rekordów filmu.
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
}
}
Możesz też określić warunek wyrażenia CEL, który musi zostać spełniony, aby mutacja wywołała odświeżenie zapytania.
Jest to wysoce zalecane. Im dokładniej określisz warunek, tym mniej niepotrzebnych zasobów bazy danych zostanie zużytych, a aplikacja będzie bardziej responsywna.
Załóżmy na przykład, że masz zapytanie, które wyświetla filmy tylko w określonym gatunku. To zapytanie powinno się odświeżać tylko wtedy, gdy mutacja zaktualizuje film w tym samym gatunku:
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
}
}
Powiązania CEL w warunkach @refresh
Wyrażenie condition w onMutationExecuted ma dostęp do 2 kontekstów:
request
Stan subskrybowanego zapytania.
| Powiązanie | Opis |
|---|---|
request.variables |
Zmienne przekazane do zapytania (np. request.variables.id) |
request.auth.uid |
Firebase Authentication Identyfikator UID użytkownika, który wykonał zapytanie |
request.auth.token |
Słownik roszczeń tokena Firebase Authentication użytkownika, który wykonał zapytanie |
mutation
Stan wykonanej mutacji.
| Powiązanie | Opis |
|---|---|
mutation.variables |
Zmienne przekazane do mutacji (np. mutation.variables.movieId) |
mutation.auth.uid |
Firebase Authentication Identyfikator UID użytkownika, który wykonał mutację |
mutation.auth.token |
Słownik roszczeń tokena Firebase Authentication użytkownika, który wykonał mutację |
Typowe wzorce
# 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"
Wiele dyrektyw @refresh
Możesz określić dyrektywę @refresh w zapytaniu kilka razy, aby wywołać odświeżenie, gdy zostanie spełnione którekolwiek z kryteriów określonych przez jedną z dyrektyw @refresh.
Na przykład to zapytanie będzie się odświeżać co 30 sekund oraz za każdym razem, gdy zostanie wykonana jedna z określonych mutacji:
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
}
}
Źródła wiedzy
Więcej przykładów znajdziesz w dokumentacji dyrektywy @refresh.