【问题标题】:Average Aggregation Queries in MeteorMeteor 中的平均聚合查询
【发布时间】:2017-07-02 15:24:57
【问题描述】:

好的,还是在我的玩具应用中,我想在一组车主的里程表上找出平均里程数。这在客户端上非常容易,但无法扩展。正确的?但是在服务器上,我并不完全明白如何完成它。

问题:

  1. 如何在服务器上实现一些东西,然后在客户端上使用它?
  2. 如何使用 mongo 的 $avg 聚合函数来利用其优化的聚合函数?
  3. 或者替代 (2) 如何在服务器上执行 map/reduce 并使其可供客户端使用?

@HubertOG 的建议是使用 Meteor.call,这是有道理的,我这样做了:

# Client side
Template.mileage.average_miles = ->
  answer = null
  Meteor.call "average_mileage", (error, result) ->
    console.log "got average mileage result #{result}"
    answer = result
  console.log "but wait, answer = #{answer}"
  answer

# Server side
Meteor.methods average_mileage: ->
  console.log "server mileage called"
  total = count = 0
  r = Mileage.find({}).forEach (mileage) ->
    total += mileage.mileage
    count += 1
  console.log "server about to return #{total / count}"
  total / count

这似乎可以正常工作,但事实并非如此,因为据我所知,Meteor.call 是一个异步调用,而answer 将始终返回空值。在服务器上处理东西似乎是一个很常见的用例,我一定只是忽略了一些东西。那会是什么?

谢谢!

