SQL Connect からリアルタイムの更新を取得する

クライアント コードはクエリをサブスクライブして、クエリの結果が変更されたときにリアルタイムで更新を取得できます。

始める前に

  • ウェブApple プラットフォームFlutter のドキュメントの説明に沿って、プロジェクトの SDK 生成を設定します。

    • 生成されたすべての SDK でクライアントサイド キャッシュ保存を有効にする必要があります。具体的には、すべての SDK 構成に次のような宣言を含める必要があります。
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • アプリ クライアントは、最新バージョンの SQL Connect コア SDK を使用している必要があります。

    • Apple: Firebase SQL Connect SDK for Swift バージョン 11.12.0 以降
    • ウェブ: 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 の実行結果の変更をサブスクライブするには:

ウェブ

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: true または angular: true を指定すると、SQL Connect は TanStack を使用して React または Angular のバインディングを生成します。

これらのバインディングは SQL Connect の組み込みのリアルタイム サポートと連携できますが、多少の困難が伴います。TanStack ベースのバインディングまたは SQL Connect の組み込みのリアルタイム サポートのいずれかを使用することをおすすめします。両方を使用することはおすすめしません

SQL Connect 独自のリアルタイム実装には、TanStack バインディングよりも優れた点がいくつかあります。

  • 正規化されたキャッシュ保存: SQL Connect正規化されたキャッシュ保存を実装します。これにより、クエリレベルのキャッシュ保存と比較して、データの整合性、メモリ、ネットワークの効率が向上します。正規化されたキャッシュ保存では、アプリの 1 つの領域でエンティティが更新されると、そのエンティティを使用する他の領域でも更新されます。
  • リモート無効化: 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, ...))で指定された単一の映画を更新するため、クエリは暗黙的な更新を利用できます。

既知の値(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 を使用したクエリ
  • カスタム リゾルバを使用するクエリ

更新ポリシーは次の 2 つの方法で指定できます。

時間ベースの間隔

固定時間間隔でクエリを更新します。

たとえば、アクティブなユーザーベースが非常に多い場合、特に映画の公開後には、映画の累積評価が毎分何度も更新される可能性があります。評価が変更されるたびにクエリを更新するのではなく、数秒ごとにクエリを更新して、複数のミューテーションの累積結果を反映した更新を取得することもできます。

# 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 バインディング

onMutationExecutedcondition 式は、次の 2 つのコンテキストにアクセスできます。

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 ディレクティブのリファレンスをご覧ください。