Obtén actualizaciones en tiempo real de SQL Connect

Tu código de cliente puede suscribirse a consultas para obtener actualizaciones en tiempo real cuando cambie el resultado de la consulta.

Antes de comenzar

  • Configura la generación del SDK para tu proyecto como se describe en la documentación para la Web, las plataformas de Apple y Flutter.

    • Debes habilitar el almacenamiento en caché del cliente para todos los SDK generados. En particular, cada configuración del SDK debe contener una declaración como la siguiente:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • Los clientes de tu app deben usar una versión reciente del SQL Connect SDK principal:

    • Apple: SDK Firebase SQL Connect para Swift versión 11.12.0 o posterior
    • Web: SDK de JavaScript versión 12.12.0 o posterior
    • Flutter: firebase_data_connect versión 0.3.0 o posterior
  • Vuelve a generar los SDK de cliente con la versión 15.14.0 de Firebase CLI o una posterior.

Cómo suscribirse a los resultados de la consulta

Puedes suscribirte a una consulta para responder a los cambios en el resultado de la consulta. Por ejemplo, supongamos que tienes el siguiente esquema y las siguientes operaciones definidas en tu proyecto:

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

Para suscribirte a los cambios en el resultado de la ejecución de GetMovieById, haz lo siguiente:

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 también admite el almacenamiento en caché y las suscripciones en tiempo real con TanStack. Cuando especificas react: true o angular: true en tu connector.yaml archivo, SQL Connect genera vinculaciones para React o Angular con TanStack.

Estas vinculaciones pueden funcionar junto con la compatibilidad integrada en tiempo real de SQL Connect, pero solo con cierta dificultad. Te recomendamos que uses las vinculaciones basadas en TanStack o SQL Connect's la compatibilidad integrada en tiempo real, pero no ambas.

Ten en cuenta que la implementación en tiempo real de SQL Connect's tiene algunas ventajas sobre las vinculaciones de TanStack:

  • Almacenamiento en caché normalizado: SQL Connect implementa el almacenamiento en caché normalizado, lo que mejora la coherencia de los datos, así como la eficiencia de la memoria y la red en comparación con el almacenamiento en caché a nivel de la consulta. Con el almacenamiento en caché normalizado, si una entidad se actualiza en un área de tu app, también se actualizará en otras áreas que usen esa entidad.
  • Invalidación remota: SQL Connect puede invalidar de forma remota las entidades almacenadas en caché en todos los dispositivos suscritos.

Si decides no usar TanStack, debes quitar la configuración react: true y angular: true de tu archivo 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 el SDK generado de tu proyecto:

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

Luego, llama al método subscribe() en una referencia de consulta:

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

Para detener las actualizaciones, puedes llamar a subscription.cancel().

Una vez que te suscribas a la consulta como en el ejemplo anterior, recibirás actualizaciones cada vez que cambie el resultado de la consulta específica. Por ejemplo, si otro cliente ejecuta la mutación UpdateMovie en el mismo ID al que te suscribiste, recibirás una actualización.

Indicadores de actualización de consultas implícitos

En el ejemplo anterior, pudiste suscribirte a una consulta y obtener actualizaciones en tiempo real sin ninguna modificación adicional en tus operaciones. En particular, no necesitaste especificar que la mutación UpdateMovie puede afectar el resultado de la consulta GetMovieById.

Esto es posible porque la consulta GetMovieById obtiene implícitamente un indicador de actualización de la mutación UpdateMovie. Los indicadores de actualización implícitos se envían entre un subconjunto de las consultas y mutaciones que podrías escribir:

Si tu consulta realiza una búsqueda de una sola entidad por clave principal, cualquier mutación que escriba en la misma entidad, también identificada por su clave principal activará implícitamente un indicador de actualización.

  • _insert y _insertMany
  • _upsert y _upsertMany
  • _update
  • _delete

_deleteMany y _updateMany no envían indicadores de actualización.

