SQL Connect से रीयल-टाइम अपडेट पाना

क्लाइंट कोड, क्वेरी की सदस्यता ले सकता है, ताकि क्वेरी के नतीजे में बदलाव होने पर, उसे रीयल-टाइम अपडेट मिल सकें.

शुरू करने से पहले

  • Apple प्लैटफ़ॉर्म, Android, वेब, और Flutter के लिए दस्तावेज़ में बताए गए तरीके से, अपने प्रोजेक्ट के लिए एसडीके जनरेट करने की सुविधा सेट अप करें.

    • जनरेट किए गए सभी एसडीके के लिए, क्लाइंट-साइड कैशिंग की सुविधा चालू करें. खास तौर पर, हर एसडीके कॉन्फ़िगरेशन में, यहां दिया गया एलान शामिल होना चाहिए:
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • आपके ऐप्लिकेशन के क्लाइंट, SQL Connect कोर एसडीके के नए वर्शन का इस्तेमाल करते हों:

    • Apple: Swift के लिए Firebase SQL Connect SDK का 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 या इसके बाद का वर्शन
  • 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 TanStack का इस्तेमाल करके React या Angular के लिए बाइंडिंग जनरेट करता है.

ये बाइंडिंग, SQL Connect's की रीयल-टाइम में काम करने की इन-बिल्ट सुविधा के साथ काम कर सकती हैं. हालांकि, इसमें कुछ समस्याएं आ सकती हैं. हमारा सुझाव है कि TanStack पर आधारित बाइंडिंग या SQL Connect's की रीयल-टाइम में काम करने की इन-बिल्ट सुविधा में से किसी एक का इस्तेमाल करें. दोनों का नहीं.

ध्यान दें कि SQL Connect's के रीयल-टाइम में काम करने की इन-बिल्ट सुविधा के, TanStack बाइंडिंग के मुकाबले कुछ फ़ायदे हैं:

  • नॉर्मलाइज़्ड कैशिंग: SQL Connect में नॉर्मलाइज़्ड कैशिंग की सुविधा है, जिससे क्वेरी-लेवल कैशिंग के मुकाबले, डेटा की सुसंगतता के साथ-साथ मेमोरी और नेटवर्क की परफ़ॉर्मेंस भी बेहतर होती है. नॉर्मलाइज़्ड कैशिंग की मदद से, अगर आपके ऐप्लिकेशन के किसी हिस्से में कोई इकाई अपडेट होती है, तो वह इकाई, ऐप्लिकेशन के उन अन्य हिस्सों में भी अपडेट हो जाएगी जहां उसका इस्तेमाल किया जाता है.
  • रिमोट इनवैलिडेशन: SQL Connect सदस्यता लेने वाले सभी डिवाइसों पर, कैश की गई   इकाइयों को रिमोट तरीके से इनवैलिडेट कर सकता है.

अगर आपको TanStack का इस्तेमाल नहीं करना है, तो connector.yaml फ़ाइल से react: true और angular: true सेटिंग हटा दें.

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

अपने प्रोजेक्ट का जनरेट किया गया एसडीके इंपोर्ट करें:

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 डायरेक्टिव का इस्तेमाल करना तब ज़रूरी है, जब आपकी क्वेरी, अपने-आप रीफ़्रेश होने की ज़रूरी शर्तें (ऊपर देखें) पूरी नहीं करती हैं. यहां कुछ ऐसी क्वेरी के उदाहरण दिए गए हैं जिनमें इस डायरेक्टिव को शामिल करना ज़रूरी है:

  • इकाइयों की सूची पाने वाली क्वेरी
  • अन्य टेबल पर जॉइन करने वाली क्वेरी
  • एग्रीगेशन क्वेरी
  • नेटिव एसक्यूएल का इस्तेमाल करने वाली क्वेरी
  • कस्टम रिज़ॉल्वर का इस्तेमाल करने वाली क्वेरी

रीफ़्रेश करने की नीति को इन दो तरीकों से तय किया जा सकता है:

समय के हिसाब से तय किए गए अंतराल

क्वेरी को तय किए गए समय अंतराल पर रीफ़्रेश करें.

उदाहरण के लिए, मान लें कि आपके ऐप्लिकेशन का इस्तेमाल करने वाले लोग, किसी मूवी की रेटिंग को हर मिनट में कई बार अपडेट कर सकते हैं. ऐसा खास तौर पर, मूवी रिलीज़ होने के बाद होता है. रेटिंग में बदलाव होने पर, क्वेरी को रीफ़्रेश करने के बजाय, क्वेरी को हर कुछ सेकंड में रीफ़्रेश किया जा सकता है. इससे, संभावित तौर पर कई म्यूटेशन के कुल नतीजे दिखाने वाले अपडेट मिलेंगे.

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

इसके अलावा, सीईएल एक्सप्रेशन की कोई ऐसी शर्त भी तय की जा सकती है जिसके पूरा होने पर, म्यूटेशन से क्वेरी को रीफ़्रेश किया जा सके.

हमारा सुझाव है कि ऐसा किया जाए. शर्त तय करते समय, जितनी सटीक जानकारी दी जाएगी, डेटाबेस के उतने ही कम संसाधनों का इस्तेमाल होगा. साथ ही, आपका ऐप्लिकेशन उतना ही ज़्यादा रिस्पॉन्सिव होगा.

उदाहरण के लिए, मान लें कि आपके पास एक ऐसी क्वेरी है जो सिर्फ़ किसी तय किए गए जॉनर की मूवी की सूची दिखाती है. इस क्वेरी को सिर्फ़ तब रीफ़्रेश किया जाना चाहिए, जब कोई म्यूटेशन, उसी जॉनर की किसी मूवी को अपडेट करता है:

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

@refresh की शर्तों में सीईएल बाइंडिंग

onMutationExecuted में मौजूद condition एक्सप्रेशन के पास दो कॉन्टेक्स्ट का ऐक्सेस होता है:

request

उस क्वेरी की स्थिति जिसकी सदस्यता ली गई है.

बाइंडिंग ब्यौरा
request.variables क्वेरी को पास किए गए वैरिएबल (उदाहरण के लिए, request.variables.id)
request.auth.uid Firebase Authentication क्वेरी को एक्ज़ीक्यूट करने वाले उपयोगकर्ता का यूआईडी
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 डायरेक्टिव का रेफ़रंस देखें.