การค้นหาใน Cloud Firestore ช่วยให้คุณค้นหาเอกสารในคอลเล็กชันขนาดใหญ่ได้ หากต้องการรับข้อมูลเชิงลึกเกี่ยวกับคุณสมบัติของคอลเลกชันโดยรวม คุณสามารถรวบรวมข้อมูลผ่านคอลเลกชันได้
คุณสามารถรวบรวมข้อมูลทั้งในเวลาอ่านหรือขณะเขียน:
การรวมเวลาอ่าน จะคำนวณผลลัพธ์ ณ เวลาที่ร้องขอ Cloud Firestore รองรับการสืบค้นการรวม
count()
,sum()
และaverage()
ในเวลาอ่าน คำค้นหาการรวมเวลาอ่านจะเพิ่มลงในแอปได้ง่ายกว่าการรวมเวลาเขียน สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการสืบค้นแบบรวม โปรดดู ที่ สรุปข้อมูลด้วยการสืบค้นแบบรวมการรวมเวลาการเขียน จะคำนวณผลลัพธ์ในแต่ละครั้งที่แอปดำเนินการเขียนที่เกี่ยวข้อง การรวมเวลาการเขียนเป็นงานที่ต้องดำเนินการมากกว่า แต่คุณอาจใช้การรวมเหล่านี้แทนการรวมเวลาอ่านด้วยเหตุผลข้อใดข้อหนึ่งต่อไปนี้:
- คุณต้องการฟังผลการรวมสำหรับการอัปเดตแบบเรียลไทม์ แบบสอบถามการรวม
count()
,sum()
และaverage()
ไม่รองรับการอัปเดตแบบเรียลไทม์ - คุณต้องการเก็บผลลัพธ์การรวมไว้ในแคชฝั่งไคลเอ็นต์ แบบสอบถามการรวม
count()
,sum()
และaverage()
ไม่รองรับการแคช - คุณกำลังรวบรวมข้อมูลจากเอกสารนับหมื่นสำหรับผู้ใช้แต่ละรายและพิจารณาค่าใช้จ่าย ด้วยจำนวนเอกสารที่น้อยกว่า การรวมเวลาอ่านจึงมีค่าใช้จ่ายน้อยลง สำหรับเอกสารจำนวนมากในการรวมกลุ่ม การรวมเวลาเขียนอาจมีค่าใช้จ่ายน้อยกว่า
- คุณต้องการฟังผลการรวมสำหรับการอัปเดตแบบเรียลไทม์ แบบสอบถามการรวม
คุณสามารถใช้การรวมเวลาการเขียนโดยใช้ธุรกรรมฝั่งไคลเอ็นต์หรือกับ Cloud Functions ส่วนต่อไปนี้จะอธิบายวิธีการใช้การรวมเวลาการเขียน
วิธีแก้ไข: การรวมเวลาการเขียนกับธุรกรรมฝั่งไคลเอ็นต์
ลองใช้แอปแนะนำในท้องถิ่นที่ช่วยให้ผู้ใช้ค้นหาร้านอาหารดีๆ ข้อความค้นหาต่อไปนี้จะดึงข้อมูลคะแนนทั้งหมดสำหรับร้านอาหารที่ระบุ:
เว็บ
db.collection("restaurants") .doc("arinell-pizza") .collection("ratings") .get();
สวิฟท์
do { let snapshot = try await db.collection("restaurants") .document("arinell-pizza") .collection("ratings") .getDocuments() print(snapshot) } catch { print(error) }
วัตถุประสงค์-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 };
สวิฟท์
struct Restaurant { let name: String let avgRating: Float let numRatings: Int } let arinell = Restaurant(name: "Arinell Pizza", avgRating: 4.65, numRatings: 683)
วัตถุประสงค์-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 }); }); }); }
สวิฟท์
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
- (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 สามารถอัปเดตได้มากที่สุดหนึ่งครั้งต่อวินาทีเท่านั้น นอกจากนี้ หากธุรกรรมอ่านเอกสารที่ได้รับการแก้ไขภายนอกธุรกรรม จะ ลองอีกครั้งตามจำนวนจำกัด แล้วล้มเหลว ตรวจสอบ ตัวนับแบบกระจาย เพื่อดูวิธีแก้ปัญหาที่เกี่ยวข้องสำหรับการรวมกลุ่มซึ่งจำเป็นต้องอัปเดตบ่อยขึ้น