รับข้อมูลอัปเดตแบบเรียลไทม์จาก SQL Connect

โค้ดไคลเอ็นต์สามารถสมัครใช้บริการการค้นหาเพื่อรับข้อมูลอัปเดตแบบเรียลไทม์เมื่อผลการค้นหาเปลี่ยนแปลง

ก่อนเริ่มต้น

  • ตั้งค่าการสร้าง SDK สำหรับโปรเจ็กต์ตามที่อธิบายไว้ในเอกสารประกอบสำหรับ แพลตฟอร์ม Apple, Android, เว็บ และ Flutter

    • คุณต้องเปิดใช้การแคชฝั่งไคลเอ็นต์สำหรับ SDK ที่สร้างขึ้นทั้งหมด โดยเฉพาะอย่างยิ่ง การกำหนดค่า SDK ทุกรายการต้องมีการประกาศลักษณะต่อไปนี้
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • ไคลเอ็นต์แอปต้องใช้ SQL Connect Core SDK เวอร์ชันล่าสุด

    • Apple: Firebase SQL Connect SDK สำหรับ Swift เวอร์ชัน 11.12.0 ขึ้นไป
    • Android: Firebase SQL Connect SDK เวอร์ชัน 17.3.0 ขึ้นไป (BoM เวอร์ชัน 34.14.0 ขึ้นไป)
    • เว็บ: JavaScript SDK เวอร์ชัน 12.12.0 ขึ้นไป
    • Flutter: firebase_data_connect เวอร์ชัน 0.3.0 ขึ้นไป
  • สร้าง SDK ไคลเอ็นต์ใหม่โดยใช้ Firebase CLI เวอร์ชัน 15.14.0 ขึ้นไป

การสมัครใช้บริการผลการค้นหา

คุณสามารถสมัครใช้บริการการค้นหาเพื่อตอบสนองต่อการเปลี่ยนแปลงในผลการค้นหา ตัวอย่างเช่น สมมติว่าคุณมีสคีมาและการดำเนินการต่อไปนี้ที่กำหนดไว้ในโปรเจ็กต์

# 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 เมื่อคุณระบุ react: true หรือ angular: true ในไฟล์ connector.yaml ของคุณ SQL Connect จะสร้างการเชื่อมโยงสำหรับ React หรือ Angular โดยใช้ TanStack

การเชื่อมโยงเหล่านี้สามารถทำงานร่วมกับการรองรับแบบเรียลไทม์ในตัวของ SQL Connect's ได้ แต่จะทำได้ค่อนข้าง ยาก เราขอแนะนำให้คุณใช้การเชื่อมโยงที่อิงตาม TanStack หรือ SQL Connectการรองรับแบบเรียลไทม์ในตัวของ's แต่ อย่างใดอย่างหนึ่ง

โปรดทราบว่าการใช้งานแบบเรียลไทม์ของ SQL Connect's เองมีข้อดีบางประการเหนือการเชื่อมโยง TanStack ดังนี้

  • การแคชแบบปกติ: SQL Connect ใช้ การแคชแบบปกติ, ซึ่งช่วยปรับปรุงความสอดคล้องของข้อมูล รวมถึงประสิทธิภาพของหน่วยความจำและเครือข่าย เมื่อเทียบกับการแคชระดับการค้นหา การแคชแบบปกติจะทำให้เอนทิตีอัปเดตในส่วนหนึ่งของแอปและอัปเดตในส่วนอื่นๆ ที่ใช้เอนทิตีนั้นด้วย
  • การทำให้เป็นโมฆะจากระยะไกล: SQL Connect สามารถทำให้เอนทิตีที่แคชไว้ เป็นโมฆะจากระยะไกลในอุปกรณ์ทั้งหมดที่สมัครใช้บริการ

หากเลือกที่จะไม่ใช้ TanStack คุณควรนำการตั้งค่า react: true และ angular: true ออกจากไฟล์ 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()
        }
    }
}

Android

