Как я могу динамически возвращать суммы всех свойств в массиве однородных объектов? - PullRequest
0 голосов
/ 14 ноября 2018

У меня есть коллекция MongoDB с такой структурой:

{
  "_id": "5bea815d2791a76283a2747a",
  "salesCategories": [
    "cake",
    "pie",
    "baklava"
  ],
  "sales": [
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:33:05.703Z",
      "_id": "5beba580b60f1a52755a85ec",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:33:06.352Z",
      "_id": "5beba581b60f1a52755a85ed",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": false,
      "updatedAt": "2018-11-14T04:33:06.995Z",
      "_id": "5beba582b60f1a52755a85ee",
      "date": "2018-11-15T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:35:49.212Z",
      "_id": "5beba582b60f1a52755a85ef",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": true,
      "updatedAt": "2018-11-14T04:36:19.590Z",
      "_id": "5beba625601d1e53cabbb6d8",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    },
    {
      "hidden": false,
      "updatedAt": "2018-11-14T04:35:42.027Z",
      "_id": "5beba643601d1e53cabbb6d9",
      "date": "2018-11-13T23:57:42.826Z",
      "salesTotals": {
        "cake": 10,
        "pie": 10,
        "baklava": 10
      }
    }
  ],
  "deposits": [],
  "name": "katie 3",
  "cogsPercentage": 0.12,
  "taxPercentage": 0.0975,
  "createdAt": "2018-11-13T07:46:37.955Z",
  "updatedAt": "2018-11-14T04:36:19.647Z",
  "__v": 0
}

Свойства salesTotals будут соответствовать свойствам salesCategories, но их может быть больше или меньше в зависимости от предпочтений пользователя. Поэтому подход не может заключаться в том, чтобы жестко закодировать суммы для каждого из свойств, как показано здесь.

Я пытаюсь использовать Mongoose для получения итоговых значений свойств в salesTotals для каждой категории продажи. Я также хочу иметь возможность не учитывать объекты в массиве продаж, для которых hidden установлено на true или между диапазонами дат для расчета. При использовании aggregate() я выяснил последние два требования, но понятия не имею, как динамически суммировать все содержимое этих объектов во всем массиве.

Вот то, что я хочу, чтобы желаемый результат был похож:

{
  "result": {
    "cake": 60,
    "pie": 60,
    "baklava": 60
  }
}

Я использую Mongo 4.0.2 и Mongoose 5.12.16.

1 Ответ

0 голосов
/ 14 ноября 2018

Основной ключ для работы с «именованными ключами», когда вы на самом деле не знаете, какие имена этих ключей заранее, использует $objectToArray, который преобразует ваш объект в «ключ / значение» пары как элементы массива таким образом, что вы действительно можете работать с ними. Это современная функция MongoDB, добавленная в более поздних выпусках для MongoDB 3.4 и, конечно, во всех текущих будущих версиях.

Существует несколько подходов различной сложности и производительности.

Современный редукционный массив

db.collection.aggregate([
  { "$project": {
    "sales": {
      "$reduce": {
        "input": {
          "$map": {
            "input": {
              "$filter": {
                "input": "$sales",
                "cond": { "$not": "$$this.hidden" }
              }
            },
            "in": { "$objectToArray": "$$this.salesTotals" }
          }
        },
        "initialValue": [],
        "in": { "$concatArrays": [ "$$value", "$$this" ] }
      }
    }

  }},
  { "$unwind": "$sales" },
  { "$group": {
    "_id": "$sales.k",
    "v": { "$sum": "$sales.v" }
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }
  }},
  { "$replaceRoot": {
    "newRoot": { "$arrayToObject": "$data" }
  }}
])

Использование $objectToArray и обратное преобразование с помощью $arrayToObject, так что на самом деле ни один из кодов не нуждается в «жестком кодировании» именованных ключей, на которых вы хотите накапливать.

$filter по существу удаляет значения hidden, а $map преобразует только то, что вам нужно. $reduce можно продолжить, но для накопления по документам вам все равно понадобится $unwind позже.

Конечно, если вы просто имеете в виду «за документ», вы можете настроить это $reduce намного дальше:

db.collection.aggregate([
  { "$replaceRoot": {
    "newRoot": {
      "$mergeObjects": [
        { "_id": "$_id" },
        {
          "$arrayToObject": {
            "$reduce": {
              "input": {
                "$reduce": {
                  "input": {
                    "$map": {
                      "input": {
                        "$filter": {
                          "input": "$sales",
                          "cond": { "$not": "$$this.hidden" }
                        }
                      },
                      "in": { "$objectToArray": "$$this.salesTotals" }
                    }
                  },
                  "initialValue": [],
                  "in": {
                    "$concatArrays": [ "$$value", "$$this" ]
                  }
                }
              },
              "initialValue": [],
              "in": {
                "$concatArrays": [
                  { "$filter": {
                    "input": "$$value",
                    "as": "val",
                    "cond": { "$ne": [ "$$this.k", "$$val.k" ] }
                  }},
                  [{ 
                    "k": "$$this.k",
                    "v": {
                      "$cond": {
                        "if": { "$in": [ "$$this.k", "$$value.k" ] },
                        "then": {
                          "$sum": [
                            { "$arrayElemAt": [
                              "$$value.v",
                              { "$indexOfArray": [ "$$value.k", "$$this.k" ] }
                            ]},
                            "$$this.v"
                          ]
                        },
                        "else": "$$this.v"
                      }
                    }
                  }]
                ]
              }
            }
          }
        }
      ]
    }
  }}
])

