Mendapatkan update real-time dari SQL Connect

Kode klien Anda dapat berlangganan kueri untuk mendapatkan update real-time saat hasil kueri berubah.

Sebelum memulai

  • Siapkan pembuatan SDK untuk project Anda seperti yang dijelaskan dalam dokumentasi untuk web, platform Apple, dan Flutter.

    • Anda harus mengaktifkan caching sisi klien untuk semua SDK yang dibuat. Secara khusus, setiap konfigurasi SDK harus berisi deklarasi seperti berikut:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • Klien aplikasi Anda harus menggunakan versi terbaru SQL Connect core SDK:

    • Apple: Firebase SQL Connect SDK untuk Swift versi 11.12.0 atau yang lebih baru
    • Web: JavaScript SDK versi 12.12.0 atau yang lebih baru
    • Flutter: firebase_data_connect versi 0.3.0 atau yang lebih baru
  • Buat ulang SDK klien Anda menggunakan Firebase CLI versi 15.14.0 atau yang lebih baru.

Berlangganan hasil kueri

Anda dapat berlangganan kueri untuk merespons perubahan pada hasil kueri. Misalnya, Anda memiliki skema dan operasi berikut yang ditentukan dalam project Anda:

# 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
    })
}

Untuk berlangganan perubahan pada hasil menjalankan 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 juga mendukung caching dan langganan real-time menggunakan TanStack. Saat Anda menentukan react: true atau angular: true dalam file connector.yaml, SQL Connect akan membuat binding untuk React atau Angular menggunakan TanStack.

Binding ini dapat berfungsi bersama SQL Connect's dukungan real-time bawaan, tetapi hanya dengan beberapa kesulitan. Sebaiknya gunakan binding berbasis TanStack atau SQL Connect's dukungan real-time bawaan, tetapi jangan keduanya.

Perhatikan bahwa implementasi real-time SQL Connect's sendiri memiliki beberapa keunggulan dibandingkan binding TanStack:

  • Caching yang dinormalkan: SQL Connect mengimplementasikan caching yang dinormalkan, yang meningkatkan konsistensi data serta efisiensi memori dan jaringan dibandingkan dengan caching tingkat kueri. Dengan caching yang dinormalkan, jika entity diperbarui di satu area aplikasi Anda, entity tersebut juga akan diperbarui di area lain yang menggunakan entity tersebut.
  • Pembatalan jarak jauh: SQL Connect dapat membatalkan entity yang di-cache dari jarak jauh di semua perangkat yang berlangganan.

Jika memilih untuk tidak menggunakan TanStack, Anda harus menghapus setelan react: true dan angular: true dari file connector.yaml.

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

Impor SDK yang dibuat project Anda:

import 'package:flutter_app/dataconnect_generated/generated.dart';

Kemudian, panggil metode subscribe() pada referensi kueri:

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);
  }
});

Untuk menghentikan update, Anda dapat memanggil subscription.cancel().

Setelah berlangganan kueri seperti dalam contoh sebelumnya, Anda akan mendapatkan update setiap kali hasil kueri tertentu berubah. Misalnya, jika klien lain menjalankan mutasi UpdateMovie pada ID yang sama dengan yang Anda langgani, Anda akan mendapatkan update.

Sinyal pemuatan ulang kueri implisit

Dalam contoh sebelumnya, Anda dapat berlangganan kueri dan mendapatkan update real-time tanpa modifikasi tambahan pada operasi Anda. Secara khusus, Anda tidak perlu menentukan bahwa mutasi UpdateMovie dapat memengaruhi hasil kueri GetMovieById.

Hal ini dapat dilakukan karena kueri GetMovieById secara implisit mendapatkan sinyal pemuatan ulang dari mutasi UpdateMovie. Sinyal pemuatan ulang implisit dikirim antara subset kueri dan mutasi yang mungkin Anda tulis:

Jika kueri Anda melakukan pencarian satu entity berdasarkan kunci utama, maka mutasi apa pun yang menulis ke entity yang sama, yang juga diidentifikasi oleh kunci utamanya akan secara implisit memicu sinyal pemuatan ulang.

  • _insert dan _insertMany
  • _upsert dan _upsertMany
  • _update
  • _delete

_deleteMany dan _updateMany tidak mengirim sinyal pemuatan ulang.

Dalam contoh sebelumnya, kueri GetMovieById mencari satu film berdasarkan ID (movie(id: $id)) dan mutasi UpdateMovie memperbarui satu film, yang ditentukan berdasarkan ID (movie_update(id: $id, ...)), sehingga kueri dapat memanfaatkan pemuatan ulang implisit.

