Buka konsol

Kueri Agregasi

Kueri lanjutan di Cloud Firestore memungkinkan Anda menemukan dokumen dalam koleksi besar dengan cepat. Jika ingin mendapatkan laporan tentang properti koleksi secara keseluruhan, Anda memerlukan agregasi dari koleksi.

Cloud Firestore tidak mendukung kueri agregasi bawaan. Namun, Anda dapat menggunakan transaksi sisi klien atau Cloud Functions untuk mengontrol informasi agregat mengenai data dengan mudah.

Sebelum melanjutkan, pastikan Anda telah membaca tentang kueri dan model data Cloud Firestore.

Solusi: Transaksi sisi klien

Bayangkan sebuah aplikasi rekomendasi lokal yang dapat membantu pengguna menemukan restoran terbaik. Kueri berikut mengambil semua rating untuk restoran tertentu:

Web

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

Swift

db.collection("restaurants")
    .document("arinell-pizza")
    .collection("ratings")
    .getDocuments() { (querySnapshot, err) in

        // ...

}

Android

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

Daripada mengambil semua rating lalu mengomputasi informasi agregat, kita dapat menyimpan informasi ini di dokumen restoran itu sendiri:

Web

var arinellDoc = {
  name: 'Arinell Pizza',
  avgRating: 4.65,
  numRatings: 683
}

Swift

struct Restaurant {

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

    init(name: String, avgRating: Float, numRatings: Int) {
        self.name = name
        self.avgRating = avgRating
        self.numRatings = numRatings
    }

}

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

Android

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);

Agar konsisten, agregasi ini harus diupdate setiap kali rating baru ditambahkan ke subkoleksi. Salah satu cara untuk mencapai konsistensi adalah dengan melakukan penambahan dan update dalam 1 transaksi:

Web

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) {
    let ratingRef: DocumentReference = restaurantRef.collection("ratings").document()

    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
    }) { (object, err) in
        // ...
    }
}

Android

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(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;
        }
    });
}

Penggunaan transaksi membuat data agregat selalu konsisten dengan koleksi yang mendasarinya. Untuk membaca lebih lanjut tentang transaksi di Cloud Firestore, lihat Transaksi dan Batch Operasi Tulis.

Batasan

Solusi yang ditunjukkan di atas mendemonstrasikan agregasi data menggunakan library klien Cloud Firestore, namun Anda harus menyadari batasan berikut:

  • Keamanan - Transaksi sisi klien memerlukan pemberian izin kepada klien untuk mengupdate data agregat di database Anda. Meskipun Anda dapat mengurangi risiko pendekatan ini dengan menulis aturan keamanan lanjutan, cara ini mungkin tidak sesuai untuk semua situasi.
  • Dukungan offline - Transaksi sisi klien akan gagal jika perangkat pengguna sedang offline. Artinya, Anda perlu menangani kasus ini di aplikasi dan mencoba lagi pada waktu yang tepat.
  • Performa - Jika transaksi Anda berisi beberapa operasi baca, tulis, dan update, diperlukan beberapa permintaan ke backend Cloud Firestore. Pada perangkat seluler, proses ini dapat memakan waktu lama.

Solusi: Cloud Functions

Jika transaksi sisi klien tidak cocok untuk aplikasi, Anda dapat menggunakan Cloud Function untuk mengupdate informasi agregat setiap kali rating baru ditambahkan ke restoran:

Node.js

exports.aggregateRatings = firestore
  .document('restaurants/{restId}/ratings/{ratingId}')
  .onWrite(event => {
    // Get value of the newly added rating
    var ratingVal = event.data.get('rating');

    // Get a reference to the restaurant
    var restRef = db.collection('restaurants').document(event.params.restId);

    // Update aggregations in a transaction
    return db.runTransaction(transaction => {
      return transaction.get(restRef).then(restDoc => {
        // Compute new number of ratings
        var newNumRatings = restDoc.data('numRatings') + 1;

        // Compute new average rating
        var oldRatingTotal = restDoc.data('avgRating') * restDoc.data('numRatings');
        var newAvgRating = (oldRatingTotal + ratingVal) / newNumRatings;

        // Update restaurant info
        return transaction.update(restRef, {
          avgRating: newAvgRating,
          numRatings: newNumRatings
        });
      });
    });
});

Solusi ini mengalihkan kerja dari klien ke fungsi yang di-hosting, sehingga aplikasi seluler Anda dapat menambahkan rating tanpa menunggu transaksi selesai. Kode yang dijalankan di Cloud Function tidak terikat oleh aturan keamanan, sehingga Anda tidak perlu lagi memberikan akses tulis ke data agregat kepada klien.

Batasan

Penggunaan Cloud Function untuk agregasi menghindari beberapa masalah terkait transaksi sisi klien, namun memunculkan beberapa batasan lain:

  • Biaya - Setiap rating yang ditambahkan akan menyebabkan pemanggilan Cloud Function, dan ini dapat meningkatkan biaya. Untuk informasi lebih lanjut, lihat halaman harga Cloud Functions.
  • Latensi - Dengan memindahkan pekerjaan agregasi ke Cloud Function, aplikasi Anda tidak dapat melihat data yang telah diupdate hingga Cloud Function selesai dijalankan dan klien diberi tahu tentang data baru tersebut. Tergantung pada kecepatan Cloud Function Anda, proses ini bisa memakan waktu lebih lama daripada menjalankan transaksi secara lokal.