Otrzymywanie aktualizacji w czasie rzeczywistym z SQL Connect

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_connect w 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.

  • _insert i _insertMany
  • _upsert i _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.