您可以使用 Cloud Firestore 中的查詢,找出大量集合中的文件。如要深入瞭解集合的整體資源,您可以匯總集合中的資料。
您可以在讀取或寫入時匯總資料:
讀取時間匯總會在要求時計算結果。Cloud Firestore 支援在讀取期間支援
count()
、sum()
和average()
匯總查詢。與寫入時間匯總相比,讀取時間匯總查詢更容易新增至應用程式。如要進一步瞭解匯總查詢,請參閱使用匯總查詢產生資料摘要。寫入時間匯總會在應用程式執行相關寫入作業時計算結果。寫入時間匯總的實作工作量較大,但您可能會基於下列其中一個原因,改用寫入時間匯總:
- 您想監聽匯總結果來即時更新。
count()
、sum()
和average()
匯總查詢不支援即時更新。 - 您想將匯總結果儲存在用戶端快取中。
count()
、sum()
和average()
匯總查詢不支援快取。 - 您正在為每位使用者匯總數萬份文件的資料,並考量成本。文件數量較少,讀取時間匯總費用會較低。對於匯總作業中的大量文件,寫入時間匯總可能較少。
- 您想監聽匯總結果來即時更新。
您可以使用用戶端交易或 Cloud Functions 實作寫入時間匯總。以下各節說明如何實作寫入時間匯總。
解決方案:透過用戶端交易進行寫入時間匯總
舉例來說,假設您開發的應用程式可提供當地推薦內容,協助使用者找到優質餐廳。以下查詢會擷取特定餐廳的所有評分:
網路
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();
我們可以將這項資訊儲存在餐廳文件本身,而非擷取所有評分,然後計算匯總資訊:
網路
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);
為了使這些匯總作業保持一致,每次在子集合中加入新評分時,就必須更新這些匯總。實現一致性的一種方法是在單一交易中執行新增和更新:
網路
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; } }); }
使用交易可讓匯總資料與基礎集合保持一致。如要進一步瞭解 Cloud Firestore 中的交易,請參閱「交易和批次寫入」。
限制
上述解決方案示範如何使用 Cloud Firestore 用戶端程式庫匯總資料,但請注意下列限制:
- 安全性:用戶端交易需要授予用戶端更新資料庫中匯總資料的權限。雖然編寫進階安全性規則可以降低這個方法的風險,但這種做法可能不適用於所有情況。
- 離線支援:使用者裝置離線時,用戶端交易會失敗,這表示您需要在應用程式中處理這種情況,並在適當時間重試。
- 效能:如果交易包含多項讀取、寫入和更新作業,可能需要向 Cloud Firestore 後端提出多項要求。在行動裝置上,這可能需要花費大量時間。
- 寫入率:由於 Cloud Firestore 文件最多只能每秒更新一次,因此這項解決方案可能不適用於經常更新的匯總資料。此外,如果交易讀取在交易之外修改的文件,則會重試有限次數,然後失敗。請參閱分散式計數器,瞭解需要更頻繁更新的匯總解決方案。
解決方案:使用 Cloud Functions 進行寫入時匯總
如果應用程式不適合使用用戶端端交易,您可以使用 Cloud Function,在每次新增餐廳評分時更新匯總資訊:
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 }); }); });
這項解決方案會將工作從用戶端卸載至代管函式,這表示行動應用程式可以不必等待交易完成,就能新增評分。在 Cloud 函式中執行的程式碼不受安全性規則約束,也就是說,您不再需要授予用戶端對匯總資料的寫入權限。
限制
使用 Cloud Functions 進行匯總可避免一些用戶端交易問題,但會帶來其他限制:
- 費用:每次新增評分都會觸發 Cloud 函式叫用,進而增加您的費用。詳情請參閱 Cloud Functions 定價頁面。
- 延遲時間:將匯總工作卸載至 Cloud 函式後,應用程式必須等到 Cloud 函式執行完畢且收到新資料的通知後,才會看到更新的資料。視 Cloud Function 的速度而定,這可能比在本機執行交易還要耗時。
- 寫入率:由於 Cloud Firestore 文件最多只能每秒更新一次,因此這項解決方案可能不適用於經常更新的匯總資料。此外,如果交易讀取在交易之外修改的文件,則會重試有限次數,然後失敗。如需針對需要更頻繁更新的匯總資料的相關解決方法,請參閱分散計數器。