Я в основном собираюсь поговорить здесь о деталях «реализации», потому что есть здравый вывод о том, как на самом деле построить методы вокруг этого, как только вы поймете, как это на самом деле делается.
В идеальной ситуации вы на самом деле записываете свои данные «предварительно накопленными» за такие интервалы, как «месяц» или «день» во время хранения. Это обычно, как мы делаем это в средах с большим объемом, записывая накопленные итоги по мере их возникновения.
Без этого вы прибегаете к агрегации существующих данных, где вам следует использовать структуру агрегации. Обычно у вас есть хотя бы некоторый код для сообщения о различных временных интервалах, которые вы на самом деле не «предварительно накапливали» в любом случае.
Основная вещь, которую вы ищете здесь, это оператор $cond
. Это «троичное» или if/then/else
условие, которое позволяет выразить условие для if
, которое разветвляет логику для возврата значения с условием, истинным как then
или ложным как else
.
Это оператор, который позволяет нам посмотреть на "payment_type"
и решить, имеет ли значение «положительное» или «отрицательное» числовое представление при накоплении с $sum
. Итак, основное утверждение здесь:
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
Здесь применяется основная математика, поэтому остальная часть задачи накопления заключается в сборе «за интервал времени», и для этого есть несколько различных способов:
в месяц
Model.aggregate([
{ "$match": {
"payment_time": { "$gte": start_date, "$lt": end_date }
}},
{ "$group": {
"_id": {
"$dateFromParts": {
"year": { "$year": "$payment_time" },
"month": { "$month": "$payment_time" }
}
},
"balance": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
}
}}
])
Использование Date Math
Model.aggregate([
{ "$match": {
"payment_time": { "$gte": start_date, "$lt": end_date }
}},
{ "$group": {
"_id": {
"$add": [
{ "$subtract": [
{ "$subtract": ["$payment_time", new Date(0)] },
{ "$mod": [
{ "$subtract": ["$payment_time", new Date(0)] },
1000 * 60 * 60 * 24
]}
]},
{ "$multiply": [
{ "$subtract": [{ "$dayOfMonth": "$payment_time" }, 1] },
-1000 * 60 * 60 * 24
]},
new Date(0)
]
},
"balance": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
}
}}
])
Операторы с обычной датой
Model.aggregate([
{ "$match": {
"payment_time": { "$gte": start_date, "$lt": end_date }
}},
{ "$group": {
"_id": {
"year": { "$year": "$payment_time" },
"month": { "$month": "$payment_time" }
},
"balance": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
}
}}
])
Разница между первыми двумя методами и последним заключается в том, что эти первоначальные методы действительно возвращают объект Date BSON, который будет представлен в NodeJS как стандартный объект JavaScript Date
. Оба с использованием $dateFromParts
и «математический» подход по существу возвращают «округленную» дату, представляющую первый день месяца.
Обычно такие вещи, как «банковские выписки», фактически выпускаются в «определенный день месяца». Это действительно требует только расширения представленной логики для настройки, просто «переделывая» день возвращения.
Скажите за "15-й" день каждого месяца:
"_id": {
"$add": [
{ "$subtract": [
{ "$subtract": ["$payment_time", new Date(0)] },
{ "$mod": [
{ "$subtract": ["$payment_time", new Date(0)] },
1000 * 60 * 60 * 24
]}
]},
{ "$multiply": [
{ "$subtract": [{ "$dayOfMonth": "$payment_time" }, 1] },
-1000 * 60 * 60 * 24
]},
1000 * 60 * 60 * 24 * (15-1), // n-1 days adjusting
new Date(0)
]
},
Что является серьезной причиной, почему я предпочитаю "математический" подход, так как он намного более гибкий, чем другие формы. Вы можете сделать это с теми, которые полагаются на другие «операторы даты», но в конечном итоге они применяют «условия диапазона» в день месяца таким образом, что для реализации требуется гораздо больше логики. Однако добавление еще одного числа в массив! Что может быть проще, чем это?
Конечно, есть и такие вещи, как $dateFromString
и $dateToString
в качестве дополнительных способов сделать это с Modern MongoDB, однако приведение к «string» является дорогостоящей операцией. Чем больше операций, которые вы выполняете для преобразования чего-либо, по существу числового, в строковую форму для манипулирования, в конечном итоге имеет кумулятивный эффект, который равнозначно влияет на производительность. А в современном мире плата за вычислительные циклы и передачу данных равняется $$ money $$. И я cheapskate , так что мне нравится, чтобы вещи были эффективными.
Ежедневно
То же самое, что и раньше, с небольшим разбросом по получению "ежедневных" интервалов
Model.aggregate([
{ "$match": {
"payment_time": { "$gte": start_date, "$lt": end_date }
}},
{ "$group": {
"_id": {
"$dateFromParts": {
"year": { "$year": "$payment_time" },
"month": { "$month": "$payment_time" },
"day": { "$dayOfMonth": "$payment_time" }
}
},
"balance": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
}
}}
])
Использование Date Math
Model.aggregate([
{ "$match": {
"payment_time": { "$gte": start_date, "$lt": end_date }
}},
{ "$group": {
"_id": {
"$add": [
{ "$subtract": [
{ "$subtract": ["$payment_time", new Date(0)] },
{ "$mod": [
{ "$subtract": ["$payment_time", new Date(0)] },
1000 * 60 * 60 * 24
]}
]},
new Date(0)
]
},
"balance": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
}
}}
])
Операторы с обычной датой
Model.aggregate([
{ "$match": {
"payment_time": { "$gte": start_date, "$lt": end_date }
}},
{ "$group": {
"_id": {
"year": { "$year": "$payment_time" },
"month": { "$month": "$payment_time" },
"day": { "$dayOfMonth": "$payment_time" }
},
"balance": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$payment_type", "deposit" ] },
"then": "$payment_amount",
"else": { "$subtract": [ 0, "$payment_amount" ] }
}
}
}
}}
])
Примечательно, что здесь другие подходы «добавили» оператор $dayOfMonth
, а подход «Math» фактически удалил этот же оператор из рассмотрения. Причина этого в том, что в математических операциях трудно иметь дело с «месяцами» из-за основного факта, что каждый месяц меняется по количеству дней, которые у него есть. Поэтому то, что мы, по сути, делаем в «ежемесячном» округлении, учитывает текущий день, чтобы найти начало месяца.
Суть в том, что та же самая представленная «математика» фактически применима к ЛЮБОМУ временному интервалу, будь то дни или годы, часы или секунды. ЕДИНСТВЕННОЕ время, которое вам нужно изменить и откорректировать, - это накопление «месяца», в котором количество дней не совпадает.
Также обратите внимание на «повторение» здесь, поскольку все очень похоже. В лучшем случае у вас должно быть два метода, но на самом деле единственной частью, которая требует изменения, является аккумулятор между месяцем и любым другим интервалом, особенно если вы используете математический подход. Так что, честно говоря, один метод, который определяет разницу между «ежемесячным» и «всем остальным», - это все, что вам действительно нужно, так как конвейеры агрегации в любом случае являются просто структурами данных.
Так что "манипулируйте" ими. Как и любая другая структура данных.
ПРИМЕЧАНИЕ Вопрос на самом деле не упоминает ничего, кроме «даты», для которой в поле должно быть что-либо накоплено. Если вы хотите что-то вроде "player_id"
, вы просто добавляете это к "_id"
этапа "$group"
как часть составного ключа. «Составной ключ» демонстрируется в каждом операторах простой даты в разделе выше.