با پُرسمانها در 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
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
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
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 برای بهروزرسانی اطلاعات تجمیعی هر بار که رتبهبندی جدیدی به رستوران اضافه میشود استفاده کنید:
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 Function اجرا میشود محدود به قوانین امنیتی نیست، یعنی دیگر لازم نیست به مشتریان دسترسی نوشتن به دادههای تجمیعی بدهید.
محدودیتها
استفاده از Cloud Function برای تجمیعها از برخی مشکلات مربوط به تراکنشهای سمت کارخواه جلوگیری میکند، اما با مجموعه متفاوتی از محدودیتها همراه است:
- هزینه - هر ردهبندی اضافهشده باعث فراخوانی Cloud Function میشود که ممکن است هزینههایتان را افزایش دهد. برای اطلاعات بیشتر، به صفحه قیمتگذاری Cloud Functions مراجعه کنید.
- تأخیر - با واگذاری کار انبوهش به Cloud Function، برنامه شما دادههای بهروزشده را تا زمانی که Cloud Function اجرا نشده باشد و مشتری از دادههای جدید مطلع نشده باشد، نخواهد دید. بسته به سرعت «تابع ابری» شما، این کار ممکن است بیشتر از اجرای تراکنش بهصورت محلی طول بکشد.
- نرخهای نوشتن - این راهحل ممکن است برای تجمیعهایی که بهطور مکرر بهروز میشوند کار نکند زیرا اسناد Cloud Firestore فقط میتوانند حداکثر یک بار در ثانیه بهروز شوند. علاوهبراین، اگر تراکنشی سندی را بخواند که خارج از تراکنش اصلاح شده است، تعداد دفعات محدودی دوباره امتحان میکند و سپس ناموفق میشود. برای راه حل جایگزین مربوط به تجمیعهایی که به بهروزرسانیهای مکرر نیاز دارند، شمارندههای توزیعشده را بررسی کنید.