Средние агрегирующие запросы в Метеор

Хорошо, все еще в моем игрушечном приложении, я хочу узнать средний пробег на одометрах группы владельцев автомобилей. Это довольно легко на клиенте, но не масштабируется. Правильно? Но на сервере я точно не вижу, как это сделать.

Вопросы:

  • Как вы реализуете что-то на сервере, а затем используете его на клиенте?
  • Как вы используете функцию агрегации $avg для манго, чтобы использовать оптимизированную функцию агрегации?
  • Или, альтернативно, (2) как вы делаете карту/сокращение на сервере и делаете ее доступной для клиента?

Предложение @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 всегда будет иметь нулевой возврат. Обработка материалов на сервере кажется достаточно распространенным вариантом использования, который я, должно быть, просто что-то упустил. Что это будет?

Спасибо!

Ответы

Ответ 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 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.

Если вы начнете комбинировать результаты публикаций, подобные этим, вам нужно будет тщательно изучить, как случайно созданные идентификаторы влияют на поле слияния. Тем не менее, простая реализация этого метода - это просто стандартный запрос к базе данных, за исключением того, что удобнее использовать его с клиентской стороной Meteor API.

TL; версия DR. Почти в любое время, когда вы выталкиваете данные с сервера, publish предпочтительнее method.

Для получения дополнительной информации о различных способах агрегации, проверить это сообщение.

Ответ 2

Я сделал это с помощью метода "aggregate". (ver 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.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;
});

Ответ 4

Если вам нужна реактивность, используйте Meteor.publish вместо Meteor.call. Пример в docs, где они публикуют количество сообщений в данной комнате (чуть выше документации для this.userId), вы должен быть способен сделать что-то подобное.