Nhận thông tin cập nhật theo thời gian thực từ SQL Connect

Mã ứng dụng có thể đăng ký các truy vấn để nhận thông tin cập nhật theo thời gian thực khi kết quả của truy vấn thay đổi.

Trước khi bắt đầu

  • Thiết lập quá trình tạo SDK cho dự án của bạn theo mô tả trong tài liệu dành cho web, các nền tảng của AppleFlutter.

    • Bạn phải bật tính năng lưu vào bộ nhớ đệm phía máy khách cho tất cả SDK đã tạo. Cụ thể, mọi cấu hình SDK đều phải chứa một nội dung khai báo như sau:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • Các ứng dụng khách của bạn phải sử dụng phiên bản mới của SDK cốt lõi SQL Connect:

    • Apple: Firebase SQL Connect SDK cho Swift phiên bản 11.12.0 trở lên
    • Web: SDK JavaScript phiên bản 12.12.0 trở lên
    • Flutter: firebase_data_connect phiên bản 0.3.0 trở lên
  • Tạo lại SDK ứng dụng bằng Firebase CLI phiên bản 15.14.0 trở lên.

Đăng ký nhận kết quả truy vấn

Bạn có thể đăng ký một truy vấn để phản hồi các thay đổi trong kết quả truy vấn. Ví dụ: giả sử bạn có giản đồ và các thao tác sau được xác định trong dự án của mình:

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

Cách đăng ký nhận thông báo về các thay đổi trong kết quả chạy 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 cũng hỗ trợ lưu vào bộ nhớ đệm và đăng ký theo thời gian thực bằng TanStack. Khi bạn chỉ định react: true hoặc angular: true trong tệp connector.yaml, SQL Connect sẽ tạo các liên kết cho React hoặc Angular bằng TanStack.

Các liên kết này có thể hoạt động cùng với tính năng hỗ trợ theo thời gian thực tích hợp của SQL Connect, nhưng chỉ với một số khó khăn. Bạn nên sử dụng các liên kết dựa trên TanStack hoặc chế độ hỗ trợ theo thời gian thực tích hợp của SQL Connect, nhưng không nên dùng cả hai.

Xin lưu ý rằng việc triển khai theo thời gian thực của riêng SQL Connect có một số lợi thế so với các liên kết TanStack:

  • Lưu vào bộ nhớ đệm được chuẩn hoá: SQL Connect triển khai lưu vào bộ nhớ đệm được chuẩn hoá, giúp cải thiện tính nhất quán của dữ liệu cũng như hiệu quả sử dụng bộ nhớ và mạng so với tính năng lưu vào bộ nhớ đệm ở cấp truy vấn. Với tính năng lưu vào bộ nhớ đệm được chuẩn hoá, nếu một thực thể cập nhật ở một khu vực trong ứng dụng của bạn, thì thực thể đó cũng sẽ cập nhật ở những khu vực khác sử dụng thực thể đó.
  • Huỷ hiệu lực từ xa: SQL Connect có thể huỷ hiệu lực từ xa các thực thể được lưu vào bộ nhớ đệm trên tất cả các thiết bị đã đăng ký.

Nếu chọn không sử dụng TanStack, bạn nên xoá các chế độ cài đặt react: trueangular: true khỏi tệp 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

Nhập SDK đã tạo của dự án:

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

Sau đó, gọi phương thức subscribe() trên một tham chiếu truy vấn:

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

Để ngừng cập nhật, bạn có thể gọi subscription.cancel().

Sau khi đăng ký nhận thông báo về truy vấn như trong ví dụ trước, bạn sẽ nhận được thông tin cập nhật bất cứ khi nào kết quả của truy vấn cụ thể thay đổi. Ví dụ: nếu một ứng dụng khác thực thi đột biến UpdateMovie trên cùng một mã nhận dạng mà bạn đã đăng ký, thì bạn sẽ nhận được thông tin cập nhật.

Tín hiệu làm mới truy vấn ngầm

Trong ví dụ trước, bạn có thể đăng ký một truy vấn và nhận thông tin cập nhật theo thời gian thực mà không cần sửa đổi thêm bất kỳ thao tác nào. Cụ thể, bạn không cần chỉ định rằng đột biến UpdateMovie có thể ảnh hưởng đến kết quả của truy vấn GetMovieById.

Điều này có thể xảy ra vì truy vấn GetMovieById sẽ nhận được tín hiệu làm mới một cách ngầm định từ đột biến UpdateMovie. Các tín hiệu làm mới ngầm được gửi giữa một nhóm nhỏ các truy vấn và đột biến mà bạn có thể viết:

Nếu truy vấn của bạn thực hiện một lần tra cứu thực thể theo khoá chính, thì mọi hoạt động đột biến ghi vào cùng một thực thể (cũng được xác định bằng khoá chính) sẽ ngầm kích hoạt tín hiệu làm mới.

  • _insert_insertMany
  • _upsert_upsertMany
  • _update
  • _delete

_deleteMany_updateMany không gửi tín hiệu làm mới.

Trong ví dụ trước, truy vấn GetMovieById sẽ tra cứu một bộ phim theo mã nhận dạng (movie(id: $id)) và thao tác đột biến UpdateMovie sẽ cập nhật một bộ phim theo mã nhận dạng (movie_update(id: $id, ...)). Do đó, truy vấn có thể tận dụng tính năng làm mới ngầm.