Operasi penyisipan dan upsert dapat memicu sinyal pemuatan ulang implisit saat Anda menggunakan nilai yang diketahui, seperti UID pengguna Firebase Authentication.

Misalnya, pertimbangkan kueri seperti berikut:

query GetExtendedProfileByUser @auth(level: USER) {
  profile(key: { id_expr: "auth.uid" }) {
    id
    status
    photoUrl
    socialLink
  }
}

Kueri akan secara implisit menerima sinyal pemuatan ulang dari mutasi seperti berikut:

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
  }
}

Jika kueri atau mutasi Anda lebih rumit, Anda harus menentukan kondisi yang memerlukan pemuatan ulang kueri. Lanjutkan ke bagian berikutnya untuk mempelajari caranya.

Sinyal pemuatan ulang kueri eksplisit

Selain sinyal pemuatan ulang yang dikirim secara implisit oleh mutasi ke kueri, Anda juga dapat menentukan secara eksplisit kapan kueri harus menerima sinyal pemuatan ulang. Anda dapat melakukannya dengan memberi anotasi pada kueri dengan direktif @refresh.

Penggunaan direktif @refresh diperlukan setiap kali kueri Anda tidak memenuhi kriteria tertentu (lihat di atas) untuk pemuatan ulang otomatis. Beberapa contoh kueri yang harus menyertakan direktif ini meliputi:

  • Kueri yang mengambil daftar entity
  • Kueri yang melakukan penggabungan pada tabel lain
  • Kueri agregasi
  • Kueri menggunakan SQL native
  • Kueri menggunakan resolver kustom

Anda dapat menentukan kebijakan pemuatan ulang dengan dua cara:

Interval berbasis waktu

Muat ulang kueri pada interval waktu tetap.

Misalnya, basis pengguna Anda yang sangat aktif dapat menyebabkan rating kumulatif film diperbarui berkali-kali setiap menit, terutama setelah film dirilis. Daripada memuat ulang kueri setiap kali rating berubah, Anda dapat memuat ulang kueri setiap beberapa detik, untuk mendapatkan update yang mencerminkan hasil kumulatif dari beberapa mutasi.

# dataconnect/connector/operations.gql

query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
  movie(id: $id) {
    id
    averageRating
  }
}

Eksekusi mutasi

Muat ulang kueri saat mutasi tertentu dijalankan. Pendekatan ini membuat mutasi yang berpotensi mengubah hasil kueri menjadi eksplisit.

Misalnya, Anda memiliki kueri yang mengambil informasi tentang beberapa film, bukan satu film tertentu. Kueri ini harus dimuat ulang setiap kali mutasi memperbarui salah satu rekaman film.

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
  }
}

Anda juga dapat menentukan kondisi ekspresi CEL yang harus dipenuhi agar mutasi dapat memicu pemuatan ulang kueri.

Tindakan ini sangat direkomendasikan. Semakin presisi Anda saat menentukan kondisi, semakin sedikit resource database yang tidak perlu akan digunakan, dan semakin responsif aplikasi Anda.

Misalnya, Anda memiliki kueri yang hanya mencantumkan film dalam genre tertentu. Kueri ini hanya boleh dimuat ulang saat mutasi memperbarui film dalam genre yang sama:

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
  }
}

Binding CEL dalam kondisi @refresh

Ekspresi condition di onMutationExecuted memiliki akses ke dua konteks:

request

Status kueri yang dilanggani.

Binding Deskripsi
request.variables Variabel yang diteruskan ke kueri (misalnya, request.variables.id)
request.auth.uid Firebase Authentication UID pengguna yang menjalankan kueri
request.auth.token Kamus klaim token Firebase Authentication untuk pengguna yang menjalankan kueri
mutation

Status mutasi yang dijalankan.

Binding Deskripsi
mutation.variables Variabel yang diteruskan ke mutasi (misalnya, mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID pengguna yang menjalankan mutasi
mutation.auth.token Kamus klaim token Firebase Authentication untuk pengguna yang menjalankan mutasi
Pola Umum
# 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"

Beberapa direktif @refresh

Anda dapat menentukan direktif @refresh beberapa kali pada kueri untuk memicu pemuatan ulang setiap kali salah satu kriteria yang ditentukan oleh salah satu direktif @refresh terpenuhi.

Misalnya, kueri berikut akan dimuat ulang setiap 30 detik serta setiap kali salah satu mutasi yang ditentukan dijalankan:

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
  }
}

Referensi

Lihat referensi direktif @refresh untuk contoh lainnya.