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

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

始める前に

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

    • 生成されたすべての SDK でクライアントサイド キャッシュを有効にする必要があります。 具体的には、すべての SDK 構成に次のような宣言を含める必要があります。
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • アプリ クライアントは、 SQL Connect Core 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's の組み込みのリアルタイム サポートと連携して動作しますが、多少の 困難が伴います。TanStack ベースのバインディングまたは SQL Connect の組み込みのリアルタイム サポートのいずれかを使用することをおすすめします。両方 を使用することはおすすめしません。

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

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

挿入オペレーションとアップサート オペレーションは、 ユーザーの 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 を使用するクエリ
  • カスタム リゾルバ を使用するクエリ

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

時間ベースの間隔

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

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

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