Kueri di Cloud Firestore memungkinkan Anda menemukan dokumen dalam koleksi besar. Untuk mempelajari properti koleksi secara keseluruhan, Anda dapat menggabungkan data dari koleksi.
Anda dapat menggabungkan data pada waktu baca atau pada waktu tulis:
Agregasi waktu baca menghitung hasil pada saat permintaan. Cloud Firestore mendukung kueri agregasi
count()
,sum()
, danaverage()
pada waktu baca. Kueri agregasi waktu baca lebih mudah ditambahkan ke aplikasi Anda daripada agregasi waktu tulis. Untuk mengetahui informasi selengkapnya tentang kueri agregasi, lihat Meringkas data dengan kueri agregasi.Agregasi waktu tulis menghitung hasil setiap kali aplikasi menjalankan operasi tulis yang relevan. Agregasi waktu tulis lebih sulit untuk diterapkan, tetapi Anda dapat menggunakannya bukan agregasi waktu baca untuk salah satu alasan berikut:
- Anda ingin mengetahui hasil agregasi untuk pembaruan real-time.
Kueri agregasi
count()
,sum()
, danaverage()
tidak mendukung pembaruan real-time. - Anda ingin menyimpan hasil agregasi dalam cache sisi klien.
Kueri agregasi
count()
,sum()
, danaverage()
tidak mendukung cache. - Anda menggabungkan data dari puluhan ribu dokumen untuk setiap pengguna dan mempertimbangkan biaya. Dengan jumlah dokumen yang lebih sedikit, agregasi waktu baca lebih hemat. Untuk jumlah dokumen yang besar dalam agregasi, agregasi waktu tulis mungkin lebih hemat.
- Anda ingin mengetahui hasil agregasi untuk pembaruan real-time.
Kueri agregasi
Anda dapat menerapkan agregasi waktu tulis menggunakan transaksi sisi klien atau dengan Cloud Functions. Bagian berikut menjelaskan cara menerapkan agregasi waktu tulis.
Solusi: Agregasi waktu tulis dengan transaksi sisi klien
Bayangkan sebuah aplikasi rekomendasi lokal yang dapat membantu pengguna menemukan restoran bagus. Kueri berikut mengambil semua rating untuk suatu restoran:
Web
db.collection("restaurants") .doc("arinell-pizza") .collection("ratings") .get();
Swift
do { let snapshot = try await db.collection("restaurants") .document("arinell-pizza") .collection("ratings") .getDocuments() print(snapshot) } catch { print(error) }
Objective-C
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();
Daripada mengambil semua rating lalu mengolah informasi agregat, kita dapat menyimpan informasi ini di dokumen restoran itu sendiri:
Web
var arinellDoc = { name: 'Arinell Pizza', avgRating: 4.65, numRatings: 683 };
Swift
struct Restaurant { let name: String let avgRating: Float let numRatings: Int } let arinell = Restaurant(name: "Arinell Pizza", avgRating: 4.65, numRatings: 683)
Objective-C
@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);
Agar konsisten, agregasi ini harus diperbarui setiap kali rating baru ditambahkan ke subkoleksi. Salah satu cara untuk mencapai konsistensi adalah dengan melakukan penambahan dan pembaruan dalam satu transaksi:
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 }); }); }); }
Swift
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 { // ... } }
Objective-C
- (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; } }); }
Penggunaan transaksi membuat data gabungan selalu konsisten dengan koleksi yang mendasarinya. Untuk membaca lebih lanjut transaksi di Cloud Firestore, lihat Transaksi dan Batch Operasi Tulis.
Batasan
Solusi yang ditunjukkan di atas mendemonstrasikan agregasi data menggunakan library klien Cloud Firestore, tetapi Anda harus mengetahui keterbatasan berikut:
- Keamanan - Transaksi sisi klien memerlukan pemberian izin kepada klien untuk memperbarui data gabungan di database Anda. Meskipun Anda dapat mengurangi risiko pendekatan ini dengan menulis aturan keamanan lanjutan, cara ini mungkin tidak sesuai dengan segala situasi.
- Dukungan offline - Transaksi sisi klien akan gagal jika perangkat pengguna sedang offline. Artinya, Anda perlu menangani kasus ini di aplikasi dan mencoba lagi pada waktu yang tepat.
- Performa - Jika transaksi Anda berisi beberapa operasi baca, tulis, dan pembaruan, mungkin diperlukan beberapa permintaan ke backend Cloud Firestore. Pada perangkat seluler, proses ini dapat memakan waktu lama.
- Kecepatan penulisan - Solusi ini mungkin tidak dapat diterapkan untuk agregasi yang sering diperbarui karena dokumen Cloud Firestore hanya dapat diperbarui maksimal sekali per detik. Selain itu, jika transaksi membaca dokumen yang dimodifikasi di luar transaksi itu sendiri, transaksi tersebut akan mencoba ulang beberapa kali, lalu gagal. Lihat penghitung terdistribusi untuk menemukan solusi relevan terkait agregasi yang memerlukan pembaruan lebih sering.
Solusi: Agregasi waktu tulis dengan Cloud Functions
Jika transaksi sisi klien tidak cocok untuk aplikasi, Anda dapat menggunakan Cloud Function untuk memperbarui informasi gabungan setiap kali rating baru ditambahkan ke restoran:
Node.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 }); }); });
Solusi ini mengalihkan pekerjaan dari klien ke fungsi yang dihosting, sehingga aplikasi seluler Anda dapat menambahkan rating tanpa menunggu transaksi selesai. Kode yang dijalankan di Cloud Function tidak terikat oleh aturan keamanan, sehingga Anda tidak perlu lagi memberikan akses tulis ke data gabungan kepada klien.
Batasan
Penggunaan Cloud Function untuk agregasi menghindari beberapa masalah terkait transaksi sisi klien, tetapi memiliki sejumlah keterbatasan lain:
- Biaya - Setiap rating yang ditambahkan akan menyebabkan pemanggilan Cloud Function, dan ini dapat meningkatkan biaya. Untuk informasi lebih lanjut, lihat halaman harga Cloud Functions.
- Latensi - Dengan memindahkan pekerjaan agregasi ke Cloud Function, aplikasi Anda tidak akan melihat data yang telah diperbarui hingga Cloud Function selesai dijalankan dan klien diberi tahu tentang data baru tersebut. Tergantung kecepatan Cloud Function Anda, proses ini bisa memakan waktu lebih lama daripada menjalankan transaksi secara lokal.
- Kecepatan penulisan - Solusi ini mungkin tidak dapat diterapkan untuk agregasi yang sering diperbarui karena dokumen Cloud Firestore hanya dapat diperbarui maksimal sekali per detik. Selain itu, jika transaksi membaca dokumen yang dimodifikasi di luar transaksi itu sendiri, transaksi tersebut akan mencoba ulang beberapa kali, lalu gagal. Lihat penghitung terdistribusi untuk menemukan solusi relevan terkait agregasi yang memerlukan pembaruan lebih sering.