Receber atualizações em tempo real do SQL Connect

O código do cliente pode se inscrever em consultas para receber atualizações em tempo real quando o resultado da consulta mudar.

Antes de começar

  • Configure a geração do SDK para seu projeto, conforme descrito na documentação para plataformas Apple, Android, Web e Flutter.

    • É necessário ativar o armazenamento em cache do lado do cliente para todos os SDKs gerados. Especificamente, cada configuração de SDK precisa conter uma declaração como a seguinte:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • Os clientes do app precisam usar uma versão recente do SQL Connect SDK principal:

    • Apple: Firebase SQL Connect SDK para Swift versão 11.12.0 ou mais recente
    • Android: Firebase SQL Connect versão do SDK 17.3.0 ou mais recente (BoM versão 34.14.0 ou mais recente)
    • Web: SDK para JavaScript versão 12.12.0 ou mais recente
    • Flutter: firebase_data_connect versão 0.3.0 ou mais recente
  • Regenere os SDKs do cliente usando a versão 15.14.0 da CLI do Firebase ou mais recente.

Como se inscrever nos resultados da consulta

É possível se inscrever em uma consulta para responder a mudanças no resultado dela. Por exemplo, suponha que você tenha o esquema e as operações a seguir definidos no seu projeto:

# 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 se inscrever nas mudanças no resultado da execução de 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 também oferece suporte a assinaturas em tempo real e armazenamento em cache usando TanStack. Quando você especifica react: true ou angular: true no arquivo connector.yaml, o SQL Connect gera vinculações para React ou Angular usando o TanStack.

Essas vinculações podem funcionar com o SQL Connect's suporte integrado em tempo real, mas apenas com alguma dificuldade. Recomendamos que você use as vinculações baseadas no TanStack ou SQL Connect's suporte integrado em tempo real, mas não ambos.

A implementação em tempo real do SQL Connect's tem algumas vantagens em relação às vinculações do TanStack:

  • Armazenamento em cache normalizado: SQL Connect implementa armazenamento em cache normalizado, que melhora a consistência dos dados, bem como a eficiência da memória e da rede em comparação com o armazenamento em cache no nível da consulta. Com o armazenamento em cache normalizado, se uma entidade for atualizada em uma área do app, ela também será atualizada em outras áreas que usam essa entidade.
  • Invalidação remota: SQL Connect pode invalidar remotamente entidades armazenadas em cache   em todos os dispositivos inscritos.

Se você optar por não usar o TanStack, remova as configurações react: true e angular: true do arquivo 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()
        }
    }
}

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

Importe o SDK gerado do seu projeto:

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

Em seguida, chame o método subscribe() em uma referência 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 interromper as atualizações, chame subscription.cancel().

Depois de se inscrever na consulta, como no exemplo anterior, você receberá atualizações sempre que o resultado da consulta específica mudar. Por exemplo, se outro cliente executar a mutação UpdateMovie no mesmo ID em que você se inscreveu, você receberá uma atualização.

Indicadores de atualização de consulta implícitos

No exemplo anterior, foi possível se inscrever em uma consulta e receber atualizações em tempo real sem outras modificações nas operações. Em particular, não foi necessário especificar que a mutação UpdateMovie pode afetar o resultado da consulta GetMovieById.

Isso é possível porque a consulta GetMovieById recebe implicitamente um indicador de atualização da mutação UpdateMovie. Os indicadores de atualização implícitos são enviados entre um subconjunto das consultas e mutações que você pode escrever:

Se a consulta realizar uma pesquisa de entidade única por chave primária, qualquer mutação que gravar na mesma entidade, também identificada pela chave primária, vai acionar implicitamente um indicador de atualização.

  • _insert e _insertMany
  • _upsert e _upsertMany
  • _update
  • _delete

_deleteMany e _updateMany não enviam indicadores de atualização.

No exemplo anterior, a consulta GetMovieById pesquisa um único filme por ID (movie(id: $id)) e a mutação UpdateMovie atualiza um único filme, especificado por ID (movie_update(id: $id, ...)), para que a consulta possa aproveitar a atualização implícita.

As operações de inserção e upsert podem acionar indicadores de atualização implícitos quando você está usando um valor conhecido, como o UID de um usuário Firebase Authentication.

Por exemplo, considere uma consulta como a seguinte:

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

A consulta receberia implicitamente um indicador de atualização de uma mutação como a seguinte:

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

Quando as consultas ou mutações forem mais complicadas, será necessário especificar as condições que exigem uma atualização de consulta. Continue para a próxima seção para saber como.

Indicadores de atualização de consulta explícitos

Além dos indicadores de atualização que são enviados implicitamente por mutações para consultas, também é possível especificar explicitamente quando uma consulta deve receber um indicador de atualização. Para isso, anote as consultas com a diretiva @refresh.

O uso da diretiva @refresh é obrigatório sempre que as consultas não atenderem aos critérios específicos (consulte acima) para atualização automática. Alguns exemplos de consultas que precisam incluir essa diretiva incluem:

  • Consultas que recuperam listas de entidades
  • Consultas que realizam mesclagens em outras tabelas
  • Consultas de agregação
  • Consultas que usam SQL nativo
  • Consultas que usam resolvers personalizados

É possível especificar uma política de atualização de duas maneiras:

Intervalos baseados em tempo

Atualize a consulta em um intervalo de tempo fixo.

Por exemplo, suponha que sua base de usuários muito ativa possa resultar na atualização da classificação cumulativa de um filme muitas vezes por minuto, principalmente após o lançamento de um filme. Em vez de atualizar a consulta sempre que a classificação mudar, você pode atualizar a consulta a cada poucos segundos para receber atualizações que reflitam o resultado cumulativo de várias mutações.

# dataconnect/connector/operations.gql

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

Execução de mutação

Atualize a consulta quando uma mutação específica for executada. Essa abordagem torna explícito quais mutações têm o potencial de mudar o resultado da consulta.

Por exemplo, suponha que você tenha uma consulta que recupera informações sobre vários filmes em vez de um específico. Essa consulta precisa ser atualizada sempre que uma mutação atualizar qualquer um dos registros de filmes.

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

Também é possível especificar uma condição de expressão CEL que precisa ser atendida para que a mutação acione uma atualização de consulta.

É altamente recomendável fazer isso. Quanto mais preciso você puder ser ao especificar a condição, menos recursos desnecessários do banco de dados serão consumidos e mais responsivo será seu app.

Por exemplo, suponha que você tenha uma consulta que liste filmes apenas em um gênero especificado. Essa consulta só será atualizada quando uma mutação atualizar um filme no mesmo 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
  }
}

Vinculações CEL em condições @refresh

A expressão condition em onMutationExecuted tem acesso a dois contextos:

request

O estado da consulta em que você está inscrito.

Vinculação Descrição
request.variables Variáveis transmitidas para a consulta (por exemplo, request.variables.id)
request.auth.uid Firebase Authentication UID da autenticação do Firebase do usuário que executou a consulta
request.auth.token Dicionário de declarações de token Firebase Authentication para o usuário que executou a consulta
mutation

O estado da mutação que foi executada.

Vinculação Descrição
mutation.variables Variáveis transmitidas para a mutação (por exemplo, mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID da autenticação do Firebase do usuário que executou a mutação
mutation.auth.token Dicionário de Firebase Authentication declarações de token para o usuário que executou a mutação
Padrões comuns
# 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"

Várias diretivas @refresh

É possível especificar a diretiva @refresh várias vezes em uma consulta para acionar uma atualização sempre que um dos critérios especificados por uma das diretivas @refresh for atendido.

Por exemplo, a consulta a seguir será atualizada a cada 30 segundos e sempre que uma das mutações especificadas for executada:

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

Referência

Consulte a referência da diretiva @refresh para mais exemplos.