Các thao tác chèn và chèn/cập nhật có thể kích hoạt các tín hiệu làm mới ngầm khi bạn đang khoá một giá trị đã biết, chẳng hạn như UID của người dùng Firebase Authentication.

Ví dụ: hãy xem xét một truy vấn như sau:

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

Truy vấn sẽ nhận được tín hiệu làm mới một cách ngầm định từ một đột biến như sau:

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

Khi các truy vấn hoặc đột biến của bạn phức tạp hơn, bạn sẽ cần chỉ định các điều kiện yêu cầu làm mới truy vấn. Hãy chuyển sang phần tiếp theo để tìm hiểu cách thực hiện.

Tín hiệu làm mới truy vấn rõ ràng

Ngoài các tín hiệu làm mới được gửi ngầm bằng các đột biến đến các truy vấn, bạn cũng có thể chỉ định rõ ràng thời điểm một truy vấn sẽ nhận được tín hiệu làm mới. Bạn thực hiện việc này bằng cách chú thích các truy vấn bằng chỉ thị @refresh.

Bạn phải sử dụng chỉ thị @refresh bất cứ khi nào các truy vấn của bạn không đáp ứng tiêu chí cụ thể (xem ở trên) để tự động làm mới. Sau đây là một số ví dụ về những cụm từ tìm kiếm phải có chỉ thị này:

  • Các truy vấn truy xuất danh sách thực thể
  • Các truy vấn thực hiện thao tác kết hợp trên các bảng khác
  • Truy vấn tổng hợp
  • Truy vấn bằng SQL gốc
  • Truy vấn bằng trình phân giải tuỳ chỉnh

Bạn có thể chỉ định chính sách làm mới theo hai cách:

Khoảng thời gian

Làm mới truy vấn theo một khoảng thời gian cố định.

Ví dụ: giả sử cơ sở người dùng rất tích cực của bạn có thể khiến điểm xếp hạng tích luỹ của một bộ phim được cập nhật nhiều lần mỗi phút, đặc biệt là sau khi bộ phim đó phát hành. Thay vì làm mới truy vấn mỗi khi điểm xếp hạng thay đổi, bạn có thể làm mới truy vấn sau mỗi vài giây để nhận được thông tin cập nhật phản ánh kết quả tích luỹ của một số đột biến có thể xảy ra.

# dataconnect/connector/operations.gql

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

Thực thi đột biến

Làm mới truy vấn khi một đột biến cụ thể được thực thi. Cách tiếp cận này cho biết rõ những đột biến có khả năng thay đổi kết quả của truy vấn.

Ví dụ: giả sử bạn có một truy vấn truy xuất thông tin về nhiều bộ phim thay vì một bộ phim cụ thể. Truy vấn này sẽ làm mới bất cứ khi nào một đột biến đã cập nhật bất kỳ bản ghi phim nào.

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

Bạn cũng có thể chỉ định một điều kiện biểu thức CEL phải được đáp ứng để đột biến kích hoạt việc làm mới truy vấn.

Bạn nên làm việc này. Bạn càng có thể chỉ định điều kiện một cách chính xác thì càng ít tài nguyên cơ sở dữ liệu không cần thiết bị tiêu thụ và ứng dụng của bạn sẽ càng phản hồi nhanh hơn.

Ví dụ: giả sử bạn có một truy vấn chỉ liệt kê các bộ phim thuộc một thể loại cụ thể. Truy vấn này chỉ nên làm mới khi một đột biến cập nhật một bộ phim thuộc cùng thể loại:

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

Các liên kết CEL trong điều kiện @refresh

Biểu thức condition trong onMutationExecuted có quyền truy cập vào hai ngữ cảnh:

request

Trạng thái của truy vấn đang được đăng ký.

Ép giấy tờ Mô tả
request.variables Các biến được truyền đến truy vấn (ví dụ: request.variables.id)
request.auth.uid Firebase Authentication UID của người dùng đã thực hiện truy vấn
request.auth.token Từ điển về các yêu cầu đối với mã thông báo Firebase Authentication cho người dùng đã thực hiện truy vấn
mutation

Trạng thái của thao tác đột biến đã thực thi.

Ép giấy tờ Mô tả
mutation.variables Các biến được truyền đến đột biến (ví dụ: mutation.variables.movieId)
mutation.auth.uid Firebase Authentication UID của người dùng đã thực hiện thao tác biến đổi
mutation.auth.token Từ điển về các yêu cầu Firebase Authentication đối với mã thông báo của người dùng đã thực hiện đột biến
Các mẫu phổ biến
# 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"

Nhiều chỉ thị @refresh

Bạn có thể chỉ định chỉ thị @refresh nhiều lần trên một truy vấn để kích hoạt quá trình làm mới bất cứ khi nào một trong các tiêu chí do một trong các chỉ thị @refresh chỉ định được đáp ứng.

Ví dụ: truy vấn sau đây sẽ làm mới sau mỗi 30 giây cũng như bất cứ khi nào một trong các đột biến được chỉ định được thực thi:

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

Tài liệu tham khảo

Hãy xem tài liệu tham khảo về chỉ thị @refresh để biết thêm ví dụ.