获取 SQL Connect 的实时更新

您的客户端代码可以订阅查询,以便在查询结果发生变化时获得实时更新。

准备工作

  • 按照 WebApple 平台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: trueangular: true 文件中指定 connector.yamlSQL Connect 会使用 TanStack 为 React 或 Angular 生成绑定。

这些绑定可以与 SQL Connect'的内置实时支持协同工作,但会遇到一些 困难。我们建议您使用基于 TanStack 的绑定或 SQL Connect'的内置实时支持,但不要同时使用两者

请注意,SQL Connect自己的实时实现与 TanStack 绑定相比具有一些 优势:

  • 规范化缓存:SQL Connect 实现 规范化缓存, 与查询级缓存相比,规范化缓存可以提高数据一致性以及内存和网络效率 。借助规范化缓存,如果实体在应用的某个区域更新,则在使用该实体的其他区域也会更新。
  • 远程失效:SQL Connect 可以远程使所有订阅设备上的缓存  实体失效。

如果您选择不使用 TanStack,则应从 connector.yaml 文件中移除 react: trueangular: 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 指令参考文档