Na verdade, mais adequado para mapReduce do que o framework de agregação, pelo menos na solução inicial do problema. A estrutura de agregação não tem conceito do valor de um documento anterior, ou o valor anterior "agrupado" de um documento, por isso não pode fazer isso.
Por outro lado, mapReduce possui um "escopo global" que pode ser compartilhado entre etapas e documentos à medida que são processados. Isso lhe dará o "total corrente" para o saldo atual no final do dia que você precisa.
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 }
}
)
Isso somará por agrupamento de datas e, em seguida, na seção "finalizar", fará uma soma cumulativa de cada dia.
"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 longo prazo, seria melhor ter uma coleção separada com uma entrada para cada dia e alterar o saldo usando
$inc
em uma atualização. Faça também um $inc
upsert no início de cada dia para criar um novo documento transportando o saldo do dia anterior:// 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 }
);
Como NÃO fazer isso
Embora seja verdade que desde a escrita original há mais operadores introduzidos no framework de agregação, o que está sendo perguntado aqui ainda não é prático fazer em uma declaração de agregação.
A mesma regra básica se aplica que a estrutura de agregação não pode referenciar um valor de um "documento" anterior, nem pode armazenar uma "variável global". "Hackeando" isso por coerção de todos os resultados em uma matriz:
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" } }
])
Essa não é uma solução de alto desempenho ou "segura" considerando que conjuntos de resultados maiores executam a probabilidade muito real de violar o limite de 16 MB de BSON. Como uma "regra de ouro" , qualquer coisa que proponha colocar TODO o conteúdo dentro do array de um único documento:
{ "$group": {
"_id": null,
"docs": { "$push": "$$ROOT" }
}}
então isso é uma falha básica e, portanto, não é uma solução .
Conclusão
As maneiras muito mais conclusivas de lidar com isso normalmente seriam o pós-processamento no cursor de execução dos resultados:
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 });
})
Então, em geral, é sempre melhor:
-
Use a iteração do cursor e uma variável de rastreamento para totais. OmapReduce
sample é um exemplo inventado do processo simplificado acima.
-
Use totais pré-agregados. Possivelmente em conjunto com a iteração do cursor, dependendo do seu processo de pré-agregação, seja apenas intervalo total ou um total de execução "transportado".
A estrutura de agregação deve realmente ser usada para "agregar" e nada mais. Forçar coerções nos dados por meio de processos, como manipular em uma matriz apenas para processar como você deseja, não é sábio nem seguro e, o mais importante, o código de manipulação do cliente é muito mais limpo e eficiente.
Deixe os bancos de dados fazerem as coisas em que são bons, pois suas "manipulações" são muito melhor tratadas no código.