Ikuti semua informasi yang diumumkan di Firebase Summit, dan pelajari bagaimana Firebase dapat membantu Anda mempercepat pengembangan aplikasi dan menjalankan aplikasi dengan percaya diri. Pelajari Lebih Lanjut

Paket data Cloud Firestore

Tetap teratur dengan koleksi Simpan dan kategorikan konten berdasarkan preferensi Anda.

Bundel data Cloud Firestore adalah file data statis yang Anda buat dari dokumen Cloud Firestore dan snapshot kueri, dan dipublikasikan oleh Anda di CDN, layanan hosting, atau solusi lainnya. Paket data mencakup dokumen yang ingin Anda berikan ke aplikasi klien dan metadata tentang kueri yang membuatnya. Anda menggunakan SDK klien untuk mengunduh bundel melalui jaringan atau dari penyimpanan lokal, setelah itu Anda memuat data bundel ke cache lokal Cloud Firestore. Setelah bundel dimuat, aplikasi klien dapat meminta dokumen dari cache lokal atau backend.

Dengan paket data, aplikasi Anda dapat memuat hasil kueri umum lebih cepat, karena dokumen tersedia saat dimulai tanpa perlu panggilan ke backend Cloud Firestore. Jika hasil dimuat dari cache lokal, Anda juga mendapat manfaat dari pengurangan biaya akses. Alih-alih membayar satu juta instans aplikasi untuk membuat kueri 100 dokumen awal yang sama, Anda hanya membayar kueri yang diperlukan untuk menggabungkan 100 dokumen tersebut.

Paket data Cloud Firestore dibuat agar berfungsi dengan baik dengan produk backend Firebase lainnya. Lihat solusi terintegrasi di mana bundel dibuat oleh Cloud Functions dan disajikan kepada pengguna dengan Firebase Hosting.

Menggunakan bundel dengan aplikasi Anda melibatkan tiga langkah:

  1. Membangun bundel dengan Admin SDK
  2. Melayani bundel dari penyimpanan lokal atau dari CDN
  3. Memuat bundel di klien

Apa itu bundel data?

Bundel data adalah file biner statis yang Anda buat untuk mengemas satu atau lebih dokumen dan/atau snapshot kueri dan dari mana Anda dapat mengekstrak kueri bernama . Seperti yang kita bahas di bawah, SDK sisi server memungkinkan Anda membuat bundel dan SDK klien menyediakan metode untuk memungkinkan Anda memuat bundel ke cache lokal.

Kueri bernama adalah fitur bundel yang sangat kuat. Kueri bernama adalah objek Query yang dapat Anda ekstrak dari bundel, lalu langsung digunakan untuk membuat kueri data baik dari cache atau dari backend, seperti yang biasa Anda lakukan di bagian mana pun dari aplikasi Anda yang berbicara dengan Cloud Firestore.

Membangun bundel data di server

Menggunakan Node.js atau Java Admin SDK memberi Anda kontrol penuh atas apa yang harus disertakan dalam bundel dan cara menyajikannya.

Node.js
var bundleId = "latest-stories";

var bundle = firestore.bundle(bundleId);

var docSnapshot = await firestore.doc('stories/stories').get();
var querySnapshot = await firestore.collection('stories').get();

// Build the bundle
// Note how querySnapshot is named "latest-stories-query"
var bundleBuffer = bundle.add(docSnapshot); // Add a document
                   .add('latest-stories-query', querySnapshot) // Add a named query.
                   .build()
      
Jawa
Firestore db = FirestoreClient.getFirestore(app);

// Query the 50 latest stories
QuerySnapshot latestStories = db.collection("stories")
    .orderBy("timestamp", Direction.DESCENDING)
    .limit(50)
    .get()
    .get();

// Build the bundle from the query results
FirestoreBundle bundle = db.bundleBuilder("latest-stories")
    .add("latest-stories-query", latestStories)
    .build();
      
Python
from google.cloud import firestore
from google.cloud.firestore_bundle import FirestoreBundle

db = firestore.Client()
bundle = FirestoreBundle("latest-stories")

