Размотайте несколько массивов документов в новые документы

1

Сегодня я столкнулся с ситуацией, когда мне нужно синхронизировать коллекцию mongoDB с Vertica (SQL Database), где моими объектными ключами будут столбцы таблицы в SQL. Я использую структуру агрегации mongoDB, сначала запрашиваю, обрабатываю и проектирую желаемый результирующий документ, а затем синхронизирую его с vertica.

Схема, которую я хочу заполнить, выглядит следующим образом:

{
  userId: 123
  firstProperty: {
    firstArray: ['x','y','z'],
    anotherAttr: 'abc'
  },
  anotherProperty: {
    secondArray: ['a','b','c'],
    anotherAttr: 'def'
  }  
}

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

db.collection('myCollection').aggregate([
            {
                $match: {
                    $or: [
                        {'firstProperty.firstArray.1': {$exists: true}},
                        {'secondProperty.secondArray.1': {$exists: true}}
                    ]
                }
            },
            {
                $project: {
                    userId: 1,
                    firstProperty: 1,
                    secondProperty: 1
                }
            }, {
                $unwind: {path:'$firstProperty.firstAray'}
            }, {
                $unwind: {path:'$secondProperty.secondArray'},
            }, {
                $project: {
                    userId: 1,
                    firstProperty: '$firstProperty.firstArray',
                    firstPropertyAttr: '$firstProperty.anotherAttr',
                    secondProperty: '$secondProperty.secondArray',
                    seondPropertyAttr: '$secondProperty.anotherAttr'
                }
            }, {
                $out: 'another_collection'
            }
        ])

Я ожидаю следующего результата:

{
  userId: 'x1',
  firstProperty: 'x',
  firstPropertyAttr: 'a'
}
{
  userId: 'x1',
  firstProperty: 'y',
  firstPropertyAttr: 'a'
}
{
  userId: 'x1',
  firstProperty: 'z',
  firstPropertyAttr: 'a'
}
{
  userId: 'x1',
  secondProperty: 'a',
  firstPropertyAttr: 'b'
}
{
  userId: 'x1',
  secondProperty: 'b',
  firstPropertyAttr: 'b'
}
{
  userId: 'x1',
  secondProperty: 'c',
  firstPropertyAttr: 'b'
}

Вместо этого я получаю что-то вроде этого:

{
  userId: 'x1',
  firstProperty: 'x',
  firstPropertyAttr: 'b'
  secondProperty: 'a',
  secondPropertyAttr: 'b'
}
{
  userId: 'x1',
  firstProperty: 'y',
  firstPropertyAttr: 'b'
  secondProperty: 'b',
  secondPropertyAttr: 'b'
}
{
  userId: 'x1',
  firstProperty: 'z',
  firstPropertyAttr: 'b'
  secondProperty: 'c',
  secondPropertyAttr: 'b'
}

Что именно мне не хватает, и как я могу это исправить?

  • 0
    Ваш ожидаемый результат содержит документы с разными именами полей. Некоторые из них имеют firstProperty, в то время как другие имеют secondProperty. Это то, что вы действительно хотите достичь?
  • 0
    Отмечая, что как ожидаемый, так и текущий выходные данные на самом деле не соответствуют предоставленным данным или не соответствуют предоставленному конвейеру агрегации. Но конвейер действительно дает понять, откуда данные «должны» поступать. В частности, ваш ожидаемый результат выглядит так, как будто вы потеряли отслеживание имен, но я думаю, что я придерживаюсь общей сути «ключей объекта как имен столбцов», которые вы намеревались представить.
Показать ещё 1 комментарий
Теги:
aggregation-framework

1 ответ

0
Лучший ответ

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

Другая очевидная проблема в вашей попытке называется "декартовым произведением". Здесь вы $unwind один массив и затем $unwind другой, что приводит к повторению элементов из "первого" $unwind для каждого значения, присутствующего в "секунде".

Обращаясь к этой второй проблеме, основной подход заключается в том, чтобы "объединить массивы", чтобы вы только $unwind от одного источника. Это довольно распространено для всех остальных подходов.

Что касается подходов, то они отличаются версией MongoDB, доступной вам и общей практичностью применения. Итак, дайте им пройти:

Удалите названные ключи

Самый простой подход заключается в том, чтобы просто не ожидать именованных ключей в выходе и вместо этого отмечать их как "name" идентифицирующее их источник в конечном результате. Итак, все, что мы хотим сделать, это указать каждый "ожидаемый" ключ в построении начального "комбинированного" массива, а затем просто $filter который для любых null значений, полученных из названных путей, не существующих в настоящем документе.

