获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

Paquetes de datos de Cloud Firestore

Los paquetes de datos de Cloud Firestore son archivos de datos estáticos creados por usted a partir de instantáneas de consultas y documentos de Cloud Firestore, y publicados por usted en una CDN, servicio de alojamiento u otra solución. Los paquetes de datos incluyen tanto los documentos que desea proporcionar a sus aplicaciones cliente como los metadatos sobre las consultas que los generaron. Utiliza los SDK del cliente para descargar paquetes a través de la red o desde el almacenamiento local, luego de lo cual carga los datos del paquete en el caché local de Cloud Firestore. Una vez que se carga un paquete, una aplicación cliente puede consultar documentos desde el caché local o el backend.

Con paquetes de datos, sus aplicaciones pueden cargar los resultados de consultas comunes antes, ya que los documentos están disponibles al inicio sin necesidad de llamadas al backend de Cloud Firestore. Si los resultados se cargan desde la caché local, también se beneficia de costos de acceso reducidos. En lugar de pagar por un millón de instancias de aplicaciones para consultar los mismos 100 documentos iniciales, solo paga por las consultas necesarias para agrupar esos 100 documentos.

Los paquetes de datos de Cloud Firestore están diseñados para funcionar bien con otros productos backend de Firebase. Eche un vistazo a una solución integrada en la que Cloud Functions crea paquetes y los ofrece a los usuarios con Firebase Hosting.

El uso de un paquete con su aplicación implica tres pasos:

  1. Creación del paquete con el SDK de administrador
  2. Sirviendo el paquete desde el almacenamiento local o desde un CDN
  3. Cargando paquetes en el cliente

¿Qué es un paquete de datos?

Un paquete de datos es un archivo binario estático creado por usted para empaquetar uno o más documentos y/o instantáneas de consultas y del cual puede extraer consultas con nombre . Como analizamos a continuación, los SDK del lado del servidor le permiten crear paquetes y los SDK de cliente proporcionan métodos que le permiten cargar paquetes en la memoria caché local.

Las consultas con nombre son una característica especialmente poderosa de los paquetes. Las consultas con nombre son objetos de Query que puede extraer de un paquete y luego usar de inmediato para consultar datos desde la memoria caché o desde el backend, como lo hace normalmente en cualquier parte de su aplicación que se comunique con Cloud Firestore.

Creación de paquetes de datos en el servidor

El uso de Node.js o Java Admin SDK le brinda control total sobre qué incluir en los paquetes y cómo servirlos.

Nodo.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()
      
Java
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();
      
Pitón
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()
      

Paquetes de datos de servicio

Puede entregar paquetes a sus aplicaciones cliente desde una CDN o descargándolos, por ejemplo, de Cloud Storage.

Suponga que el paquete creado en la sección anterior se guardó en un archivo llamado bundle.txt y se publicó en un servidor. Este archivo de paquete es como cualquier otro activo que puede servir en la web, como se muestra aquí para una aplicación simple de Node.js Express.

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

Cargando paquetes de datos en el cliente

Los paquetes de Firestore se cargan al obtenerlos de un servidor remoto, ya sea mediante una solicitud HTTP, llamando a una API de almacenamiento o utilizando cualquier otra técnica para obtener archivos binarios en una red.

Una vez obtenida, con el SDK del cliente de Cloud Firestore, su aplicación llama al método loadBundle , que devuelve un objeto de seguimiento de tareas, cuya finalización puede monitorear tanto como monitorea el estado de una Promesa. Al completar con éxito la tarea de carga del paquete, el contenido del paquete está disponible en la memoria caché local.

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
  // ...
}
Rápido
Nota: este producto no está disponible en los objetivos watchOS y 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

      }
    }
  }
}
C objetivo
Nota: este producto no está disponible en los objetivos watchOS y 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> &){
      // ...
    });
  });
});

Tenga en cuenta que si carga una consulta con nombre de un paquete creado menos de 30 minutos antes, una vez que la use para leer desde el backend en lugar de la memoria caché, solo pagará las lecturas de la base de datos necesarias para actualizar los documentos para que coincidan con lo que está almacenado en el backend ; es decir, solo paga por los deltas.

¿Qué sigue?