קבלת עדכונים בזמן אמת מ-SQL Connect

קוד הלקוח יכול להירשם לשאילתות כדי לקבל עדכונים בזמן אמת כשתוצאת השאילתה משתנה.

לפני שמתחילים

  • מגדירים יצירת SDK לפרויקט כמו שמתואר במסמכי התיעוד של אינטרנט, פלטפורמות של אפל ו-Flutter.

    • חובה להפעיל שמירה במטמון בצד הלקוח בכל ערכות ה-SDK שנוצרות. בפרט, כל הגדרה של SDK צריכה לכלול הצהרה כמו זו שבהמשך:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • לקוחות האפליקציה שלך צריכים להשתמש בגרסה עדכנית של SQL Connect core SDK:

    • ‫Apple: Firebase SQL Connect SDK ל-Swift בגרסה 11.12.0 ואילך
    • אינטרנט: גרסה 12.12.0 ואילך של JavaScript SDK
    • ‫Flutter: גרסה 0.3.0 ואילךfirebase_data_connect
  • יוצרים מחדש את ערכות ה-SDK של הלקוח באמצעות גרסה 15.14.0 של Firebase CLI, או גרסה חדשה יותר.

הרשמה לתוצאות של שאילתה

אתם יכולים להירשם לשאילתה כדי להגיב לשינויים בתוצאת השאילתה. לדוגמה, נניח שהגדרתם את הסכימה והפעולות הבאות בפרויקט:

# 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, אבל רק עם קצת קושי. מומלץ להשתמש בקישורי TanStack או בתמיכה המובנית בזמן אמת של SQL Connect, אבל לא בשניהם.

שימו לב שההטמעה בזמן אמת של SQL Connect מציעה כמה יתרונות על פני הקישורים של TanStack:

  • שמירה במטמון אחרי נרמול: SQL Connect הטמעה של שמירה במטמון אחרי נרמול, שמשפרת את עקביות הנתונים ואת היעילות של הזיכרון והרשת בהשוואה לשמירה במטמון ברמת השאילתה. כשמשתמשים בשמירה במטמון עם נורמליזציה, אם יש עדכון לישות באזור אחד של האפליקציה, היא תתעדכן גם באזורים אחרים שמשתמשים באותה ישות.
  • ביטול תוקף מרחוק: SQL Connect יכול לבטל מרחוק את התוקף של ישויות במטמון בכל המכשירים הרשומים.

אם אתם בוחרים לא להשתמש ב-TanStack, אתם צריכים להסיר את ההגדרות react: true ו-angular: true מהקובץ 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

מייבאים את ה-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, ...)), כך שהשאילתה יכולה להשתמש ברענון מרומז.

פעולות של הוספה ועדכון יכולות להפעיל אותות רענון מרומזים כשמגדירים מפתח על סמך ערך ידוע, כמו 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 מקורי
  • שאילתות שמשתמשות בפותרים מותאמים אישית

יש שתי דרכים לציין מדיניות רענון:

מרווחי זמן

רענון השאילתה במרווחי זמן קבועים.

לדוגמה, נניח שיש לכם בסיס משתמשים פעיל מאוד, שיכול לגרום לכך שהדירוג המצטבר של סרט יעודכן הרבה פעמים בכל דקה, במיוחד אחרי שהסרט יוצא לאקרנים. במקום לרענן את השאילתה בכל פעם שהדירוג משתנה, אפשר לרענן את השאילתה כל כמה שניות כדי לקבל עדכונים שמשקפים את התוצאה המצטברת של כמה מוטציות פוטנציאליות.

# 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 יש גישה לשני הקשרים:

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 מזהה המשתמש שביצע את השינוי
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.