Ваш клиентский код может подписаться на запросы, чтобы получать обновления в реальном времени при изменении результата запроса.
Прежде чем начать
Настройте генерацию SDK для вашего проекта, как описано в документации для веб-платформ , платформ Apple и Flutter .
- Необходимо включить кэширование на стороне клиента для всех сгенерированных SDK. В частности, каждая конфигурация SDK должна содержать объявление следующего вида:
clientCache: maxAge: 5s storage: ... # Optional.Клиентские приложения должны использовать последнюю версию основного SDK SQL Connect :
- Apple: Firebase SQL Connect SDK для Swift версии 11.12.0 или новее
- Веб: JavaScript SDK версии 12.12.0 или новее.
- Flutter:
firebase_data_connectверсии 0.3.0 или новее
Перегенерируйте клиентские SDK, используя версию 15.14.0 Firebase CLI или более новую.
Подписка на результаты запроса
Вы можете подписаться на запрос, чтобы реагировать на изменения в его результатах. Например, предположим, что в вашем проекте определены следующие схема и операции:
# 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
})
}
Чтобы подписаться на изменения в результатах выполнения GetMovieById :
Веб
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);
Веб (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 также поддерживает кэширование и подписки в реальном времени с использованием TanStack. Если вы укажете react: true или angular: true в файле connector.yaml , SQL Connect сгенерирует привязки для React или Angular с использованием TanStack.
Эти привязки могут работать совместно со встроенной поддержкой реального времени SQL Connect , но с некоторыми трудностями. Мы рекомендуем использовать либо привязки на основе TanStack, либо встроенную поддержку реального времени SQL Connect , но не оба варианта одновременно .
Следует отметить, что собственная реализация SQL Connect в режиме реального времени имеет некоторые преимущества перед привязками TanStack:
- Нормализованное кэширование: SQL Connect реализует нормализованное кэширование , которое повышает согласованность данных, а также эффективность использования памяти и сети по сравнению с кэшированием на уровне запросов. При нормализованном кэшировании, если сущность обновляется в одной области вашего приложения, она также обновится в других областях, использующих эту сущность.
- Удалённая аннулирование кэша: SQL Connect может удалённо аннулировать кэшированные сущности на всех устройствах, на которые оформлена подписка.
Если вы решите не использовать TanStack, вам следует удалить параметры react: true и angular: true из файла 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()
}
}
}
Порхание
Импортируйте сгенерированный SDK вашего проекта:
import 'package:flutter_app/dataconnect_generated/generated.dart';
Затем вызовите метод subscribe() для ссылки на запрос:
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);
}
});
Чтобы остановить обновления, вы можете вызвать subscription.cancel() .
После подписки на запрос, как в предыдущем примере, вы будете получать обновления всякий раз, когда изменяется результат конкретного запроса. Например, если другой клиент выполнит мутацию UpdateMovie для того же ID, на который вы подписались, вы получите обновление.
Неявные сигналы обновления запроса
В приведенном выше примере вы смогли подписаться на запрос и получать обновления в реальном времени без каких-либо дополнительных изменений в ваших операциях. В частности, вам не нужно было указывать, что мутация UpdateMovie может влиять на результат запроса GetMovieById .
Это возможно, потому что запрос GetMovieById неявно получает сигнал обновления от мутации UpdateMovie . Неявные сигналы обновления передаются между подмножеством запросов и мутаций, которые вы можете написать:
Если ваш запрос выполняет поиск одной сущности по первичному ключу , то любое изменение , которое записывает данные в ту же сущность, также идентифицированную по первичному ключу, неявно вызовет сигнал обновления.
-
_insertи_insertMany -
_upsertи_upsertMany -
_update -
_delete
_deleteMany и _updateMany не отправляют сигналы обновления.
В предыдущем примере запрос GetMovieById ищет один фильм по ID ( movie(id: $id) ) и мутация UpdateMovie обновляет один фильм, указанный по ID ( movie_update(id: $id, ...) ), поэтому запрос может использовать преимущества неявного обновления.
Операции вставки и обновления могут вызывать неявные сигналы обновления, если вы используете в качестве ключа известное значение, например, UID пользователя Firebase Authentication .
Например, рассмотрим следующий запрос:
query GetExtendedProfileByUser @auth(level: USER) {
profile(key: { id_expr: "auth.uid" }) {
id
status
photoUrl
socialLink
}
}
Запрос будет неявно получать сигнал обновления от мутации следующего вида:
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
}
}
Если ваши запросы или мутации более сложные, вам потребуется указать условия, требующие обновления запроса. Перейдите к следующему разделу, чтобы узнать, как это сделать.
Явные сигналы обновления запроса
В дополнение к сигналам обновления, которые неявно отправляются мутациями запросам, вы также можете явно указать, когда запрос должен получать сигнал обновления. Для этого используйте аннотацию @refresh для ваших запросов.
Использование директивы @refresh необходимо всякий раз, когда ваши запросы не соответствуют определенным критериям (см. выше) для автоматического обновления. Примеры запросов, которые должны содержать эту директиву:
- Запросы, извлекающие списки сущностей.
- Запросы, выполняющие объединение других таблиц.
- Агрегационные запросы
- Запросы с использованием нативного SQL
- Запросы с использованием пользовательских резолверов
Политику обновления можно задать двумя способами:
Интервалы, основанные на времени
Обновляйте запрос через фиксированный интервал времени.
Например, предположим, что из-за высокой активности пользователей совокупный рейтинг фильма обновляется много раз в минуту, особенно после выхода фильма в прокат. Вместо того чтобы обновлять запрос каждый раз при изменении рейтинга, вы могли бы обновлять его каждые несколько секунд, чтобы получать обновления, отражающие совокупный результат потенциально нескольких изменений.
# dataconnect/connector/operations.gql
query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
movie(id: $id) {
id
averageRating
}
}
Выполнение мутации
Обновляйте запрос при выполнении определенной мутации. Такой подход позволяет явно указать, какие мутации потенциально могут изменить результат запроса.
Например, предположим, у вас есть запрос, который извлекает информацию о нескольких фильмах, а не о конкретном. Этот запрос должен обновляться всякий раз, когда изменение вносит изменения в любую из записей о фильмах.
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
}
}
Вы также можете указать условие выражения CEL, которое должно быть выполнено для того, чтобы изменение инициировало обновление запроса.
Настоятельно рекомендуется это сделать. Чем точнее вы укажете условие, тем меньше ненужных ресурсов базы данных будет потребляться, и тем быстрее будет работать ваше приложение.
Например, предположим, у вас есть запрос, который отображает только фильмы определенного жанра. Этот запрос должен обновляться только тогда, когда мутация обновляет фильм в том же жанре:
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 в условиях @refresh
condition выражение в onMutationExecuted имеет доступ к двум контекстам:
request
Состояние запроса, на который оформлена подписка.
| Связывание | Описание |
|---|---|
request.variables | Переменные, передаваемые в запрос (например, request.variables.id ). |
request.auth.uid | UID Firebase Authentication пользователя, выполнившего запрос. |
request.auth.token | Словарь утверждений токенов Firebase Authentication для пользователя, выполнившего запрос. |
mutation
Состояние выполненной мутации.
| Связывание | Описание |
|---|---|
mutation.variables | Переменные, передаваемые в функцию мутации (например, mutation.variables.movieId ). |
mutation.auth.uid | UID Firebase Authentication пользователя, выполнившего мутацию. |
mutation.auth.token | Словарь утверждений токенов Firebase Authentication для пользователя, выполнившего мутацию. |
Общие закономерности
# 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"
Несколько директив @refresh
Директиву @refresh можно указывать несколько раз в запросе, чтобы обновление происходило всякий раз, когда выполняется хотя бы одно из условий, указанных в директиве @refresh .
Например, следующий запрос будет обновляться каждые 30 секунд, а также всякий раз, когда выполняется одна из указанных мутаций:
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
}
}
Ссылка
Дополнительные примеры см. в справочнике по директиве @refresh .