获取 SQL Connect 的实时更新

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

准备工作

  • 按照网页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: trueangular: true 时,SQL Connect 会使用 TanStack 为 React 或 Angular 生成绑定。

这些绑定可以与 SQL Connect 的内置实时支持协同工作,但只能在一定程度上实现。我们建议您使用基于 TanStack 的绑定或 SQL Connect 的内置实时支持,但不要同时使用这两者

请注意,SQL Connect 自身的实时实现相对于 TanStack 绑定具有以下优势:

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

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