【问题讨论】:

    标签: mongodb meteor aggregation-framework


    【解决方案1】:

    从 Meteor 0.6.5 开始,集合 API 还不支持聚合查询,因为没有(直接的)方法可以对它们进行实时更新。但是,您仍然可以自己编写它们,并在Meteor.publish 中提供它们,尽管结果将是静态的。在我看来,这样做仍然更可取,因为您可以合并多个聚合并使用客户端集合 API。

    Meteor.publish("someAggregation", function (args) {
        var sub = this;
        // This works for Meteor 0.6.5
        var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
    
        // Your arguments to Mongo's aggregation. Make these however you want.
        var pipeline = [
            { $match: doSomethingWith(args) },
            { $group: {
                _id: whatWeAreGroupingWith(args),
                count: { $sum: 1 }
            }}
        ];
    
        db.collection("server_collection_name").aggregate(        
            pipeline,
            // Need to wrap the callback so it gets called in a Fiber.
            Meteor.bindEnvironment(
                function(err, result) {
                    // Add each of the results to the subscription.
                    _.each(result, function(e) {
                        // Generate a random disposable id for aggregated documents
                        sub.added("client_collection_name", Random.id(), {
                            key: e._id.somethingOfInterest,                        
                            count: e.count
                        });
                    });
                    sub.ready();
                },
                function(error) {
                    Meteor._debug( "Error doing aggregation: " + error);
                }
            )
        );
    });
    

    以上是一个示例分组/计数聚合。一些注意事项:

    • 执行此操作时,您自然会在 server_collection_name 上进行聚合,并将结果推送到名为 client_collection_name 的不同集合中。
    • 此订阅不会生效,并且可能会在参数发生变化时更新,因此我们使用了一个非常简单的循环,将所有结果推送出去。
    • 聚合的结果没有 Mongo ObjectID,所以我们自己生成一些任意的。
    • 对聚合的回调需要包装在 Fiber 中。我在这里使用Meteor.bindEnvironment,但也可以使用Future 进行更底层的控制。

    如果您开始合并此类出版物的结果,则需要仔细考虑随机生成的 ID 如何影响合并框。然而,一个简单的实现只是一个标准的数据库查询,除了在客户端使用 Meteor API 更方便。

    TL;DR 版本:几乎在您从服务器推送数据的任何时候,publish 都比method 更可取。

    如需更多关于不同聚合方式的信息,check out this post

    【讨论】:

    • 我不想在没有“谢谢”的情况下留下这个答案。这是一个非常棒的答案。我暂时被另一个项目拉走了,但是安德鲁,你显然为此付出了很多,我非常感激。
    • @SteveRoss 不客气。谢谢你的好话!
    • 对出色的聚合示例表示敬意。这是唯一对我有用的。并且您设法在没有包的情况下使用 MongoInternals 并在发布功能中做到这一点……为红色天鹅绒蛋糕锦上添花。谢谢分享!
    • 安德鲁的好答案。你知道有什么包可以抽象出这里的棘手部分吗?
    • @TomColeman github.com/jhoxray/meteor-mongo-extensions 但未维护。
    【解决方案2】:

    我使用“聚合”方法做到了这一点。 (版本 0.7.x)

    if(Meteor.isServer){
    Future = Npm.require('fibers/future');
    Meteor.methods({
        'aggregate' : function(param){
            var fut = new Future();
            MongoInternals.defaultRemoteCollectionDriver().mongo._getCollection(param.collection).aggregate(param.pipe,function(err, result){
                fut.return(result);
            });
            return fut.wait();
        }
        ,'test':function(param){
            var _param = {
                pipe : [
                { $unwind:'$data' },
                { $match:{ 
                    'data.y':"2031",
                    'data.m':'01',
                    'data.d':'01'
                }},
                { $project : {
                    '_id':0
                    ,'project_id'               : "$project_id"
                    ,'idx'                      : "$data.idx"
                    ,'y'                        : '$data.y'
                    ,'m'                        : '$data.m'
                    ,'d'                        : '$data.d'
                }}
            ],
                collection:"yourCollection"
            }
            Meteor.call('aggregate',_param);
        }
    });
    

    }

    【讨论】:

      【解决方案3】:

      如果您想要反应性,请使用 Meteor.publish 而不是 Meteor.call。在docs 中有一个示例,他们发布了给定房间中的消息数量(就在this.userId 的文档上方),您应该能够做类似的事情。

      【讨论】:

      • 因此,反应性似乎很好,因为每当有人更新他们的里程时,平均变化。因此,这将建议根据您所说的发布/订阅。但在我看来,pub/sub 更像是返回过滤或映射的集合,而不是像平均值这样的标量值。这似乎应该只是一两行代码——你不觉得吗?
      • @SteveRoss 我同意像这样的通用功能应该很容易编写,但是 Meteor 没有特别支持它(目前是 6.5 版),所以替代方案是使用方法/调用或发布/订阅。鉴于文档中的类似示例以及反应性的好处,我会选择发布/订阅。
      • 发布一个只包含一个带有标量值的文档的集合并没有错。谁知道呢?也许有一天你会想要不止一个平均...然后你可以发布多个文档:)
      【解决方案4】:

      您可以为此使用Meteor.methods

      // server
      Meteor.methods({
        average: function() {
          ...
          return something;
        },
      
      });
      
      // client
      
      var _avg = {                      /* Create an object to store value and dependency */
        dep: new Deps.Dependency();
      };
      
      Template.mileage.rendered = function() {
        _avg.init = true;
      };
      
      Template.mileage.averageMiles = function() {
        _avg.dep.depend();              /* Make the function rerun when _avg.dep is touched */
        if(_avg.init) {                 /* Fetch the value from the server if not yet done */
          _avg.init = false; 
          Meteor.call('average', function(error, result) {
            _avg.val = result;
            _avg.dep.changed();         /* Rerun the helper */
          });
        }
        return _avg.val;
      });
      

      【讨论】:

      • 所以我很乐意继续获取新的输入值等等。在什么时候对服务器进行直写。因为我所看到的是,除了客户端上的数据尚未到达服务器之外,这将起作用。
      • 我已经修改了原来的问题。
      • 您可以使用依赖项在需要的地方创建响应性。我已经更新了我的答案。结果可能有点过于复杂,目前我不确定在您的情况下如何更简单。
      猜你喜欢
      • 1970-01-01
      • 2021-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多