Mendapatkan update real-time dari SQL Connect

Kode klien Anda dapat berlangganan kueri untuk mendapatkan pembaruan 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 penyimpanan dalam cache sisi klien untuk semua SDK yang dihasilkan. Secara khusus, setiap konfigurasi SDK harus berisi pernyataan seperti berikut:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • Klien aplikasi Anda harus menggunakan SDK inti SQL Connect versi terbaru:

    • 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 dukungan real-time bawaan SQL Connect, tetapi hanya dengan beberapa kesulitan. Sebaiknya gunakan binding berbasis TanStack atau dukungan real time bawaan SQL Connect, tetapi jangan keduanya.

Perhatikan bahwa penerapan real-time SQL Connect sendiri memiliki beberapa keunggulan dibandingkan binding TanStack:

  • Penyimpanan dalam cache yang dinormalisasi: SQL Connect mengimplementasikan penyimpanan dalam cache yang dinormalisasi, yang meningkatkan konsistensi data serta efisiensi memori dan jaringan dibandingkan dengan penyimpanan dalam cache tingkat kueri. Dengan penayangan cache yang dinormalisasi, jika entitas diperbarui di satu area aplikasi, entitas tersebut juga akan diperbarui di area lain yang menggunakan entitas tersebut.
  • Pembatalan validasi dari jarak jauh: SQL Connect dapat membatalkan validasi entitas yang di-cache dari jarak jauh di semua perangkat yang berlangganan.

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

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

Mengimpor SDK yang dihasilkan 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 daftarkan, 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 terjadi karena kueri GetMovieById secara implisit mendapatkan sinyal pembaruan dari mutasi UpdateMovie. Sinyal refresh implisit dikirim di antara subset kueri dan mutasi yang mungkin Anda tulis:

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

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

_deleteMany dan _updateMany tidak mengirim sinyal refresh.

Pada contoh sebelumnya, kueri GetMovieById mencari satu film menurut ID (movie(id: $id)) dan mutasi UpdateMovie memperbarui satu film, yang ditentukan oleh ID (movie_update(id: $id, ...)), sehingga kueri dapat memanfaatkan refresh implisit.

Operasi penyisipan dan upsert dapat memicu sinyal refresh 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 refresh 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 refresh kueri. Lanjutkan ke bagian berikutnya untuk mempelajari caranya.

Sinyal pemuatan ulang kueri eksplisit

Selain sinyal pembaruan yang dikirim secara implisit oleh mutasi ke kueri, Anda juga dapat menentukan secara eksplisit kapan kueri harus menerima sinyal pembaruan. Anda dapat melakukannya dengan menganotasi kueri menggunakan direktif @refresh.

Penggunaan direktif @refresh diperlukan setiap kali kueri Anda tidak memenuhi kriteria spesifik (lihat di atas) untuk refresh 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 refresh dengan dua cara:

Interval berbasis waktu

Memuat ulang kueri pada interval waktu tetap.

Misalnya, anggaplah basis pengguna yang sangat aktif dapat menyebabkan rating kumulatif film diperbarui berkali-kali setiap menit, terutama setelah rilis film. Daripada memuat ulang kueri setiap kali rating berubah, Anda dapat memuat ulang kueri setiap beberapa detik untuk mendapatkan pembaruan 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

Memuat ulang kueri saat mutasi tertentu dijalankan. Pendekatan ini secara eksplisit menunjukkan mutasi mana yang berpotensi mengubah hasil kueri.

Misalnya, Anda memiliki kueri yang mengambil informasi tentang beberapa film, bukan satu film tertentu. Kueri ini harus diperbarui setiap kali mutasi telah 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 memicu refresh kueri.

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

Misalnya, Anda memiliki kueri yang mencantumkan film hanya dalam genre tertentu. Kueri ini hanya boleh diperbarui 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 sedang dilanggan.

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 dieksekusi.

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 pembaruan setiap kali salah satu kriteria yang ditentukan oleh salah satu direktif @refresh terpenuhi.

Misalnya, kueri berikut akan di-refresh setiap 30 detik dan 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.