تجمیع‌های زمان نوشتن

با پُرسمان‌ها در 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

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

Objective-C

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

Swift

توجه: این محصول در 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)

Objective-C

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

Swift

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

Objective-C

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

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