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 projektu zgodnie z opisem w dokumentacji dotyczącej internetu, platform AppleFluttera.

    • 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ą korzystać z aktualnej wersji SQL Connectgłównego pakietu SDK:

    • Apple: Firebase SQL Connect pakiet SDK dla Swift w wersji 11.12.0 lub nowszej
    • Internet: pakiet JavaScript SDK w wersji 12.12.0 lub nowszej
    • Flutter: firebase_data_connect wersja 0.3.0 lub nowsza
  • Wygeneruj ponownie pakiety SDK klienta, używając wiersza poleceń Firebase w wersji 15.14.0 lub nowszej.

Subskrybowanie wyników zapytania

Możesz zasubskrybować 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 zasubskrybować zmiany w wyniku działania funkcji 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);

Internet (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ż zapisywanie w pamięci podręcznej i subskrypcje w czasie rzeczywistym za pomocą TanStack. Jeśli 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 wbudowaną obsługą w czasie rzeczywistym w SQL Connect, ale tylko z pewnymi trudnościami. Zalecamy używanie albo powiązań opartych na TanStack, albo wbudowanej obsługi czasu rzeczywistego w SQL Connect, ale nie obu tych rozwiązań naraz.

Pamiętaj, że własna implementacja SQL Connect w czasie rzeczywistym 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 element zostanie zaktualizowany w jednym obszarze aplikacji, zostanie też zaktualizowany w innych obszarach, które go używają.
  • Zdalne unieważnianie: SQL Connect może zdalnie unieważniać zapisane w pamięci podręcznej jednostki na wszystkich subskrybowanych urządzeniach.

Jeśli nie chcesz korzystać z TanStack, usuń ustawienia react: trueangular: true z pliku 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

Zaimportuj wygenerowany pakiet SDK projektu:

import 'package:flutter_app/dataconnect_generated/generated.dart';

Następnie wywołaj metodę subscribe() na odwołaniu 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 zadzwonić pod numer subscription.cancel().

Gdy zasubskrybujesz zapytanie, jak w poprzednim przykładzie, będziesz otrzymywać aktualizacje za każdym razem, gdy zmieni się wynik tego zapytania. Jeśli na przykład inny klient wykona mutację UpdateMovie na tym samym identyfikatorze, do którego subskrybujesz, otrzymasz aktualizację.

Sygnały odświeżania zapytań niejawnych

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 musisz 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żania z mutacji UpdateMovie. Sygnały niejawnego odświeżania są wysyłane między podzbiorem zapytań i mutacji, które możesz napisać:

Jeśli zapytanie wykonuje wyszukiwanie pojedynczego elementu według klucza podstawowego, każda mutacja, która zapisuje dane w tym samym elemencie również identyfikowanym przez klucz podstawowy, będzie niejawnie wywoływać sygnał odświeżania.

  • _insert i _insertMany
  • _upsert i _upsertMany
  • _update
  • _delete

_deleteMany_updateMany nie wysyłają sygnałów odświeżania.

W poprzednim przykładzie zapytanie GetMovieById wyszukuje jeden film według identyfikatora (movie(id: $id)), a mutacja UpdateMovie aktualizuje jeden film określony przez identyfikator (movie_update(id: $id, ...)), więc zapytanie może korzystać z niejawnego odświeżania.

Operacje wstawiania i wstawiania lub aktualizowania mogą wywoływać sygnały niejawnego odświeżania, gdy używasz znanego klucza, np. Firebase Authentication identyfikatora użytkownika.

Rozważmy na przykład takie zapytanie:

query GetExtendedProfileByUser @auth(level: USER) {
  profile(key: { id_expr: "auth.uid" }) {
    id
    status
    photoUrl
    socialLink
  }
}

Zapytanie otrzymałoby niejawny sygnał odświeżania 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
  }
}

Jeśli Twoje zapytania lub mutacje są bardziej skomplikowane, musisz określić warunki, które wymagają odświeżenia zapytania. W następnej sekcji dowiesz się, jak to zrobić.

Sygnały jawnego odświeżania zapytań

Oprócz sygnałów odświeżania, które są niejawnie wysyłane przez mutacje do zapytań, możesz też jawnie określić, kiedy zapytanie powinno otrzymać sygnał odświeżania. W tym celu dodaj do zapytań dyrektywę @refresh.

Użycie dyrektywy @refresh jest wymagane, gdy zapytania nie spełniają określonych kryteriów (patrz wyżej) automatycznego odświeżania. Przykłady zapytań, które muszą zawierać tę dyrektywę:

  • Zapytania, które pobierają listy encji
  • Zapytania, które wykonują złączenia z innymi tabelami
  • Zapytania agregujące
  • Zapytania z użyciem natywnego SQL
  • Zapytania korzystające z niestandardowych resolverów

Zasady odświeżania możesz określić na 2 sposoby:

Przedziały czasowe

Odświeżanie zapytania w określonych odstępach czasu.

Załóżmy na przykład, że bardzo aktywna baza użytkowników może powodować, że łączna ocena filmu jest aktualizowana wiele razy na minutę, zwłaszcza po premierze. Zamiast odświeżać zapytanie za każdym razem, gdy zmienia się ocena, możesz odświeżać je co kilka sekund, aby otrzymywać aktualizacje odzwierciedlające skumulowany wynik potencjalnie kilku zmian.

# dataconnect/connector/operations.gql

query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
  movie(id: $id) {
    id
    averageRating
  }
}

Wykonywanie mutacji

Odświeżanie zapytania po wykonaniu określonej mutacji. Dzięki temu wyraźnie widać, 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 jednym 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 być spełniony, aby mutacja spowodowała odświeżenie zapytania.

Zdecydowanie zalecamy wykonanie tej czynności. Im dokładniej określisz warunek, tym mniej niepotrzebnych zasobów bazy danych zostanie wykorzystanych i tym szybciej będzie działać aplikacja.

Załóżmy na przykład, że masz zapytanie, które wyświetla tylko filmy z określonego gatunku. To zapytanie powinno być odświeżane tylko wtedy, gdy mutacja zaktualizuje film z tego samego 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 conditiononMutationExecuted ma dostęp do 2 kontekstów:

request

Stan zapytania, do którego subskrybujesz.

Powiązanie Opis
request.variables Zmienne przekazywane do zapytania (np. request.variables.id)
request.auth.uid Firebase Authentication Identyfikator UID użytkownika, który wykonał zapytanie.
request.auth.token Słownik Firebase Authentication roszczeń dotyczących tokena użytkownika, który wykonał zapytanie.
mutation

Stan wykonanej mutacji.

Powiązanie Opis
mutation.variables Zmienne przekazywane do mutacji (np. mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID użytkownika, który wykonał mutację
mutation.auth.token Słownik Firebase Authentication roszczeń tokena 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

Dyrektywę @refresh możesz określić w zapytaniu wiele razy, aby odświeżanie było wywoływane, gdy zostanie spełnione którekolwiek z kryteriów określonych przez jedną z dyrektyw @refresh.

Na przykład to zapytanie będzie odświeżane co 30 sekund, a także 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 @refreshdokumentacji dyrektywy.