Echtzeitaktualisierungen von SQL Connect erhalten

Ihr Clientcode kann Abfragen abonnieren, um Aktualisierungen in Echtzeit zu erhalten, wenn sich das Ergebnis der Abfrage ändert.

Hinweis

  • Richten Sie die SDK-Generierung für Ihr Projekt ein, wie in der Dokumentation für Apple-Plattformen, Android, Web und Flutter beschrieben.

    • Sie müssen das clientseitige Caching für alle generierten SDKs aktivieren. Jede SDK-Konfiguration muss eine Deklaration wie die folgende enthalten:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • Ihre App-Clients müssen eine aktuelle Version des SQL Connect-Core-SDK verwenden:

    • Apple: Firebase SQL Connect SDK für Swift-Version 11.12.0 oder höher
    • Android: Firebase SQL Connect SDK-Version 17.3.0 oder höher (BoM-Version 34.14.0 oder höher)
    • Web: JavaScript SDK-Version 12.12.0 oder höher
    • Flutter: firebase_data_connect-Version 0.3.0 oder höher
  • Generieren Sie Ihre Client-SDKs mit Version 15.14.0 oder höher der Firebase CLI neu.

Abfrageergebnisse abonnieren

Sie können eine Abfrage abonnieren, um auf Änderungen im Abfrageergebnis zu reagieren. Angenommen, Sie haben das folgende Schema und die folgenden Vorgänge in Ihrem Projekt definiert:

# 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
    })
}

So abonnieren Sie Änderungen am Ergebnis der Ausführung von 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 unterstützt auch Caching und Echtzeitabos mit TanStack. Wenn Sie react: true oder angular: true in Ihrer connector.yaml-Datei angeben, generiert SQL Connect Bindungen für React oder Angular mit TanStack.

Diese Bindungen können zusammen mit der integrierten Echtzeitunterstützung von SQL Connect verwendet werden, allerdings nur mit einigem Aufwand. Wir empfehlen, entweder die TanStack-basierten Bindungen oder die integrierte Echtzeitunterstützung von SQL Connect zu verwenden, aber nicht beides.

Die Echtzeitimplementierung von SQL Connect bietet einige Vorteile gegenüber den TanStack-Bindungen:

  • Normalisiertes Caching: SQL Connect implementiert normalisiertes Caching, wodurch die Datenkonsistenz sowie die Speicher- und Netzwerkeffizienz im Vergleich zum Caching auf Abfrageebene verbessert werden. Wenn ein Element mit normalisiertem Caching in einem Bereich Ihrer App aktualisiert wird, wird es auch in anderen Bereichen aktualisiert, in denen dieses Element verwendet wird.
  • Ungültigmachung per Fernzugriff: SQL Connect kann zwischengespeicherte Entitäten auf allen abonnierten Geräten per Fernzugriff ungültig machen.

Wenn Sie TanStack nicht verwenden möchten, sollten Sie die Einstellungen react: true und angular: true aus Ihrer connector.yaml-Datei entfernen.

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()
        }
    }
}

Android

class ExampleViewModel(
  private val movieId: UUID
) : ViewModel() {
  private val _uiState = MutableStateFlow<GetMovieByIdQuery.Data.Movie?>(null)
  val uiState = _uiState.asStateFlow()

  // Subscribe to the query.
  private val movieInfoSub = ExampleConnector.instance
      .getMovieById.ref(GetMovieByIdQuery.Variables(id = movieId))
      .subscribe()

  init {
    viewModelScope.launch {
      movieInfoSub.flow.collect {
        // As query results are collected, update the UI state.
        val result = it.result.getOrElse { return@collect }
        _uiState.update{ result.data.movie }
      }
    }
  }

  companion object {
    fun provideFactory(movieId: UUID): ViewModelProvider.Factory =
      viewModelFactory {
        initializer {
          ExampleViewModel(movieId = movieId)
        }
      }
  }
}

Flutter

Importieren Sie das generierte SDK Ihres Projekts:

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

Rufen Sie dann die Methode subscribe() für eine Abfragereferenz auf:

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);
  }
});

Wenn Sie keine Updates mehr erhalten möchten, können Sie subscription.cancel() anrufen.

Wenn Sie die Abfrage wie im vorherigen Beispiel abonnieren, erhalten Sie jedes Mal Updates, wenn sich das Ergebnis der jeweiligen Abfrage ändert. Wenn beispielsweise ein anderer Client die Mutation UpdateMovie für dieselbe ID ausführt, für die Sie ein Abo abgeschlossen haben, erhalten Sie eine Aktualisierung.

Implizite Signale zur Aktualisierung von Abfragen

Im vorherigen Beispiel konnten Sie eine Abfrage abonnieren und Echtzeitupdates erhalten, ohne dass zusätzliche Änderungen an Ihren Abläufen erforderlich waren. Insbesondere mussten Sie nicht angeben, dass sich die UpdateMovie-Mutation auf das Ergebnis der GetMovieById-Abfrage auswirken kann.

Das ist möglich, weil die GetMovieById-Abfrage implizit ein Aktualisierungssignal von der UpdateMovie-Mutation erhält. Implizite Aktualisierungssignale werden zwischen einer Teilmenge der Abfragen und Mutationen gesendet, die Sie möglicherweise schreiben:

Wenn mit Ihrer Abfrage ein Lookup für eine einzelne Entität nach Primärschlüssel ausgeführt wird, löst jede Mutation, mit der in dieselbe Entität geschrieben wird, die ebenfalls durch ihren Primärschlüssel identifiziert wird, implizit ein Aktualisierungssignal aus.

  • _insert und _insertMany
  • _upsert und _upsertMany
  • _update
  • _delete

