您的客户端代码可以订阅查询,以便在查询结果发生变化时获得实时更新。
准备工作
按照 Web、Apple 平台和Flutter的文档中所述,为您的项目设置 SDK 生成。
- 您必须为所有生成的 SDK 启用客户端缓存。 具体来说,每个 SDK 配置都必须包含如下声明:
clientCache: maxAge: 5s storage: ... # Optional.您的应用客户端必须使用最新版本的 SQL Connect 核心 SDK:
- Apple:Firebase SQL Connect SDK for Swift 版本 11.12.0 或更高版本
- Web:JavaScript SDK 版本 12.12.0 或更高版本
- Flutter:
firebase_data_connect版本 0.3.0 或更高版本
使用 Firebase CLI 版本 15.14.0 或更高版本重新生成客户端 SDK。
订阅查询结果
您可以订阅查询,以便在查询结果发生变化时做出响应。例如,假设您在项目中定义了以下架构和操作:
# 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 运行结果的变化,请执行以下操作:
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 还支持使用
TanStack 进行缓存和实时订阅。如果您在 react: true 或 angular: true 文件中指定
connector.yaml,SQL Connect 会使用 TanStack 为 React 或 Angular 生成绑定。
这些绑定可以与 SQL Connect'的内置实时支持协同工作,但会遇到一些 困难。我们建议您使用基于 TanStack 的绑定或 SQL Connect'的内置实时支持,但不要同时使用两者。
请注意,SQL Connect自己的实时实现与 TanStack 绑定相比具有一些 优势:
- 规范化缓存:SQL Connect 实现 规范化缓存, 与查询级缓存相比,规范化缓存可以提高数据一致性以及内存和网络效率 。借助规范化缓存,如果实体在应用的某个区域更新,则在使用该实体的其他区域也会更新。
- 远程失效:SQL Connect 可以远程使所有订阅设备上的缓存 实体失效。
如果您选择不使用 TanStack,则应从 connector.yaml 文件中移除 react: true 和 angular: true 设置。
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
导入项目生成的 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()。
如上例所示,订阅查询后,每当特定查询的结果发生变化时,您都会收到更新。例如,如果另一个客户端对您订阅的同一 ID 执行 UpdateMovie 变更,您将收到更新。
隐式查询刷新信号
在上例中,您无需对操作进行任何其他修改,即可订阅查询并获得实时更新。具体来说,您无需指定 UpdateMovie 变更可能会影响 GetMovieById 查询的结果。
之所以能够实现这一点,是因为 GetMovieById 查询会隐式接收来自 UpdateMovie 变更的刷新信号。隐式刷新信号会在您可能会编写的部分查询和变更之间发送:
如果您的查询 执行按主键进行单实体查找 ,则任何 变更 (也由其主键 标识 )都会隐式触发刷新信号。
_insert和_insertMany_upsert和_upsertMany_update_delete
_deleteMany 和 _updateMany 不会发送刷新信号。
在上一个示例中,GetMovieById 查询按 ID
(movie(id: $id)) 查找单个电影,而 UpdateMovie 变更更新由 ID (movie_update(id: $id, ...)) 指定的单个电影,因此查询可以利用隐式刷新。
当您 使用已知值(例如 Firebase Authentication 用户的 UID)作为键时,插入和 upsert 操作可以触发隐式刷新信号。
例如,考虑如下查询:
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
}
}
@refresh 条件中的 CEL 绑定
onMutationExecuted 中的 condition 表达式可以访问两个上下文:
request
被订阅查询的状态。
| 绑定 | 说明 |
|---|---|
request.variables |
传递给查询的变量(例如 request.variables.id) |
request.auth.uid |
Firebase Authentication 执行查询的用户的 UID |
request.auth.token |
执行查询的用户的 Firebase Authentication 令牌声明字典 |
mutation
已执行变更的状态。
| 绑定 | 说明 |
|---|---|
mutation.variables |
传递给变更的变量(例如 mutation.variables.movieId) |
mutation.auth.uid |
Firebase Authentication 执行变更的用户的 UID |
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 指令参考文档。