2013-08-29 12 views
15

Ok, wciąż w mojej aplikacji z zabawkami, chcę dowiedzieć się, jaki jest średni przebieg w grupie odometrów właścicieli samochodów. Jest to dość łatwe dla klienta, ale nie skaluje się. Dobrze? Ale na serwerze nie wiem dokładnie, jak to osiągnąć.Średnia liczba zapytań o agregację w Meteoru

Pytania:

  1. Jak wdrożyć coś na serwerze następnie używać go na kliencie?
  2. Jak wykorzystać funkcję agregacji $ avg mongo do zoptymalizowania funkcji agregacji?
  3. Lub alternatywnie (2) jak zrobić mapę/zmniejszyć na serwerze i udostępnić ją klientowi?

Sugestia przez @HubertOG było wykorzystanie Meteor.call, co ma sens i zrobiłem tak:

# 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 

To wydaje się działać dobrze, ale to nie dlatego, że tak blisko jak ja można powiedzieć, że Meteor.call jest połączeniem asynchronicznym, a answer zawsze będzie zwracanym zerem. Obsługa rzeczy na serwerze wydaje się dość powszechnym przypadkiem użycia, który musiałam przeoczyć. Co by to było?

Dzięki!

Odpowiedz

28

Od wersji Meteor 0.6.5 interfejs API kolekcji nie obsługuje zapytań o agregację, ponieważ nie ma (prostego) sposobu aktualizowania na bieżąco. Jednak nadal możesz je napisać samodzielnie i udostępnić je w postaci Meteor.publish, chociaż wynik będzie statyczny. Moim zdaniem, robienie tego w ten sposób jest jeszcze lepsze, ponieważ można łączyć wiele agregacji i korzystać z interfejsu API po stronie klienta.

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

Powyższe jest przykładową agregacją grupowania/liczenia. Niektóre rzeczy uwaga:

  • Kiedy to zrobisz, będziesz naturalnie robić agregacji na server_collection_name i pchania wyniki do innej kolekcji o nazwie client_collection_name.
  • Ta subskrypcja nie będzie dostępna i prawdopodobnie zostanie zaktualizowana po zmianie argumentów, więc używamy naprawdę prostej pętli, która wypycha wszystkie wyniki.
  • Wyniki agregacji nie mają identyfikatorów obiektów Mongo, więc generujemy kilka własnych.
  • Oddzwonienie do agregacji musi być opakowane w Fibre. Używam tutaj Meteor.bindEnvironment, ale można również użyć Future, aby uzyskać więcej kontroli niskiego poziomu.

Jeśli zaczniesz łączyć wyniki takich publikacji, musisz dokładnie rozważyć, w jaki sposób losowo wygenerowane identyfikatory wpływają na pole scalania.Jednak prosta implementacja tego jest tylko standardową kwerendą bazy danych, z tą różnicą, że jest wygodniejsza w korzystaniu z interfejsów API Meteor po stronie klienta.

TL; DR wersja: Prawie zawsze, gdy pchają się od danych z serwerem, publish jest stosowniejsze method.

Aby uzyskać więcej informacji na temat różnych sposobów wykonywania agregacji, check out this post.

+2

Nie chcę opuścić tej odpowiedzi bez "dziękuję". To jest całkowicie niesamowita odpowiedź. Niedługo potem zostałem odsunięty od innego projektu, ale Andrew, oczywiście, w to włożyłeś dużo uwagi i jestem bardzo wdzięczny. –

+0

@SteveRoss nie ma za co. Dzięki za miłe słowa! –

+0

Kudos za doskonały przykład agregacji. To był jedyny, który pracował dla mnie. I że udało ci się to zrobić bez pakietu, z MongoInternals, i funkcją publikowania ... wisienką na czerwonym aksamicie. Dziękuję za podzielenie się! – AbigailW

1

Możesz użyć do tego 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; 
}); 
+0

Więc jestem szczęśliwie idąc coraz nowe wartości wejściowe i tak dalej. W którym momencie następuje przeniesienie na serwer. Bo widzę, że to zadziała, z wyjątkiem tego, że dane na kliencie jeszcze nie dotarły na serwer. –

+0

Zmieniłem oryginalne pytanie. –

+0

Możesz użyć zależności, aby w razie potrzeby utworzyć reaktywność. Zaktualizowałem swoją odpowiedź. Wynik może być nieco skomplikowany, w tej chwili nie jestem pewien, jak to zrobić w twoim przypadku. –

1

Jeśli chcesz reaktywności, użyj Meteor.publish zamiast Meteor.call. Jest taki przykład w docs, gdzie publikują liczbę wiadomości w danym pomieszczeniu (tuż nad dokumentacją dla this.userId), powinieneś być w stanie zrobić coś podobnego.

+0

Reaktywność wydaje się więc dobra, ponieważ za każdym razem, gdy ktoś aktualizuje przebieg, średnie zmiany. Tak więc zaleca się publikowanie/subskrybowanie zgodnie z tym, co mówisz. Ale wydaje mi się, że pub/sub jest bardziej do zwracania filtrowanych lub zmapowanych zbiorów niż wartości skalarne, takie jak średnia. Wygląda na to, że powinna to być tylko linia lub dwa kodu - nie sądzisz? –

+0

@SteveRoss Zgadzam się, że typowa funkcja taka jak ta powinna być łatwa do napisania, ale Meteor nie ma na to specjalnego wsparcia (jeszcze wersja 6.5), więc alternatywy używają metod/call lub publish/subscribe. Biorąc pod uwagę podobny przykład w dokumentach i korzyść z reaktywności, chciałbym pójść z publikacją/subskrypcją. –

+0

Nie ma nic złego w publikowaniu kolekcji zawierającej tylko jeden dokument o wartości skalarnej. A kto wie? Może pewnego dnia będziesz potrzebował więcej niż jednej średniej ... a potem możesz opublikować wiele dokumentów :) –

2

Zrobiłem to metodą "agregatu". (Wersja 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); 
    } 
}); 

}