การรวมเวลาการเขียน

การค้นหาใน Cloud Firestore ช่วยให้คุณค้นหาเอกสารในคอลเล็กชันขนาดใหญ่ได้ หากต้องการรับข้อมูลเชิงลึกเกี่ยวกับคุณสมบัติของคอลเลกชันโดยรวม คุณสามารถรวบรวมข้อมูลผ่านคอลเลกชันได้

คุณสามารถรวบรวมข้อมูลทั้งในเวลาอ่านหรือขณะเขียน:

  • การรวมเวลาอ่าน จะคำนวณผลลัพธ์ ณ เวลาที่ร้องขอ Cloud Firestore รองรับการสืบค้นการรวม count() , sum() และ average() ในเวลาอ่าน คำค้นหาการรวมเวลาอ่านจะเพิ่มลงในแอปได้ง่ายกว่าการรวมเวลาเขียน สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการสืบค้นแบบรวม โปรดดู ที่ สรุปข้อมูลด้วยการสืบค้นแบบรวม

  • การรวมเวลาการเขียน จะคำนวณผลลัพธ์ในแต่ละครั้งที่แอปดำเนินการเขียนที่เกี่ยวข้อง การรวมเวลาการเขียนเป็นงานที่ต้องดำเนินการมากกว่า แต่คุณอาจใช้การรวมเหล่านี้แทนการรวมเวลาอ่านด้วยเหตุผลข้อใดข้อหนึ่งต่อไปนี้:

    • คุณต้องการฟังผลการรวมสำหรับการอัปเดตแบบเรียลไทม์ แบบสอบถามการรวม 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)
}

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์นี้ไม่สามารถใช้งานได้บนเป้าหมาย watchOS และ App Clip
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
};

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์นี้ไม่สามารถใช้งานได้บนเป้าหมาย 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)

วัตถุประสงค์-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+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 });
        });
    });
}

สวิฟท์

หมายเหตุ: ผลิตภัณฑ์นี้ไม่สามารถใช้งานได้บนเป้าหมาย 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 {
    // ...
  }
}

วัตถุประสงค์-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+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 สามารถอัปเดตได้มากที่สุดหนึ่งครั้งต่อวินาทีเท่านั้น นอกจากนี้ หากธุรกรรมอ่านเอกสารที่ได้รับการแก้ไขภายนอกธุรกรรม จะ ลองอีกครั้งตามจำนวนจำกัด แล้วล้มเหลว ตรวจสอบ ตัวนับแบบกระจาย เพื่อดูวิธีแก้ปัญหาที่เกี่ยวข้องสำหรับการรวมกลุ่มซึ่งจำเป็นต้องอัปเดตบ่อยขึ้น

โซลูชัน: การรวมเวลาการเขียนด้วยฟังก์ชันคลาวด์

หากธุรกรรมฝั่งไคลเอ็นต์ไม่เหมาะกับแอปพลิเคชันของคุณ คุณสามารถใช้ ฟังก์ชันคลาวด์ เพื่ออัปเดตข้อมูลรวมทุกครั้งที่มีการเพิ่มคะแนนใหม่ในร้านอาหาร:

โหนด 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 Firestore สามารถอัปเดตได้มากที่สุดหนึ่งครั้งต่อวินาทีเท่านั้น นอกจากนี้ หากธุรกรรมอ่านเอกสารที่ได้รับการแก้ไขภายนอกธุรกรรม จะ ลองอีกครั้งตามจำนวนจำกัด แล้วล้มเหลว ตรวจสอบ ตัวนับแบบกระจาย เพื่อดูวิธีแก้ปัญหาที่เกี่ยวข้องสำหรับการรวมกลุ่มซึ่งจำเป็นต้องอัปเดตบ่อยขึ้น