_deleteMany und _updateMany senden keine Aktualisierungssignale.

Im vorherigen Beispiel ruft die GetMovieById-Abfrage einen einzelnen Film anhand der ID (movie(id: $id)) auf und die UpdateMovie-Mutation aktualisiert einen einzelnen Film, der durch die ID (movie_update(id: $id, ...)) angegeben wird. Daher kann die Abfrage das implizite Aktualisieren nutzen.

Einfüge- und Upsert-Vorgänge können implizite Aktualisierungssignale auslösen, wenn Sie einen bekannten Wert wie die UID eines Firebase Authentication-Nutzers verwenden.

Betrachten Sie beispielsweise die folgende Abfrage:

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

Die Abfrage würde implizit ein Aktualisierungssignal von einer Mutation wie der folgenden erhalten:

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
  }
}

Wenn Ihre Abfragen oder Mutationen komplexer sind, müssen Sie die Bedingungen angeben, die eine Aktualisierung der Abfrage erfordern. Im nächsten Abschnitt erfahren Sie, wie das geht.

Explizite Signale zur Aktualisierung von Anfragen

Zusätzlich zu den Aktualisierungssignalen, die implizit durch Mutationen an Abfragen gesendet werden, können Sie auch explizit angeben, wann eine Abfrage ein Aktualisierungssignal erhalten soll. Dazu annotieren Sie Ihre Anfragen mit der @refresh-Anweisung.

Die Verwendung der Direktive @refresh ist immer dann erforderlich, wenn Ihre Abfragen die oben genannten spezifischen Kriterien für die automatische Aktualisierung nicht erfüllen. Beispiele für Anfragen, die diese Anweisung enthalten müssen:

  • Abfragen, mit denen Listen von Entitäten abgerufen werden
  • Abfragen, die Joins für andere Tabellen ausführen
  • Aggregationsabfragen
  • Abfragen mit nativem SQL
  • Abfragen mit benutzerdefinierten Resolvern

Sie haben zwei Möglichkeiten, eine Aktualisierungsrichtlinie anzugeben:

Zeitbasierte Intervalle

Aktualisieren Sie die Abfrage in einem festen Zeitintervall.

Angenommen, Ihre sehr aktiven Nutzer führen dazu, dass die kumulative Bewertung eines Films mehrmals pro Minute aktualisiert wird, insbesondere nach der Veröffentlichung des Films. Anstatt die Abfrage jedes Mal zu aktualisieren, wenn sich die Bewertung ändert, können Sie sie stattdessen alle paar Sekunden aktualisieren, um Updates zu erhalten, die das kumulative Ergebnis von möglicherweise mehreren Mutationen widerspiegeln.

# dataconnect/connector/operations.gql

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

Mutation ausführen

Aktualisieren Sie die Abfrage, wenn eine bestimmte Mutation ausgeführt wird. Bei diesem Ansatz wird explizit angegeben, welche Mutationen das Ergebnis der Abfrage ändern können.

Angenommen, Sie haben eine Abfrage, mit der Informationen zu mehreren Filmen anstelle eines bestimmten Films abgerufen werden. Diese Abfrage sollte immer dann aktualisiert werden, wenn durch eine Mutation einer der Filmdatensätze aktualisiert wurde.

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
  }
}

Sie können auch eine CEL-Ausdrucksbedingung angeben, die erfüllt sein muss, damit durch die Mutation eine Aktualisierung der Abfrage ausgelöst wird.

Das wird dringend empfohlen. Je genauer Sie die Bedingung angeben, desto weniger unnötige Datenbankressourcen werden verbraucht und desto reaktionsschneller ist Ihre App.

Angenommen, Sie haben eine Abfrage, in der nur Filme eines bestimmten Genres aufgeführt sind. Diese Abfrage sollte nur aktualisiert werden, wenn durch eine Mutation ein Film im selben Genre aktualisiert wird:

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
  }
}

CEL-Bindungen in @refresh-Bedingungen

Der Ausdruck condition in onMutationExecuted hat Zugriff auf zwei Kontexte:

request

Der Status der abonnierten Abfrage.

Bindung Beschreibung
request.variables Variablen, die an die Abfrage übergeben werden (z. B. request.variables.id)
request.auth.uid Firebase Authentication UID des Nutzers, der die Abfrage ausgeführt hat
request.auth.token Dictionary mit Firebase Authentication-Token-Claims für den Nutzer, der die Abfrage ausgeführt hat
mutation

Der Status der ausgeführten Mutation.

Bindung Beschreibung
mutation.variables Variablen, die an die Mutation übergeben werden (z.B. mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID des Nutzers, der die Mutation ausgeführt hat
mutation.auth.token Dictionary mit Firebase Authentication-Token-Ansprüchen für den Nutzer, der die Mutation ausgeführt hat
Häufige Muster
# 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"

Mehrere @refresh-Anweisungen

Sie können die @refresh-Anweisung mehrmals in einer Abfrage angeben, um eine Aktualisierung auszulösen, wenn eines der Kriterien erfüllt ist, die durch eine der @refresh-Anweisungen angegeben werden.

Die folgende Abfrage wird beispielsweise alle 30 Sekunden und immer dann aktualisiert, wenn eine der angegebenen Mutationen ausgeführt wird:

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
  }
}

Referenz

Weitere Beispiele finden Sie in der Referenz zur @refresh-Anweisung.