doc_snapshot = db.collection("stories").document("news-item").get()
query = db.collection("stories")._query()

# Build the bundle
# Note how `query` is named "latest-stories-query"
bundle_buffer: str = bundle.add_document(doc_snapshot).add_named_query(
    "latest-stories-query", query,
).build()
      

Melayani bundel data

Anda dapat menyajikan bundel ke aplikasi klien Anda dari CDN atau dengan mengunduhnya dari, misalnya, Cloud Storage.

Asumsikan bundel yang dibuat di bagian sebelumnya telah disimpan ke file bernama bundle.txt dan diposting di server. File bundel ini seperti aset lain yang dapat Anda layani melalui web, seperti yang ditunjukkan di sini untuk aplikasi Node.js Express sederhana.

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
  const src = fs.createReadStream('./bundle.txt');
  src.pipe(res);
});

server.listen(8000);

Memuat bundel data di klien

Anda memuat bundel Firestore dengan mengambilnya dari server jarak jauh, baik dengan membuat permintaan HTTP, memanggil API penyimpanan, atau menggunakan teknik lain untuk mengambil file biner di jaringan.

Setelah diambil, menggunakan SDK klien Cloud Firestore, aplikasi Anda memanggil metode loadBundle , yang mengembalikan objek pelacakan tugas, penyelesaiannya dapat Anda pantau sebanyak Anda memantau status Promise. Pada penyelesaian tugas pemuatan bundel yang berhasil, konten bundel tersedia di cache lokal.

Web
// If you are using module bundlers.
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/firestore/bundle"; // This line enables bundle loading as a side effect.

// ...

async function fetchFromBundle() {
  // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'
  // response header will be set to 'HIT'
  const resp = await fetch('/createBundle');

  // Load the bundle contents into the Firestore SDK
  await db.loadBundle(resp.body);

  // Query the results from the cache
  // Note: omitting "source: cache" will query the Firestore backend.
  const query = await db.namedQuery('latest-stories-query');
  const storiesSnap = await query.get({ source: 'cache' });

  // Use the results
  // ...
}
Cepat
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
// Utility function for errors when loading bundles.
func bundleLoadError(reason: String) -> NSError {
  return NSError(domain: "FIRSampleErrorDomain",
                 code: 0,
                 userInfo: [NSLocalizedFailureReasonErrorKey: reason])
}

// Loads a remote bundle from the provided url.
func fetchRemoteBundle(for firestore: Firestore,
                       from url: URL,
                       completion: @escaping ((Result<LoadBundleTaskProgress, Error>) -> ())) {
  guard let inputStream = InputStream(url: url) else {
    let error = self.bundleLoadError(reason: "Unable to create stream from the given url: \(url)")
    completion(.failure(error))
    return
  }

  // The return value of this function is ignored, but can be used for more granular
  // bundle load observation.
  let _ = firestore.loadBundle(inputStream) { (progress, error) in
    switch (progress, error) {

    case (.some(let value), .none):
      if value.state == .success {
        completion(.success(value))
      } else {
        let concreteError = self.bundleLoadError(
          reason: "Expected bundle load to be completed, but got \(value.state) instead"
        )
        completion(.failure(concreteError))
      }

    case (.none, .some(let concreteError)):
      completion(.failure(concreteError))

    case (.none, .none):
      let concreteError = self.bundleLoadError(reason: "Operation failed, but returned no error.")
      completion(.failure(concreteError))

    case (.some(let value), .some(let concreteError)):
      let concreteError = self.bundleLoadError(
        reason: "Operation returned error \(concreteError) with nonnull progress: \(value)"
      )
      completion(.failure(concreteError))
    }
  }
}

// Fetches a specific named query from the provided bundle.
func loadQuery(named queryName: String,
               fromRemoteBundle bundleURL: URL,
               with store: Firestore,
               completion: @escaping ((Result<Query, Error>) -> ())) {
  fetchRemoteBundle(for: store,
                    from: bundleURL) { (result) in
    switch result {
    case .success:
      store.getQuery(named: queryName) { query in
        if let query = query {
          completion(.success(query))
        } else {
          completion(
            .failure(
              self.bundleLoadError(reason: "Could not find query named \(queryName)")
            )
          )
        }
      }

    case .failure(let error):
      completion(.failure(error))
    }
  }
}

