Las consultas en Cloud Firestore te permiten buscar documentos en grandes colecciones. Para obtener información sobre las propiedades de la colección en su conjunto, puede agregar datos sobre una colección.
Puede agregar datos en tiempo de lectura o en tiempo de escritura:
Las agregaciones en tiempo de lectura calculan un resultado en el momento de la solicitud. Cloud Firestore admite consultas de agregación
count()
,sum()
yaverage()
en tiempo de lectura. Las consultas de agregación en tiempo de lectura son más fáciles de agregar a su aplicación que las agregaciones en tiempo de escritura. Para obtener más información sobre consultas de agregación, consulte Resumir datos con consultas de agregación .Las agregaciones de tiempo de escritura calculan un resultado cada vez que la aplicación realiza una operación de escritura relevante. Las agregaciones en tiempo de escritura requieren más trabajo de implementar, pero puede usarlas en lugar de agregaciones en tiempo de lectura por una de las siguientes razones:
- Quiere escuchar el resultado de la agregación para obtener actualizaciones en tiempo real. Las consultas de agregación
count()
,sum()
yaverage()
no admiten actualizaciones en tiempo real. - Quiere almacenar el resultado de la agregación en una caché del lado del cliente. Las consultas de agregación
count()
,sum()
yaverage()
no admiten el almacenamiento en caché. - Está agregando datos de decenas de miles de documentos para cada uno de sus usuarios y considerando los costos. Con una cantidad menor de documentos, las agregaciones en tiempo de lectura cuestan menos. Para una gran cantidad de documentos en una agregación, las agregaciones en tiempo de escritura pueden costar menos.
- Quiere escuchar el resultado de la agregación para obtener actualizaciones en tiempo real. Las consultas de agregación
Puede implementar una agregación en tiempo de escritura mediante una transacción del lado del cliente o con Cloud Functions. Las siguientes secciones describen cómo implementar agregaciones en tiempo de escritura.
Solución: agregación en tiempo de escritura con una transacción del lado del cliente
Considere una aplicación de recomendaciones locales que ayude a los usuarios a encontrar excelentes restaurantes. La siguiente consulta recupera todas las calificaciones de un restaurante determinado:
Web
db.collection("restaurants") .doc("arinell-pizza") .collection("ratings") .get();
Rápido
do { let snapshot = try await db.collection("restaurants") .document("arinell-pizza") .collection("ratings") .getDocuments() print(snapshot) } catch { print(error) }
C objetivo
FIRQuery *query = [[[self.db collectionWithPath:@"restaurants"] documentWithPath:@"arinell-pizza"] collectionWithPath:@"ratings"]; [query getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) { // ... }];
Kotlin+KTX
db.collection("restaurants") .document("arinell-pizza") .collection("ratings") .get()
Java
db.collection("restaurants") .document("arinell-pizza") .collection("ratings") .get();
En lugar de obtener todas las calificaciones y luego calcular la información agregada, podemos almacenar esta información en el documento del restaurante:
Web
var arinellDoc = { name: 'Arinell Pizza', avgRating: 4.65, numRatings: 683 };
Rápido
struct Restaurant { let name: String let avgRating: Float let numRatings: Int } let arinell = Restaurant(name: "Arinell Pizza", avgRating: 4.65, numRatings: 683)
C objetivo
@interface FIRRestaurant : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) float averageRating; @property (nonatomic, readonly) NSInteger ratingCount; - (instancetype)initWithName:(NSString *)name averageRating:(float)averageRating ratingCount:(NSInteger)ratingCount; @end @implementation FIRRestaurant - (instancetype)initWithName:(NSString *)name averageRating:(float)averageRating ratingCount:(NSInteger)ratingCount { self = [super init]; if (self != nil) { _name = name; _averageRating = averageRating; _ratingCount = ratingCount; } return self; } @end
Kotlin+KTX
data class Restaurant( // default values required for use with "toObject" internal var name: String = "", internal var avgRating: Double = 0.0, internal var numRatings: Int = 0, )
val arinell = Restaurant("Arinell Pizza", 4.65, 683)
Java
public class Restaurant { String name; double avgRating; int numRatings; public Restaurant(String name, double avgRating, int numRatings) { this.name = name; this.avgRating = avgRating; this.numRatings = numRatings; } }
Restaurant arinell = new Restaurant("Arinell Pizza", 4.65, 683);
Para mantener la coherencia de estas agregaciones, se deben actualizar cada vez que se agrega una nueva calificación a la subcolección. Una forma de lograr coherencia es realizar la adición y la actualización en una sola transacción:
Web
function addRating(restaurantRef, rating) { // Create a reference for a new rating, for use inside the transaction var ratingRef = restaurantRef.collection('ratings').doc(); // In a transaction, add the new rating and update the aggregate totals return db.runTransaction((transaction) => { return transaction.get(restaurantRef).then((res) => { if (!res.exists) { throw "Document does not exist!"; } // Compute new number of ratings var newNumRatings = res.data().numRatings + 1; // Compute new average rating var oldRatingTotal = res.data().avgRating * res.data().numRatings; var newAvgRating = (oldRatingTotal + rating) / newNumRatings; // Commit to Firestore transaction.update(restaurantRef, { numRatings: newNumRatings, avgRating: newAvgRating }); transaction.set(ratingRef, { rating: rating }); }); }); }
Rápido
func addRatingTransaction(restaurantRef: DocumentReference, rating: Float) async { let ratingRef: DocumentReference = restaurantRef.collection("ratings").document() do { let _ = try await db.runTransaction({ (transaction, errorPointer) -> Any? in do { let restaurantDocument = try transaction.getDocument(restaurantRef).data() guard var restaurantData = restaurantDocument else { return nil } // Compute new number of ratings let numRatings = restaurantData["numRatings"] as! Int let newNumRatings = numRatings + 1 // Compute new average rating let avgRating = restaurantData["avgRating"] as! Float let oldRatingTotal = avgRating * Float(numRatings) let newAvgRating = (oldRatingTotal + rating) / Float(newNumRatings) // Set new restaurant info restaurantData["numRatings"] = newNumRatings restaurantData["avgRating"] = newAvgRating // Commit to Firestore transaction.setData(restaurantData, forDocument: restaurantRef) transaction.setData(["rating": rating], forDocument: ratingRef) } catch { // Error getting restaurant data // ... } return nil }) } catch { // ... } }
C objetivo
- (void)addRatingTransactionWithRestaurantReference:(FIRDocumentReference *)restaurant rating:(float)rating { FIRDocumentReference *ratingReference = [[restaurant collectionWithPath:@"ratings"] documentWithAutoID]; [self.db runTransactionWithBlock:^id (FIRTransaction *transaction, NSError **errorPointer) { FIRDocumentSnapshot *restaurantSnapshot = [transaction getDocument:restaurant error:errorPointer]; if (restaurantSnapshot == nil) { return nil; } NSMutableDictionary *restaurantData = [restaurantSnapshot.data mutableCopy]; if (restaurantData == nil) { return nil; } // Compute new number of ratings NSInteger ratingCount = [restaurantData[@"numRatings"] integerValue]; NSInteger newRatingCount = ratingCount + 1; // Compute new average rating float averageRating = [restaurantData[@"avgRating"] floatValue]; float newAverageRating = (averageRating * ratingCount + rating) / newRatingCount; // Set new restaurant info restaurantData[@"numRatings"] = @(newRatingCount); restaurantData[@"avgRating"] = @(newAverageRating); // Commit to Firestore [transaction setData:restaurantData forDocument:restaurant]; [transaction setData:@{@"rating": @(rating)} forDocument:ratingReference]; return nil; } completion:^(id _Nullable result, NSError * _Nullable error) { // ... }]; }
Kotlin+KTX
private fun addRating(restaurantRef: DocumentReference, rating: Float): Task<Void> { // Create reference for new rating, for use inside the transaction val ratingRef = restaurantRef.collection("ratings").document() // In a transaction, add the new rating and update the aggregate totals return db.runTransaction { transaction -> val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()!! // Compute new number of ratings val newNumRatings = restaurant.numRatings + 1 // Compute new average rating val oldRatingTotal = restaurant.avgRating * restaurant.numRatings val newAvgRating = (oldRatingTotal + rating) / newNumRatings // Set new restaurant info restaurant.numRatings = newNumRatings restaurant.avgRating = newAvgRating // Update restaurant transaction.set(restaurantRef, restaurant) // Update rating val data = hashMapOf<String, Any>( "rating" to rating, ) transaction.set(ratingRef, data, SetOptions.merge()) null } }
Java
private Task<Void> addRating(final DocumentReference restaurantRef, final float rating) { // Create reference for new rating, for use inside the transaction final DocumentReference ratingRef = restaurantRef.collection("ratings").document(); // In a transaction, add the new rating and update the aggregate totals return db.runTransaction(new Transaction.Function<Void>() { @Override public Void apply(@NonNull Transaction transaction) throws FirebaseFirestoreException { Restaurant restaurant = transaction.get(restaurantRef).toObject(Restaurant.class); // Compute new number of ratings int newNumRatings = restaurant.numRatings + 1; // Compute new average rating double oldRatingTotal = restaurant.avgRating * restaurant.numRatings; double newAvgRating = (oldRatingTotal + rating) / newNumRatings; // Set new restaurant info restaurant.numRatings = newNumRatings; restaurant.avgRating = newAvgRating; // Update restaurant transaction.set(restaurantRef, restaurant); // Update rating Map<String, Object> data = new HashMap<>(); data.put("rating", rating); transaction.set(ratingRef, data, SetOptions.merge()); return null; } }); }
El uso de una transacción mantiene sus datos agregados consistentes con la colección subyacente. Para leer más sobre las transacciones en Cloud Firestore, consulte Transacciones y escrituras por lotes .
Limitaciones
La solución que se muestra arriba demuestra cómo agregar datos mediante la biblioteca cliente de Cloud Firestore, pero debes tener en cuenta las siguientes limitaciones:
- Seguridad : las transacciones del lado del cliente requieren otorgar permiso a los clientes para actualizar los datos agregados en su base de datos. Si bien se pueden reducir los riesgos de este enfoque escribiendo reglas de seguridad avanzadas, es posible que esto no sea apropiado en todas las situaciones.
- Soporte sin conexión : las transacciones del lado del cliente fallarán cuando el dispositivo del usuario esté desconectado, lo que significa que debe manejar este caso en su aplicación y volver a intentarlo en el momento apropiado.
- Rendimiento : si su transacción contiene múltiples operaciones de lectura, escritura y actualización, podría requerir múltiples solicitudes al backend de Cloud Firestore. En un dispositivo móvil, esto podría llevar mucho tiempo.
- Tasas de escritura : es posible que esta solución no funcione para agregaciones que se actualizan con frecuencia porque los documentos de Cloud Firestore solo se pueden actualizar como máximo una vez por segundo. Además, si una transacción lee un documento que se modificó fuera de la transacción, lo vuelve a intentar un número finito de veces y luego falla. Consulte los contadores distribuidos para encontrar una solución alternativa relevante para las agregaciones que necesitan actualizaciones más frecuentes.
Solución: agregación en tiempo de escritura con Cloud Functions
Si las transacciones del lado del cliente no son adecuadas para su aplicación, puede utilizar una función en la nube para actualizar la información agregada cada vez que se agrega una nueva calificación a un restaurante:
Nodo.js
exports.aggregateRatings = functions.firestore .document('restaurants/{restId}/ratings/{ratingId}') .onWrite(async (change, context) => { // Get value of the newly added rating const ratingVal = change.after.data().rating; // Get a reference to the restaurant const restRef = db.collection('restaurants').doc(context.params.restId); // Update aggregations in a transaction await db.runTransaction(async (transaction) => { const restDoc = await transaction.get(restRef); // Compute new number of ratings const newNumRatings = restDoc.data().numRatings + 1; // Compute new average rating const oldRatingTotal = restDoc.data().avgRating * restDoc.data().numRatings; const newAvgRating = (oldRatingTotal + ratingVal) / newNumRatings; // Update restaurant info transaction.update(restRef, { avgRating: newAvgRating, numRatings: newNumRatings }); }); });
Esta solución descarga el trabajo del cliente a una función alojada, lo que significa que su aplicación móvil puede agregar calificaciones sin esperar a que se complete una transacción. El código ejecutado en una función de nube no está sujeto a reglas de seguridad, lo que significa que ya no es necesario otorgar a los clientes acceso de escritura a los datos agregados.
Limitaciones
El uso de una función en la nube para agregaciones evita algunos de los problemas con las transacciones del lado del cliente, pero conlleva un conjunto diferente de limitaciones:
- Costo : cada calificación agregada provocará una invocación de la función de nube, lo que puede aumentar sus costos. Para obtener más información, consulte la página de precios de Cloud Functions.
- Latencia : al descargar el trabajo de agregación a una función de nube, su aplicación no verá datos actualizados hasta que la función de nube haya terminado de ejecutarse y se haya notificado al cliente sobre los nuevos datos. Dependiendo de la velocidad de su función en la nube, esto podría llevar más tiempo que ejecutar la transacción localmente.
- Tasas de escritura : es posible que esta solución no funcione para agregaciones que se actualizan con frecuencia porque los documentos de Cloud Firestore solo se pueden actualizar como máximo una vez por segundo. Además, si una transacción lee un documento que se modificó fuera de la transacción, lo vuelve a intentar un número finito de veces y luego falla. Consulte los contadores distribuidos para encontrar una solución alternativa relevante para las agregaciones que necesitan actualizaciones más frecuentes.