En el ejemplo anterior, la consulta GetMovieById busca una sola película por ID (movie(id: $id)) y la mutación UpdateMovie actualiza una sola película, especificada por ID (movie_update(id: $id, ...)), por lo que la consulta puede aprovechar la actualización implícita.

Las operaciones de inserción y upsert pueden activar indicadores de actualización implícitos cuando usas un valor conocido, como el UID de un usuario Firebase Authentication.

Por ejemplo, considera una consulta como la siguiente:

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

La consulta recibiría implícitamente un indicador de actualización de una mutación como la siguiente:

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

Cuando tus consultas o mutaciones sean más complicadas, deberás especificar las condiciones que requieren una actualización de la consulta. Continúa con la siguiente sección para obtener más información.

Indicadores de actualización de consultas explícitos

Además de los indicadores de actualización que las mutaciones envían implícitamente a las consultas, también puedes especificar de forma explícita cuándo una consulta debe recibir un indicador de actualización. Para ello, anota tus consultas con la directiva @refresh.

Se requiere el uso de la directiva @refresh siempre que tus consultas no cumplan con los criterios específicos (consulta más arriba) para la actualización automática. Algunos ejemplos de consultas que deben incluir esta directiva son los siguientes:

  • Consultas que recuperan listas de entidades
  • Consultas que realizan uniones en otras tablas
  • Consultas de agregación
  • Consultas que usan SQL nativo
  • Consultas que usan resolutores personalizados

Puedes especificar una política de actualización de dos maneras:

Intervalos basados en el tiempo

Actualiza la consulta en un intervalo fijo.

Por ejemplo, supongamos que tu base de usuarios muy activa puede hacer que la calificación acumulativa de una película se actualice muchas veces por minuto, en particular después del lanzamiento de una película. En lugar de actualizar la consulta cada vez que cambia la calificación, puedes actualizarla cada pocos segundos para obtener actualizaciones que reflejen el resultado acumulativo de varias mutaciones.

# dataconnect/connector/operations.gql

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

Ejecución de mutaciones

Actualiza la consulta cuando se ejecuta una mutación específica. Este enfoque hace explícito qué mutaciones tienen el potencial de cambiar el resultado de la consulta.

Por ejemplo, supongamos que tienes una consulta que recupera información sobre varias películas en lugar de una específica. Esta consulta debe actualizarse cada vez que una mutación actualice cualquiera de los registros de películas.

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

También puedes especificar una condición de expresión CEL que se debe cumplir para que la mutación active una actualización de la consulta.

Se recomienda hacerlo. Cuanto más preciso puedas ser cuando especifiques la condición, menos recursos innecesarios de la base de datos se consumirán y más sensible será tu app.

Por ejemplo, supongamos que tienes una consulta que muestra películas solo en un género especificado. Esta consulta solo debe actualizarse cuando una mutación actualiza una película del mismo género:

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

Vinculaciones de CEL en condiciones @refresh

La expresión condition en onMutationExecuted tiene acceso a dos contextos:

request

Es el estado de la consulta a la que se suscribe.

Vinculación Descripción
request.variables Variables que se pasan a la consulta (por ejemplo, request.variables.id)
request.auth.uid Firebase Authentication UID del usuario que ejecutó la consulta
request.auth.token Diccionario de Firebase Authentication declaraciones de token para el usuario que ejecutó la consulta
mutation

Es el estado de la mutación que se ejecutó.

Vinculación Descripción
mutation.variables Variables que se pasan a la mutación (p.ej., mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID de Firebase Authentication del usuario que ejecutó la mutación
mutation.auth.token Diccionario de declaraciones de token Firebase Authentication para el usuario que ejecutó la mutación
Patrones comunes
# 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"

Varias directivas @refresh

Puedes especificar la directiva @refresh varias veces en una consulta para activar una actualización cada vez que se cumpla alguno de los criterios especificados por una de las directivas @refresh.

Por ejemplo, la siguiente consulta se actualizará cada 30 segundos y cada vez que se ejecute una de las mutaciones especificadas:

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

Reference

Consulta la referencia de la directiva @refresh para obtener más ejemplos.