// Load a query and fetch its results from a bundle.
func runStoriesQuery() {
  let queryName = "latest-stories-query"
  let firestore = Firestore.firestore()
  let remoteBundle = URL(string: "https://example.com/createBundle")!
  loadQuery(named: queryName,
            fromRemoteBundle: remoteBundle,
            with: firestore) { (result) in
    switch result {
    case .failure(let error):
      print(error)

    case .success(let query):
      query.getDocuments { (snapshot, error) in

        // handle query results

      }
    }
  }
}
Objective-C
Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
// Utility function for errors when loading bundles.
- (NSError *)bundleLoadErrorWithReason:(NSString *)reason {
  return [NSError errorWithDomain:@"FIRSampleErrorDomain"
                             code:0
                         userInfo:@{NSLocalizedFailureReasonErrorKey: reason}];
}

// Loads a remote bundle from the provided url.
- (void)fetchRemoteBundleForFirestore:(FIRFirestore *)firestore
                              fromURL:(NSURL *)url
                           completion:(void (^)(FIRLoadBundleTaskProgress *_Nullable,
                                                NSError *_Nullable))completion {
  NSInputStream *inputStream = [NSInputStream inputStreamWithURL:url];
  if (inputStream == nil) {
    // Unable to create input stream.
    NSError *error =
        [self bundleLoadErrorWithReason:
            [NSString stringWithFormat:@"Unable to create stream from the given url: %@", url]];
    completion(nil, error);
    return;
  }

  [firestore loadBundleStream:inputStream
                   completion:^(FIRLoadBundleTaskProgress * _Nullable progress,
                                NSError * _Nullable error) {
    if (progress == nil) {
      completion(nil, error);
      return;
    }

    if (progress.state == FIRLoadBundleTaskStateSuccess) {
      completion(progress, nil);
    } else {
      NSError *concreteError =
          [self bundleLoadErrorWithReason:
              [NSString stringWithFormat:
                  @"Expected bundle load to be completed, but got %ld instead",
                  (long)progress.state]];
      completion(nil, concreteError);
    }
    completion(nil, nil);
  }];
}

// Loads a bundled query.
- (void)loadQueryNamed:(NSString *)queryName
   fromRemoteBundleURL:(NSURL *)url
         withFirestore:(FIRFirestore *)firestore
            completion:(void (^)(FIRQuery *_Nullable, NSError *_Nullable))completion {
  [self fetchRemoteBundleForFirestore:firestore
                              fromURL:url
                           completion:^(FIRLoadBundleTaskProgress *progress, NSError *error) {
    if (error != nil) {
      completion(nil, error);
      return;
    }

    [firestore getQueryNamed:queryName completion:^(FIRQuery *query) {
      if (query == nil) {
        NSString *errorReason =
            [NSString stringWithFormat:@"Could not find query named %@", queryName];
        NSError *error = [self bundleLoadErrorWithReason:errorReason];
        completion(nil, error);
        return;
      }
      completion(query, nil);
    }];
  }];
}

- (void)runStoriesQuery {
  NSString *queryName = @"latest-stories-query";
  FIRFirestore *firestore = [FIRFirestore firestore];
  NSURL *bundleURL = [NSURL URLWithString:@"https://example.com/createBundle"];
  [self loadQueryNamed:queryName
   fromRemoteBundleURL:bundleURL
         withFirestore:firestore
            completion:^(FIRQuery *query, NSError *error) {
    // Handle query results
  }];
}

Java

public InputStream getBundleStream(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    return connection.getInputStream();
}

