تجمعات زمان نوشتن

کوئری‌ها در Cloud Firestore به شما امکان می‌دهند اسناد را در مجموعه‌های بزرگ پیدا کنید. برای به دست آوردن بینشی در مورد ویژگی‌های کل مجموعه، می‌توانید داده‌ها را در یک مجموعه جمع‌آوری کنید.

می‌توانید داده‌ها را یا در زمان خواندن یا در زمان نوشتن جمع‌آوری کنید:

  • تجمیع‌های زمان خواندن، نتیجه را در زمان درخواست محاسبه می‌کنند. Cloud Firestore از کوئری‌های تجمیع count() ، sum() و average() در زمان خواندن پشتیبانی می‌کند. اضافه کردن کوئری‌های تجمیع زمان خواندن به برنامه شما آسان‌تر از تجمیع‌های زمان نوشتن است. برای اطلاعات بیشتر در مورد کوئری‌های تجمیع، به Summarize data with aggregation queries مراجعه کنید.

  • تجمیع‌های زمان نوشتن، هر بار که برنامه یک عملیات نوشتن مرتبط را انجام می‌دهد، نتیجه‌ای را محاسبه می‌کنند. تجمیع‌های زمان نوشتن کار بیشتری برای پیاده‌سازی دارند، اما می‌توانید به یکی از دلایل زیر از آنها به جای تجمیع‌های زمان خواندن استفاده کنید:

    • شما می‌خواهید به نتیجه تجمیع برای به‌روزرسانی‌های بلادرنگ گوش دهید. کوئری‌های تجمیع count() ، sum() و average() از به‌روزرسانی‌های بلادرنگ پشتیبانی نمی‌کنند.
    • شما می‌خواهید نتیجه تجمیع را در یک حافظه پنهان سمت کلاینت ذخیره کنید. کوئری‌های تجمیع count() ، sum() و average() از ذخیره‌سازی پشتیبانی نمی‌کنند.
    • شما در حال جمع‌آوری داده‌ها از ده‌ها هزار سند برای هر یک از کاربران خود هستید و هزینه‌ها را در نظر می‌گیرید. در تعداد اسناد کمتر، جمع‌آوری‌های زمان خواندن هزینه کمتری دارند. برای تعداد زیادی سند در یک جمع‌آوری، جمع‌آوری‌های زمان نوشتن ممکن است هزینه کمتری داشته باشند.

شما می‌توانید تجمیع زمان نوشتن را با استفاده از یک تراکنش سمت کلاینت یا با Cloud Functions پیاده‌سازی کنید. بخش‌های زیر نحوه پیاده‌سازی تجمیع زمان نوشتن را شرح می‌دهند.

راه حل: تجمیع زمان نوشتن با یک تراکنش سمت کلاینت

یک اپلیکیشن پیشنهاد محلی را در نظر بگیرید که به کاربران در یافتن رستوران‌های عالی کمک می‌کند. کوئری زیر تمام امتیازهای یک رستوران مشخص را بازیابی می‌کند:

وب

db.collection("restaurants")
  .doc("arinell-pizza")
  .collection("ratings")
  .get();

سویفت

توجه: این محصول در watchOS و App Clip موجود نیست.
do {
  let snapshot = try await db.collection("restaurants")
    .document("arinell-pizza")
    .collection("ratings")
    .getDocuments()
  print(snapshot)
} catch {
  print(error)
}

هدف-سی

توجه: این محصول در watchOS و App Clip موجود نیست.
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
};

سویفت

توجه: این محصول در watchOS و App Clip موجود نیست.
struct Restaurant {

  let name: String
  let avgRating: Float
  let numRatings: Int

}

let arinell = Restaurant(name: "Arinell Pizza", avgRating: 4.65, numRatings: 683)

هدف-سی

توجه: این محصول در watchOS و App Clip موجود نیست.
@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 });
        });
    });
}

سویفت

توجه: این محصول در watchOS و App Clip موجود نیست.
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 {
    // ...
  }
}

هدف-سی

توجه: این محصول در watchOS و App Clip موجود نیست.
- (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 حداکثر می‌توانند یک بار در ثانیه به‌روزرسانی شوند. علاوه بر این، اگر یک تراکنش سندی را بخواند که خارج از تراکنش تغییر یافته است، تعداد محدودی بار دوباره تلاش می‌کند و سپس شکست می‌خورد. برای یافتن راه حلی مرتبط برای تجمیع‌هایی که به به‌روزرسانی‌های مکررتری نیاز دارند، شمارنده‌های توزیع شده را بررسی کنید.

راه حل: تجمیع زمان نوشتن با توابع ابری

اگر تراکنش‌های سمت کلاینت برای برنامه شما مناسب نیستند، می‌توانید از یک تابع ابری برای به‌روزرسانی اطلاعات کلی هر بار که رتبه‌بندی جدیدی به یک رستوران اضافه می‌شود، استفاده کنید:

نود جی اس

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 Firestore حداکثر می‌توانند یک بار در ثانیه به‌روزرسانی شوند. علاوه بر این، اگر یک تراکنش سندی را بخواند که خارج از تراکنش تغییر یافته است، تعداد محدودی بار دوباره تلاش می‌کند و سپس شکست می‌خورد. برای یافتن راه حلی مرتبط برای تجمیع‌هایی که به به‌روزرسانی‌های مکررتری نیاز دارند، شمارنده‌های توزیع شده را بررسی کنید.