class ExampleViewModel(
  private val movieId: UUID
) : ViewModel() {
  private val _uiState = MutableStateFlow<GetMovieByIdQuery.Data.Movie?>(null)
  val uiState = _uiState.asStateFlow()

  // Subscribe to the query.
  private val movieInfoSub = ExampleConnector.instance
      .getMovieById.ref(GetMovieByIdQuery.Variables(id = movieId))
      .subscribe()

  init {
    viewModelScope.launch {
      movieInfoSub.flow.collect {
        // As query results are collected, update the UI state.
        val result = it.result.getOrElse { return@collect }
        _uiState.update{ result.data.movie }
      }
    }
  }

  companion object {
    fun provideFactory(movieId: UUID): ViewModelProvider.Factory =
      viewModelFactory {
        initializer {
          ExampleViewModel(movieId = movieId)
        }
      }
  }
}

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()

เมื่อสมัครใช้บริการการค้นหาตามตัวอย่างก่อนหน้าแล้ว คุณจะได้รับการอัปเดตทุกครั้งที่ผลการค้นหาเฉพาะเปลี่ยนแปลง ตัวอย่างเช่น หากไคลเอ็นต์อื่นดำเนินการกลายพันธุ์ UpdateMovie ในรหัสเดียวกันกับที่คุณสมัครใช้บริการ คุณจะได้รับการอัปเดต

สัญญาณรีเฟรชการค้นหาโดยนัย

ในตัวอย่างก่อนหน้า คุณสามารถสมัครใช้บริการการค้นหาและรับข้อมูลอัปเดตแบบเรียลไทม์ได้โดยไม่ต้องแก้ไขการดำเนินการเพิ่มเติม โดยเฉพาะอย่างยิ่ง คุณไม่จำเป็นต้องระบุว่าการกลายพันธุ์ UpdateMovie อาจส่งผลต่อผลการค้นหา GetMovieById

ที่เป็นเช่นนี้ได้เนื่องจากการค้นหา GetMovieById จะได้รับสัญญาณรีเฟรชจากการกลายพันธุ์ UpdateMovie โดยนัย ระบบจะส่งสัญญาณรีเฟรชโดยนัยระหว่างการค้นหาและการกลายพันธุ์บางส่วนที่คุณอาจเขียนขึ้น

หากการค้นหา ทำการค้นหาเอนทิตีเดียวด้วยคีย์หลัก แล้ว การกลายพันธุ์ ใดก็ตามที่เขียนไปยังเอนทิตีเดียวกัน ซึ่งระบุด้วยคีย์หลัก ก็จะทริกเกอร์สัญญาณรีเฟรชโดยนัย

  • _insert และ _insertMany
  • _upsert และ _upsertMany
  • _update
  • _delete

_deleteMany และ _updateMany จะไม่ส่งสัญญาณรีเฟรช

ในตัวอย่างก่อนหน้า การค้นหา GetMovieById จะค้นหาภาพยนตร์เรื่องเดียวตามรหัส (movie(id: $id)) และการกลายพันธุ์ UpdateMovie จะอัปเดตภาพยนตร์เรื่องเดียวที่ระบุด้วยรหัส (movie_update(id: $id, ...)) ดังนั้นการค้นหาจึงใช้ประโยชน์จากการรีเฟรชโดยนัยได้

การดำเนินการแทรกและ upsert สามารถทริกเกอร์สัญญาณรีเฟรชโดยนัยได้เมื่อคุณ ใช้ค่าที่ทราบ เช่น 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 วิธีดังนี้

ช่วงเวลาตามเวลา

รีเฟรชการค้นหาตามช่วงเวลาที่กำหนด

ตัวอย่างเช่น ฐานผู้ใช้ที่ใช้งานอยู่มากอาจทำให้มีการอัปเดตคะแนนสะสมของภาพยนตร์หลายครั้งทุกนาที โดยเฉพาะอย่างยิ่งหลังจากที่ภาพยนตร์ออกฉาย แทนที่จะรีเฟรชการค้นหาทุกครั้งที่คะแนนเปลี่ยนแปลง คุณสามารถรีเฟรชการค้นหาทุกๆ 2-3 วินาทีเพื่อรับข้อมูลอัปเดตที่แสดงผลสะสมของการกลายพันธุ์หลายรายการ

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

การเชื่อมโยง CEL ในเงื่อนไข @refresh

นิพจน์ condition ใน onMutationExecuted มีสิทธิ์เข้าถึง 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