db.getCollection('myCollection').aggregate([
  { "$match": {
    "$or": [
      { "firstProperty.firstArray.0": { "$exists": true } },
      { "anotherProperty.secondArray.0": { "$exists": true } }
    ]  
  }},
  { "$project": {
    "_id": 0,
    "userId": 1,
    "combined": {
      "$filter": {
        "input": [
          { 
            "name": { "$literal": "first" },
            "array": "$firstProperty.firstArray",
            "attr": "$firstProperty.anotherAttr"
          },
          {
            "name": { "$literal": "another" },
            "array": "$anotherProperty.secondArray",
            "attr": "$anotherProperty.anotherAttr"
          }
        ],
        "cond": {
          "$ne": ["$$this.array", null ]
        }
      }
    }
  }},
  { "$unwind": "$combined" },
  { "$unwind": "$combined.array" },
  { "$project": {
    "userId": 1,
    "name": "$combined.name",
    "value": "$combined.array",
    "attr": "$combined.attr"
  }}
])

Из данных, включенных в ваш вопрос, это создаст:

/* 1 */
{
    "userId" : 123.0,
    "name" : "first",
    "value" : "x",
    "attr" : "abc"
}

/* 2 */
{
    "userId" : 123.0,
    "name" : "first",
    "value" : "y",
    "attr" : "abc"
}

/* 3 */
{
    "userId" : 123.0,
    "name" : "first",
    "value" : "z",
    "attr" : "abc"
}

/* 4 */
{
    "userId" : 123.0,
    "name" : "another",
    "value" : "a",
    "attr" : "def"
}

/* 5 */
{
    "userId" : 123.0,
    "name" : "another",
    "value" : "b",
    "attr" : "def"
}

/* 6 */
{
    "userId" : 123.0,
    "name" : "another",
    "value" : "c",
    "attr" : "def"
}

Объединить объекты - Требуется минимум MongoDB 3.4.4

Чтобы на самом деле использовать "named keys", нам нужны операторы $objectToArray и $arrayToObject которые были доступны только с MongoDB 3.4.4. Используя эти и $replaceRoot конвейера $replaceRoot мы можем просто обрабатывать желаемый результат без явного указания ключей для вывода на любом этапе:

db.getCollection('myCollection').aggregate([
  { "$match": {
    "$or": [
      { "firstProperty.firstArray.0": { "$exists": true } },
      { "anotherProperty.secondArray.0": { "$exists": true } }
    ]  
  }},
  { "$project": {
    "_id": 0,
    "userId": 1,
    "data": {
      "$reduce": {
        "input": {
          "$map": {
            "input": {
              "$filter": {
                "input": { "$objectToArray": "$$ROOT" },
                "cond": { "$not": { "$in": [ "$$this.k", ["_id", "userId"] ] } }
              }
            },
            "as": "d",
            "in": {
              "$let": {
                "vars": {
                  "inner": {
                    "$map": {
                      "input": { "$objectToArray": "$$d.v" },
                      "as": "i",
                      "in": {
                        "k": {
                          "$cond": {
                            "if": { "$ne": [{ "$indexOfCP": ["$$i.k", "Array"] }, -1] },
                            "then": "$$d.k",
                            "else": { "$concat": ["$$d.k", "Attr"] }
                          }  
                        },
                        "v": "$$i.v"
                      }
                    }
                  }
                },
                "in": {
                  "$map": {
                    "input": { 
                      "$arrayElemAt": [
                        "$$inner.v",
                        { "$indexOfArray": ["$$inner.k", "$$d.k"] } 
                      ]
                    },
                    "as": "v",
                    "in": {
                      "$arrayToObject": [[
                        { "k": "$$d.k", "v": "$$v" },
                        { 
                          "k": { "$concat": ["$$d.k", "Attr"] },
                          "v": {
                            "$arrayElemAt": [
                              "$$inner.v",
                              { "$indexOfArray": ["$$inner.k", { "$concat": ["$$d.k", "Attr"] }] }
                            ]
                          }
                        }
                      ]]
                    }
                  }
                }
              }
            }
          }
        },
        "initialValue": [],
        "in": { "$concatArrays": [ "$$value", "$$this" ] }
      }
    }
  }},
  { "$unwind": "$data" },
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": {
        "$concatArrays": [
          [{ "k": "userId", "v": "$userId" }],
          { "$objectToArray": "$data" }
        ]
      } 
    }   
  }}
])

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

Ключевые части, являющиеся $objectToArray по существу необходимы для "преобразования" ваших структур "вложенных ключей" в массивы "k" и "v" представляющие "имя" ключа и "значение". Это вызывается дважды, однократно для "внешних" частей документа и исключая "постоянные" поля, такие как "_id" и "userId" в такую структуру массива. Затем второй вызов обрабатывается на каждом из этих элементов массива, чтобы сделать эти "внутренние ключи" похожим "массивом".

Затем выполняется $indexOfCP с помощью $indexOfCP для определения которого "внутренний ключ" является значением для значения, а "Attr". Затем ключи переименовываются здесь в значение "внешнего" ключа, доступ к которому мы можем получить, потому что это "v" любезно предоставлено $objectToArray.

Затем для "внутреннего значения", который является "массивом", мы хотим, чтобы $map каждую запись в объединенный "массив", который в основном имеет форму:

