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 de SDKs para seu projeto conforme descrito na documentação para Web, plataformas da Apple e Flutter.
- É necessário ativar o 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 seu app precisam usar uma versão recente do SDK principal SQL Connect:
- Apple: SDK do Firebase SQL Connect para Swift versão 11.12.0 ou mais recente
- Web: SDK para JavaScript versão 12.12.0 ou mais recente
- Flutter:
firebase_data_connectversão 0.3.0 ou mais recente
Regenere os SDKs do cliente usando a versão 15.14.0 ou mais recente da CLI do Firebase.
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 seguinte esquema e operações 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>
);
};
O 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 suporte em tempo real integrado do SQL Connect, mas com alguma dificuldade. Recomendamos usar as vinculações baseadas em TanStack ou o suporte em tempo real integrado do SQL Connect, mas não os dois.
A implementação em tempo real do próprio SQL Connect tem algumas vantagens em relação às vinculações do TanStack:
- Armazenamento em cache normalizado: o SQL Connect implementa o armazenamento em cache normalizado, que melhora a consistência dos dados e 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: o SQL Connect pode invalidar remotamente entidades 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 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
Importe o SDK gerado do 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ê vai 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ê vai receber uma atualização.
Indicadores de atualização de consulta implícita
No exemplo anterior, você conseguiu se inscrever em uma consulta e receber atualizações em tempo real sem modificações adicionais nas suas operações. Em
particular, não é 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 sinal de atualização da mutação UpdateMovie. Os indicadores de atualização implícita 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 grave na mesma entidade, também identificada pela chave primária, vai acionar implicitamente um sinal de atualização.
_inserte_insertMany_upserte_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, ...)). Assim, a consulta pode aproveitar a atualização implícita.
As operações de inserção e upsert podem acionar sinais de atualização implícita quando você está usando um valor conhecido, como o UID de um usuário Firebase Authentication.
Por exemplo, considere uma consulta como esta:
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 e saiba como.
Indicadores explícitos de atualização de consultas
Além dos indicadores de atualizaçã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 atendem aos critérios específicos (veja acima) para atualização automática. Confira alguns exemplos de consultas que precisam incluir essa diretiva:
- Consultas que recuperam listas de entidades
- Consultas que realizam junções em outras tabelas
- Consultas de agregação
- Consultas usando SQL nativo
- Consultas que usam resoluções personalizadas
É possível especificar uma política de atualização de duas maneiras:
Intervalos com base no 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 várias vezes por minuto, principalmente após o lançamento. Em vez de atualizar a consulta toda vez que a classificação muda, atualize a consulta a cada poucos segundos para receber atualizações que refletem 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 deixa 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 precisa for a condição, menos recursos desnecessários do banco de dados serão consumidos e mais responsivo será o app.
Por exemplo, suponha que você tenha uma consulta que liste filmes apenas de um gênero específico. Essa consulta só deve ser atualizada quando uma mutação atualiza um filme do 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 de CEL em condições @refresh
A expressão condition em onMutationExecuted tem acesso a dois contextos:
request
O estado da consulta a que você se inscreveu.
| Vinculação | Descrição |
|---|---|
request.variables |
Variáveis transmitidas à consulta (por exemplo, request.variables.id) |
request.auth.uid |
Firebase Authentication UID 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 executada.
| Vinculação | Descrição |
|---|---|
mutation.variables |
Variáveis transmitidas à mutação (por exemplo, mutation.variables.movieId) |
mutation.auth.uid |
UID Firebase Authentication do usuário que executou a mutação |
mutation.auth.token |
Dicionário de declarações de token Firebase Authentication 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.