Те же имена динамических ключей, но только для каждого документа, и в этом случае вам вообще не нужно $unwind.

без $ уменьшить

Конечно, вы всегда можете делать такие вещи довольно традиционно:

db.collection.aggregate([
   { "$project": { "sales": "$sales" } },
   { "$unwind": "$sales" },
   { "$match": {
     "sales.hidden": { "$ne": true }
   }},
   { "$project": {
     "sales": { "$objectToArray": "$sales.salesTotals" }
   }},
   { "$unwind": "$sales" },
   { "$group": {
     "_id": "$sales.k",
     "v": { "$sum": "$sales.v" }
   }},
   { "$group": {
     "_id": null,
     "data": { "$push": { "k": "$_id", "v": "$v" } }
   }},
   { "$replaceRoot": {
     "newRoot": { "$arrayToObject": "$data" }
   }}
])

Это не выглядит сложным, но оно проходит через множество этапов, чтобы достичь результата. Таким образом, вместо $filter вы $unwind $match, а вместо $map вы делаете $project только для требуемых свойств.

Нет необходимости объединять массивы в документах, потому что каждый $unwind разбивает эти массивы на части.

В целом, это может быть просто и легко читаемо, но накладные расходы на выполнение значительно возрастают с большими коллекциями.

То же самое относится и к форме "единого документа":

db.collection.aggregate([
   { "$project": { "sales": "$sales" } },
   { "$unwind": "$sales" },
   { "$match": {
     "sales.hidden": { "$ne": true }
   }},
   { "$project": {
     "sales": { "$objectToArray": "$sales.salesTotals" }
   }},
   { "$unwind": "$sales" },
   { "$group": {
     "_id": {
       "_id": "$_id",
       "k": "$sales.k"
     },
     "v": { "$sum": "$sales.v" }
   }},
   { "$group": {
     "_id": "$_id._id",
     "data": { "$push": { "k": "$_id.k", "v": "$v" } }
   }},
   { "$replaceRoot": {
     "newRoot": {
       "$mergeObjects": [
         { "_id": "$_id" },
         { "$arrayToObject": "$data" }
       ]
     }
   }}
])

Существует лишь небольшое изменение в этапах $group в конце и, конечно, сохранение значения _id документа в конечном результате приводит к восстановлению ключей.


Конечно, результаты, как и ожидалось, могут быть такими:

{ 
  "baklava" : 20,
  "pie" : 20,
  "cake" : 20
}

Или для каждого документа (вы предоставили только один):

{
    "_id" : "5bea815d2791a76283a2747a",
    "cake" : 20,
    "pie" : 20,
    "baklava" : 20
}

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

Разобрать начальные формы может быть немного сложнее для понимания, но если вы потратите время на просмотр каждой части, вы в конечном итоге увидите, как они все сочетаются друг с другом.


Альтернативная картаReduce

Хотя вы не можете получить ту же производительность, что и структура агрегации, если у вас была MongoDB до поздней версии 3.4, вы всегда можете использовать mapReduce:

db.collection.mapReduce(
  function() {
    this.sales.forEach(s => {
      if (!s.hidden)
        emit(null, s.salesTotals);
    })
  },
  function(key,values) {
    var obj = {};

    values.forEach(value =>
      Object.keys(value).forEach(k => {
        if (!obj.hasOwnProperty(k))
          obj[k] = 0;
        obj[k] += value[k];
      })
    )

    return obj;
  },
  { out: { inline: 1 } }
)

Вывод немного отличается, поскольку mapReduce имеет строгую форму вывода «ключ / значение»:

    {
        "_id" : null,
        "value" : {
                "cake" : 20,
                "pie" : 20,
                "baklava" : 20
        }
    }

А для каждого документа достаточно просто заменить null в emit() текущим документом _id значение:

db.collection.mapReduce(
  function() {
    var id = this._id;
    this.sales.forEach(s => {
      if (!s.hidden)
        emit(id, s.salesTotals);
    })
  },
  function(key,values) {
    var obj = {};

    values.forEach(value =>
      Object.keys(value).forEach(k => {
        if (!obj.hasOwnProperty(k))
          obj[k] = 0;
        obj[k] += value[k];
      })
    )

    return obj;
  },
  { out: { inline: 1 } }
)

С довольно очевидными результатами:

    {
        "_id" : "5bea815d2791a76283a2747a",
        "value" : {
                "cake" : 20,
                "pie" : 20,
                "baklava" : 20
        }
    }

Не так быстро, но довольно простой процесс, который снова использует Object.keys() как способ извлечь произведение с "именованными ключами", не зная их имен.

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