您的客户端代码可以订阅查询,以便在查询结果发生变化时获得实时更新。
准备工作
按照网页、Apple 平台和 Flutter 的文档中所述,为您的项目设置 SDK 生成。
- 您必须为所有生成的 SDK 启用客户端缓存。 具体来说,每个 SDK 配置都必须包含如下声明:
clientCache: maxAge: 5s storage: ... # Optional.您的应用客户端必须使用最新版本的 SQL Connect 核心 SDK:
- Apple:适用于 Swift 版本 11.12.0 或更高版本的 Firebase SQL Connect SDK
- 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 进行缓存和实时订阅。在 connector.yaml 文件中指定 react: true 或 angular: true 时,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 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()
}
}
}
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, ...)) 指定的单个电影,因此该查询可以利用隐式刷新。
当您以已知值(例如用户的 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
}
}
突变执行
在执行特定 mutation 时刷新查询。这种方法明确指出了哪些突变可能会改变查询结果。
例如,假设您有一个查询,用于检索多部电影(而非特定电影)的相关信息。每当有变异更新了任何电影记录时,此查询都应刷新。
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 指令参考文档。