In realtà più adatto a mapReduce rispetto al framework di aggregazione, almeno nella risoluzione iniziale dei problemi. Il framework di aggregazione non ha il concetto del valore di un documento precedente o del precedente valore "raggruppato" di un documento, ecco perché non può farlo.
D'altra parte, mapReduce ha un "ambito globale" che può essere condiviso tra fasi e documenti man mano che vengono elaborati. In questo modo otterrai il "totale corrente" per il saldo corrente alla fine della giornata di cui hai bisogno.
db.collection.mapReduce(
function () {
var date = new Date(this.dateEntry.valueOf() -
( this.dateEntry.valueOf() % ( 1000 * 60 * 60 * 24 ) )
);
emit( date, this.amount );
},
function(key,values) {
return Array.sum( values );
},
{
"scope": { "total": 0 },
"finalize": function(key,value) {
total += value;
return total;
},
"out": { "inline": 1 }
}
)
Verrà sommato per raggruppamento di date e quindi nella sezione "finalizza" verrà creata una somma cumulativa per ogni giorno.
"results" : [
{
"_id" : ISODate("2015-01-06T00:00:00Z"),
"value" : 50
},
{
"_id" : ISODate("2015-01-07T00:00:00Z"),
"value" : 150
},
{
"_id" : ISODate("2015-01-09T00:00:00Z"),
"value" : 179
}
],
A lungo termine sarebbe meglio avere una raccolta separata con una voce per ogni giorno e modificare il saldo utilizzando $inc
in un aggiornamento. Basta fare anche un $inc
upsert all'inizio di ogni giornata per creare un nuovo documento che riporti il saldo del giorno precedente:
// increase balance
db.daily(
{ "dateEntry": currentDate },
{ "$inc": { "balance": amount } },
{ "upsert": true }
);
// decrease balance
db.daily(
{ "dateEntry": currentDate },
{ "$inc": { "balance": -amount } },
{ "upsert": true }
);
// Each day
var lastDay = db.daily.findOne({ "dateEntry": lastDate });
db.daily(
{ "dateEntry": currentDate },
{ "$inc": { "balance": lastDay.balance } },
{ "upsert": true }
);
Come NON farlo
Se è vero che dalla scrittura originale ci sono più operatori introdotti nel framework di aggregazione, ciò che viene chiesto qui non è ancora pratico da fare in una dichiarazione di aggregazione.
Si applica la stessa regola di base che il framework di aggregazione non può fa riferimento a un valore da un "documento" precedente, né può memorizzare una "variabile globale". "Hacking" questo per coercizione di tutti i risultati in un array:
db.collection.aggregate([
{ "$group": {
"_id": {
"y": { "$year": "$dateEntry" },
"m": { "$month": "$dateEntry" },
"d": { "$dayOfMonth": "$dateEntry" }
},
"amount": { "$sum": "$amount" }
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": null,
"docs": { "$push": "$$ROOT" }
}},
{ "$addFields": {
"docs": {
"$map": {
"input": { "$range": [ 0, { "$size": "$docs" } ] },
"in": {
"$mergeObjects": [
{ "$arrayElemAt": [ "$docs", "$$this" ] },
{ "amount": {
"$sum": {
"$slice": [ "$docs.amount", 0, { "$add": [ "$$this", 1 ] } ]
}
}}
]
}
}
}
}},
{ "$unwind": "$docs" },
{ "$replaceRoot": { "newRoot": "$docs" } }
])
Questa non è né una soluzione performante né "sicura" considerando che set di risultati più grandi corrono la probabilità molto reale di violare il limite BSON di 16 MB. Come "regola d'oro" , tutto ciò che propone di inserire TUTTO il contenuto all'interno dell'array di un singolo documento:
{ "$group": {
"_id": null,
"docs": { "$push": "$$ROOT" }
}}
allora questo è un difetto di base e quindi non una soluzione .
Conclusione
I modi molto più conclusivi per gestirlo in genere sarebbero la post-elaborazione sul cursore in esecuzione dei risultati:
var globalAmount = 0;
db.collection.aggregate([
{ $group: {
"_id": {
y: { $year:"$dateEntry"},
m: { $month:"$dateEntry"},
d: { $dayOfMonth:"$dateEntry"}
},
amount: { "$sum": "$amount" }
}},
{ "$sort": { "_id": 1 } }
]).map(doc => {
globalAmount += doc.amount;
return Object.assign(doc, { amount: globalAmount });
})
Quindi in generale è sempre meglio:
-
Usa l'iterazione del cursore e una variabile di tracciamento per i totali. Il
mapReduce
sample è un esempio artificiale del processo semplificato di cui sopra. -
Utilizza i totali preaggregati. Forse in combinazione con l'iterazione del cursore a seconda del processo di pre-aggregazione, sia che si tratti solo di un totale di intervallo o di un totale parziale "portato avanti".
Il framework di aggregazione dovrebbe davvero essere utilizzato per "aggregare" e nient'altro. Forzare le coercizioni sui dati tramite processi come la manipolazione in un array solo per elaborare come si desidera non è né saggio né sicuro e, soprattutto, il codice di manipolazione del client è molto più pulito ed efficiente.
Consenti ai database di fare le cose in cui sono bravi, poiché le tue "manipolazioni" sono invece gestite molto meglio nel codice.