即時取得 SQL Connect 的最新動態

您的用戶端程式碼可以訂閱查詢,在查詢結果變更時取得即時更新。

事前準備

  • 按照網頁Apple 平台Flutter 的說明文件,為專案設定 SDK 生成作業。

    • 您必須為所有產生的 SDK 啟用用戶端快取。 具體來說,每個 SDK 設定都必須包含類似下列的宣告:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • 應用程式用戶端必須使用新版的 SQL Connect 核心 SDK:

    • Apple:Firebase SQL Connect適用於 Swift 的 SDK 11.12.0 以上版本
    • 網頁:JavaScript SDK 12.12.0 以上版本
    • Flutter:firebase_data_connect0.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 執行結果的變更:

網頁

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 進行快取和即時訂閱。在 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 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指令參考資料