聚合查询

Cloud Firestore 中的高级查询让您能够快速查找大型集合中的文档。但您如果想从整体上深入了解相关集合的属性,则将需要对集合进行聚合操作。

Cloud Firestore 不支持本机聚合查询。但是,您可以使用客户端事务或 Cloud Functions 轻松维护有关您的数据的聚合信息。

继续学习之前,请确保您已阅读有关查询和 Cloud Firestore 数据模型的内容。

解决方案:客户端事务

假设有一个帮助用户寻找人气餐厅的本地推荐类应用。以下查询会检索指定餐厅的所有评分:

网页

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

我们无需提取所有评分,然后计算聚合信息,而是可以将这些信息存储在这家餐厅的文档中:

网页

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

为了保持这些聚合数据的一致性,每当有新的评分添加到子集合时,您都必须对数据进行更新。实现一致性的一种方法是在单个事务中执行添加和更新操作:

网页

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

使用事务可让您的聚合数据与底层集合保持一致。要详细了解 Cloud Firestore 中的事务,请参阅事务和批量写入

限制

上面显示的解决方案演示了如何使用 Cloud Firestore 客户端库聚合数据,但是您应该注意以下限制:

  • 安全性 - 客户端事务要求授予客户端更新数据库中聚合数据的权限。虽然您可以通过编写高级安全规则来降低此方法的风险,但安全规则并非在所有情况下都适用。
  • 离线支持 - 如果用户的设备离线,客户端事务将失败,这意味着您需要在自己的应用中处理这种情况,并在适当的时候重试。
  • 性能 - 如果事务包含多个读取、写入和更新操作,则可能需要多次对 Cloud Firestore 后端提出请求。在移动设备上,可能需要很长时间才能完成事务。

解决方案:Cloud Functions

如果客户端事务不适合您的应用,则可以使用 Cloud Function 在每次有新的评分添加到餐厅时更新聚合信息:

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

该解决方案将客户端的工作分流到一个托管函数中,这意味着您的移动应用无需等待事务完成即可添加评分。在 Cloud Function 中执行的代码不受安全规则约束,这意味着您无需再为客户端提供写入聚合数据的权限。

限制

使用 Cloud Functions 函数进行聚合可以避免客户端事务存在的一些问题,但也有其他限制:

  • 费用 - 添加的每个评分都将引发一次 Cloud Functions 函数调用,这可能会增加费用。有关详细信息,请参阅 Cloud Functions 的定价页面
  • 延迟 - 将聚合工作分流到某个 Cloud Functions 函数后,您的应用将不会看到更新后的数据,直到该 Cloud Functions 函数执行完毕并且客户端已收到有关新数据的通知。这一过程所需的时间可能比在本地执行事务更长,具体取决于您的 Cloud Functions 函数的速度。

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面