public void fetchBundleFrom() throws IOException {
    final InputStream bundleStream = getBundleStream("https://example.com/createBundle");
    LoadBundleTask loadTask = db.loadBundle(bundleStream);

    // Chain the following tasks
    // 1) Load the bundle
    // 2) Get the named query from the local cache
    // 3) Execute a get() on the named query
    loadTask.continueWithTask(new Continuation<LoadBundleTaskProgress, Task<Query>>() {
        @Override
        public Task<Query> then(@NonNull Task<LoadBundleTaskProgress> task) throws Exception {
            // Close the stream
            bundleStream.close();

            // Calling getResult() propagates errors
            LoadBundleTaskProgress progress = task.getResult(Exception.class);

            // Get the named query from the bundle cache
            return db.getNamedQuery("latest-stories-query");
        }
    }).continueWithTask(new Continuation<Query, Task<QuerySnapshot>>() {
        @Override
        public Task<QuerySnapshot> then(@NonNull Task<Query> task) throws Exception {
            Query query = task.getResult(Exception.class);

            // get() the query results from the cache
            return query.get(Source.CACHE);
        }
    }).addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
        @Override
        public void onComplete(@NonNull Task<QuerySnapshot> task) {
            if (!task.isSuccessful()) {
                Log.w(TAG, "Bundle loading failed", task.getException());
                return;
            }

            // Get the QuerySnapshot from the bundle
            QuerySnapshot storiesSnap = task.getResult();

            // Use the results
            // ...
        }
    });
}

Kotlin+KTX

@Throws(IOException::class)
fun getBundleStream(urlString: String?): InputStream {
    val url = URL(urlString)
    val connection = url.openConnection() as HttpURLConnection
    return connection.inputStream
}

@Throws(IOException::class)
fun fetchFromBundle() {
    val bundleStream = getBundleStream("https://example.com/createBundle")
    val loadTask = db.loadBundle(bundleStream)

    // Chain the following tasks
    // 1) Load the bundle
    // 2) Get the named query from the local cache
    // 3) Execute a get() on the named query
    loadTask.continueWithTask<Query> { task ->
        // Close the stream
        bundleStream.close()

        // Calling .result propagates errors
        val progress = task.getResult(Exception::class.java)

        // Get the named query from the bundle cache
        db.getNamedQuery("latest-stories-query")
    }.continueWithTask { task ->
        val query = task.getResult(Exception::class.java)!!

        // get() the query results from the cache
        query.get(Source.CACHE)
    }.addOnCompleteListener { task ->
        if (!task.isSuccessful) {
            Log.w(TAG, "Bundle loading failed", task.exception)
            return@addOnCompleteListener
        }

        // Get the QuerySnapshot from the bundle
        val storiesSnap = task.result

        // Use the results
        // ...
    }
}
C++
db->LoadBundle("bundle_name", [](const LoadBundleTaskProgress& progress) {
  switch(progress.state()) {
    case LoadBundleTaskProgress::State::kError: {
      // The bundle load has errored. Handle the error in the returned future.
      return;
    }
    case LoadBundleTaskProgress::State::kInProgress: {
      std::cout << "Bytes loaded from bundle: " << progress.bytes_loaded()
                << std::endl;
      break;
    }
    case LoadBundleTaskProgress::State::kSuccess: {
      std::cout << "Bundle load succeeeded" << std::endl;
      break;
    }
  }
}).OnCompletion([db](const Future<LoadBundleTaskProgress>& future) {
  if (future.error() != Error::kErrorOk) {
    // Handle error...
    return;
  }

  const std::string& query_name = "latest_stories_query";
  db->NamedQuery(query_name).OnCompletion([](const Future<Query>& query_future){
    if (query_future.error() != Error::kErrorOk) {
      // Handle error...
      return;
    }

    const Query* query = query_future.result();
    query->Get().OnCompletion([](const Future<QuerySnapshot> &){
      // ...
    });
  });
});

Perhatikan bahwa jika Anda memuat kueri bernama dari bundel yang dibuat kurang dari 30 menit sebelumnya, setelah Anda menggunakannya untuk membaca dari backend daripada cache, Anda hanya akan membayar untuk pembacaan basis data yang diperlukan untuk memperbarui dokumen agar sesuai dengan apa yang disimpan di backend ; yaitu, Anda hanya membayar untuk delta.

Apa selanjutnya?