[
  { "k": "firstProperty", "v": "x" },
  { "k": "firstPropertyAttr", "v": "abc" }
]

Это происходит для каждого элемента "внутренний массив", для которого $arrayToObject меняет процесс и превращает каждый "k" и "v" в "ключ" и "значение" объекта соответственно.

Так как вывод по-прежнему является "массивом массивов" "внутренних ключей" в этой точке, $concatArrays $reduce которые выводят и применяют $concatArrays при обработке каждого элемента, чтобы "присоединиться" к одному массиву для "data".

Остается только просто $unwind массив, созданный из каждого исходного документа, а затем применить $replaceRoot, который является частью, которая фактически позволяет "разные имена ключей" в "корне" каждого выходного документа.

"Слияние" здесь осуществляется путем подачи массива объектов с теми же "k" и "v" конструкциями, обозначенными для "userId", и "concatentating", что с преобразованием $objectToArray "data". Конечно, этот "новый массив" затем преобразуется в объект через $arrayToObject последний раз, который формирует аргумент "object" для "newRoot" как выражение.

Вы делаете что-то подобное, когда имеется большое количество "названных ключей", которые вы явно не можете назвать явно. И это фактически дает вам результат, который вы хотите:

/* 1 */
{
    "userId" : 123.0,
    "firstProperty" : "x",
    "firstPropertyAttr" : "abc"
}

/* 2 */
{
    "userId" : 123.0,
    "firstProperty" : "y",
    "firstPropertyAttr" : "abc"
}

/* 3 */
{
    "userId" : 123.0,
    "firstProperty" : "z",
    "firstPropertyAttr" : "abc"
}

/* 4 */
{
    "userId" : 123.0,
    "anotherProperty" : "a",
    "anotherPropertyAttr" : "def"
}

/* 5 */
{
    "userId" : 123.0,
    "anotherProperty" : "b",
    "anotherPropertyAttr" : "def"
}

/* 6 */
{
    "userId" : 123.0,
    "anotherProperty" : "c",
    "anotherPropertyAttr" : "def"
}

Именованные ключи без MongoDB 3.4.4 или больше

Без поддержки оператора, как показано в приведенном выше списке, просто невозможно, чтобы структура агрегации выводила документы с разными именами ключей.

Поэтому, хотя это невозможно дать указание "серверу" сделать это через $out, вы можете, конечно, просто перебрать курсор и написать новую коллекцию

var ops = [];

db.getCollection('myCollection').find().forEach( d => {
  ops = ops.concat(Object.keys(d).filter(k => ['_id','userId'].indexOf(k) === -1 )
    .map(k => 
      d[k][Object.keys(d[k]).find(ki => /Array$/.test(ki))]
        .map(v => ({
          [k]: v,
          ['${k}Attr']: d[k][Object.keys(d[k]).find(ki => /Attr$/.test(ki))]
      }))
    )
    .reduce((acc,curr) => acc.concat(curr),[])
    .map( o => Object.assign({ userId: d.userId },o) )
  );

  if (ops.length >= 1000) {
    db.getCollection("another_collection").insertMany(ops);
    ops = [];
  }

})

if ( ops.length > 0 ) {
  db.getCollection("another_collection").insertMany(ops);
  ops = [];
}

То же самое, что и в ранней агрегации, но просто "извне". Он по существу создает и массивы документов для каждого документа, соответствующие "внутренним" массивам, например:

[ 
    {
        "userId" : 123.0,
        "firstProperty" : "x",
        "firstPropertyAttr" : "abc"
    }, 
    {
        "userId" : 123.0,
        "firstProperty" : "y",
        "firstPropertyAttr" : "abc"
    }, 
    {
        "userId" : 123.0,
        "firstProperty" : "z",
        "firstPropertyAttr" : "abc"
    }, 
    {
        "userId" : 123.0,
        "anotherProperty" : "a",
        "anotherPropertyAttr" : "def"
    }, 
    {
        "userId" : 123.0,
        "anotherProperty" : "b",
        "anotherPropertyAttr" : "def"
    }, 
    {
        "userId" : 123.0,
        "anotherProperty" : "c",
        "anotherPropertyAttr" : "def"
    }
]

Они получают "кэшированные" в большой массив, который, когда достигает 1000 или более, наконец, записывается в новую коллекцию через .insertMany(). Конечно, это требует "обратной и четвертой" связи с сервером, но он делает работу максимально эффективным, если у вас нет функций, доступных для предыдущей агрегации.

Заключение

Общая точка здесь заключается в том, что если у вас на самом деле нет MongoDB, которые его поддерживают, то вы не собираетесь получать документы с "разными именами ключей" на выходе, исключительно из конвейера агрегации.

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

  • 0
    Первое решение, которое вы предложили («Удалить именованные ключи»), прекрасно делает то, что мне нужно. Мои данные выглядят немного иначе в реальности, но пример, который я привел, упрощает это. Большое спасибо.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню