Условно $ проект из разных полей - PullRequest
0 голосов
/ 23 ноября 2018

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

{
  fruits: [
    {
      id: 123
    },
    {
      id: 456
    }
  ]
}

(Важно отметить, что id не является типом ObjectId BSON, это просто случайная серия символов, сгенерированнаяна стороне клиента).

... но теперь ключ id изменен на.

{
  fruits: [
    {
      fruit_id: 'xxx'
    },
    {
      fruit_id: 'yyy'
    }
  ]
}

Итак, я пытаюсь сделать $project, чтобы оба id и fruit_id изменились на что-то общее, например general_id, так что я могу продолжить с другой агрегацией, такой как $group, и просто обратиться к одному полю

Я пробовал что-то вроде:

[
  $unwind: {
    path: '$fruits'
  },
  $project: {
    general_id: {
      $cond: {
        if: {
          'fruits.fruit_id': {
            $type: ['string']
          }
        },
        then: '$fruits.fruit_id',
        else: '$fruits.id'
      }
    }
  }
]

1 Ответ

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

Это действительно зависит от того, что вы ищете, но для общего случая знания двух возможностей, вероятно, лучше использовать $ifNull, чтобы вернуть значение для поля, еслиприсутствует, в противном случае возвращает значение другого поля.

Добавление немного больше данных для демонстрации, так как вы, вероятно, не хотите терять что-либо еще в элементах массива:

{
  _id: 1,
  fruits: [
    {
      id: 123,
      data: 1
    },
    {
      id: 456,
      data: 2
    }
  ]
},
{
  _id: 2,
  fruits: [
    {
      fruit_id: 'xxx',
      data: 1
    },
    {
      fruit_id: 'yyy',
      data: 2
    }
  ]
},
{
  _id: 3,
  fruits: [
    {
      fruit_id: 'xxx',
      data: 1,
    },
    {
      fruit_id: 'yyy',
      data: 2
    },
    {
      id: 123,
      data: 3
    },
    {
      id: 456,
      data: 4
    }
  ]
}

Тогда вы можетелибо выполните процесс, используя $unwind в качестве шага первый , что делает именование путей более простым, особенно с $addFields вместо $project:

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$addFields": {
    "fruits": {
      "id": "$$REMOVE",
      "fruit_id": "$$REMOVE",
      "general_id": { "$ifNull": [ "$fruits.id", "$fruits.fruit_id" ] }
    }
  }}
])

Использует $$REMOVE из MongoDB 3.6 и выше (это должна быть минимальная версия, которую вы используете), чтобы "удалить" поля, которые выне хочуВам не нужно этого делать, и вы можете просто объявить все, что вы действительно хотите, с помощью $project, если у вас нет поддержки.

Тогда, конечно, есть альтернатива с $ifNull выражение.

Это дает результаты для этих данных, такие как:

{ "_id" : 1, "fruits" : { "data" : 1, "general_id" : 123 } }
{ "_id" : 1, "fruits" : { "data" : 2, "general_id" : 456 } }
{ "_id" : 2, "fruits" : { "data" : 1, "general_id" : "xxx" } }
{ "_id" : 2, "fruits" : { "data" : 2, "general_id" : "yyy" } }
{ "_id" : 3, "fruits" : { "data" : 1, "general_id" : "xxx" } }
{ "_id" : 3, "fruits" : { "data" : 2, "general_id" : "yyy" } }
{ "_id" : 3, "fruits" : { "data" : 3, "general_id" : 123 } }
{ "_id" : 3, "fruits" : { "data" : 4, "general_id" : 456 } }

Если вы хотите $group для этого значения, тонет необходимости в каком-либо промежуточном «проекте».Просто выполните $ifNull непосредственно на этом этапе:

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$group": {
    "_id": { "$ifNull": [ "$fruits.id", "$fruits.fruit_id" ] },
    "count": { "$sum": 1 }
  }}
])

И выведите:

{ "_id" : "yyy", "count" : 2 }
{ "_id" : "xxx", "count" : 2 }
{ "_id" : 456, "count" : 2 }
{ "_id" : 123, "count" : 2 }

Или, если вам на самом деле не нужно было $unwind массив для других целей, вы можете использовать $map и некоторые другие манипуляции с $objectToArray и $arrayToObject:

Model.aggregate([
  { "$addFields": {
    "fruits": {
      "$map": {
        "input": "$fruits",
        "in": {
          "$mergeObjects": [
            { "$arrayToObject": {
              "$filter": {
                "input": { "$objectToArray": "$$this" },
                "cond": { "$not": { "$in": [ "$$this.k", ["fruit_id","id"] ] } }
              }
            }},
            {
              "general_id": { "$ifNull": ["$$this.id","$$this.fruit_id"] }
            }
          ]
        }
      }
    }
  }}
])

Который возвращает результаты, такие как:

{
        "_id" : 1,
        "fruits" : [
                {
                        "data" : 1,
                        "general_id" : 123
                },
                {
                        "data" : 2,
                        "general_id" : 456
                }
        ]
}
{
        "_id" : 2,
        "fruits" : [
                {
                        "data" : 1,
                        "general_id" : "xxx"
                },
                {
                        "data" : 2,
                        "general_id" : "yyy"
                }
        ]
}
{
        "_id" : 3,
        "fruits" : [
                {
                        "data" : 1,
                        "general_id" : "xxx"
                },
                {
                        "data" : 2,
                        "general_id" : "yyy"
                },
                {
                        "data" : 3,
                        "general_id" : 123
                },
                {
                        "data" : 4,
                        "general_id" : 456
                }
        ]
}

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

На этот раз мы удалили id и fruit_id, преобразовав каждый элемент массива в массив "ключ /значение "пар через $objectToArray.Затем мы $filter массив на основе тех "k" значений, которые являются именами полей.$arrayToObject снова делает его объектом со всем другим содержимым, кроме этих полей.

$mergeObjects - $map то, что $addFields является корневым «документом», поскольку он берет несколько объектов и «объединяет» их вместе.Таким образом, «отфильтрованный» объект, как описано выше, и новый объект только с ключом general_id и его значением, переведенным из любого поля, присутствующего.

Списки из более чем двух полей

Какпоследнее замечание: $ifNull работает лучше, чем $cond, где у вас есть только два значения, но на самом деле ни то, ни другое не слишком велико, если есть большее возможноесписок.Вы можете вкладывать $cond выражений или даже использовать $switch, но на самом деле, вероятно, лучше всего фильтровать контент через $objectToArray, как показано ранее:

var valid_names = [ "id", "fruit_id", "apple_id", "orange_id" ];

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$group": {
    "_id": {
      "$arrayElemAt": [
        { "$map": {
          "input": {
            "$filter": {
              "input": { "$objectToArray": "$fruits" },
              "cond": { "$in": [ "$$this.k", valid_names ] }
            }
          },
          "in": "$$this.v"
        }},
        0
      ]
    },
    "count": { "$sum": 1 }
  }}  
])

Это обычно имеет смысл, иначе для работы с таким списком в динамическом режиме вы в конечном итоге создадите этапы конвейера агрегации в коде, например, с использованием $switchbe:

var valid_names = [ "id", "fruit_id", "apple_id", "orange_id" ];

var branches = valid_names.map(name => 
  ({
    "case": { "$gt": [`$fruits.${name}`, null ] },
    "then": `$fruits.${name}`
  })
)

Model.aggregate([
  { "$unwind": "$fruits" },
  { "$group": {
    "_id": { "$switch": { branches, "default": null } },
    "count": { "$sum": 1 }
  }}
])

, который выглядит чище в вашем коде, но на самом деле отправляет гораздо больший конвейер в BSON:

[
    { "$unwind" : "$fruits" },
    { "$group" : {
      "_id" : {
        "$switch" : {
          "branches" : [
           { 
             "case" : { "$gt" : [ "$fruits.id", null ] },
             "then" : "$fruits.id"
           },
           {
             "case" : { "$gt" : [ "$fruits.fruit_id", null ] },
             "then" : "$fruits.fruit_id"
           },
           {
             "case" : { "$gt" : [ "$fruits.apple_id", null ] },
             "then" : "$fruits.apple_id"
           },
           {
             "case" : { "$gt" : [ "$fruits.orange_id", null ] },
             "then" : "$fruits.orange_id"
           }
         ],
         "default" : null
        }
      },
      "count" : { "$sum" : 1 }
    }}
]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...