Обновление вложенного массива с помощью MongoDB

Я пытаюсь обновить значение во вложенном массиве, но не могу заставить его работать.

Мой объект похож на этот

 {
    "_id": {
        "$oid": "1"
    },
    "array1": [
        {
            "_id": "12",
            "array2": [
                  {
                      "_id": "123",
                      "answeredBy": [],
                  },
                  {
                      "_id": "124",
                      "answeredBy": [],
                  }
             ],
         }
     ]
 }

Мне нужно нажать значение в массив "answerBy".

В приведенном ниже примере я попытался нажать строку "success" в массив "answerBy" объекта "123 _id", но он не работает.

callback = function(err,value){
     if(err){
         res.send(err);
     }else{
         res.send(value);
     }
};
conditions = {
    "_id": 1,
    "array1._id": 12,
    "array2._id": 123
  };
updates = {
   $push: {
     "array2.$.answeredBy": "success"
   }
};
options = {
  upsert: true
};
Model.update(conditions, updates, options, callback);

Я нашел эту ссылку , но ее ответ только говорит, что я должен использовать объект как структуру вместо массива. Это не может быть применено в моей ситуации. Мне действительно нужно, чтобы мой объект был вложен в массивы

Было бы здорово, если бы вы могли помочь мне здесь. Я потратил часы, чтобы понять это.

Заранее благодарю вас!

Ответ 1

Общие рамки и пояснения

Есть кое-что неправильное в том, что вы здесь делаете. Во-первых, ваши условия запроса. Вы имеете в виду несколько значений _id где вам не нужно, и по крайней мере один из них не находится на верхнем уровне.

Чтобы попасть в "вложенное" значение, а также предполагая, что значение _id уникально и не будет отображаться ни в каком другом документе, форма запроса должна выглядеть примерно так:

Model.update(
    { "array1.array2._id": "123" },
    { "$push": { "array1.0.array2.$.answeredBy": "success" } },
    function(err,numAffected) {
       // something with the result in here
    }
);

Теперь это действительно сработает, но на самом деле это всего лишь случайность, потому что есть очень веские причины, почему это не должно работать для вас.

Важное замечание содержится в официальной документации для оператора positional $ под темой "Вложенные массивы". Что это говорит:

Оператор positional $ не может использоваться для запросов, которые пересекают более одного массива, например запросов, которые пересекают массивы, вложенные в другие массивы, потому что замена для $ placeholder является единственным значением

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

Поэтому, если вы посмотрите на нотацию запроса, как показано, мы "жестко закодировали" первую (или индексную) позицию в массиве верхнего уровня, и так получилось, что соответствующий элемент внутри "array2" также является нулевым индексом.

Чтобы продемонстрировать это, вы можете изменить соответствующее значение _id на "124", и результат будет $push новой записи в элемент с _id "123", поскольку они оба находятся в нулевой индексной строке "array1", и это значение возвращается к заполнителю.

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

Старайтесь избегать вложенных массивов, поскольку вы столкнетесь с проблемами обновления, как показано.

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

 {
   "answers": [
     { "by": "success", "type2": "123", "type1": "12" }
   ]
 }

Или даже при принятии внутреннего массива только $push и никогда не обновляется:

 {
   "array": [
     { "type1": "12", "type2": "123", "answeredBy": ["success"] },
     { "type1": "12", "type2": "124", "answeredBy": [] }
   ]
 }

Которых обе поддаются атомным обновлениям в рамках оператора positional $


MongoDB 3.6 и выше

Из MongoDB 3.6 доступны новые функции для работы с вложенными массивами. Это использует синтаксис позиционного фильтра $[<identifier>], чтобы соответствовать конкретным элементам и применять различные условия через arrayFilters в операторе обновления:

Model.update(
  {
    "_id": 1,
    "array1": {
      "$elemMatch": {
        "_id": "12","array2._id": "123"
      }
    }
  },
  {
    "$push": { "array1.$[outer].array2.$[inner].answeredBy": "success" }
  },
  {
    "arrayFilters": [{ "outer._id": "12" },{ "inner._id": "123" }] 
  }
)

"arrayFilters" переданные параметрам .update() или даже .updateOne(), .updateMany(), .findOneAndUpdate() или .bulkWrite() условия для соответствия идентификатору, указанному в инструкции обновления. Любые элементы, соответствующие указанному условию, будут обновлены.

Поскольку структура "вложенная", мы фактически используем "несколько фильтров", как указано в "массиве" определений фильтров, как показано. Помеченный "идентификатор" используется в сопоставлении с синтаксисом позиционного фильтра $[<identifier>] фактически используемым в блоке обновления инструкции. В этом случае inner и outer являются идентификаторами, используемыми для каждого условия, как указано в вложенной цепочке.

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

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

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

ПРИМЕЧАНИЕ. Как ни странно, поскольку это указано в аргументе "options" для .update() и подобных методах, синтаксис обычно совместим со всеми версиями последних версий драйверов.

Однако это не относится к оболочке mongo, поскольку способ там реализован ("по иронии судьбы для обратной совместимости") аргумент arrayFilters не распознается и не удаляется внутренним методом, который анализирует параметры для доставки "обратной совместимости", с предыдущими версиями сервера MongoDB и синтаксисом вызова API-интерфейса "устаревший" .update().

Поэтому, если вы хотите использовать команду в оболочке mongo или других продуктах, основанных на оболочке (в частности, Robo 3T), вам понадобится последняя версия либо из ветки разработки, либо из производственной версии с 3,6 или более.

См. Также positional all $[] который также обновляет "несколько элементов массива", но не применяет к указанным условиям и относится ко всем элементам массива, где это желаемое действие.

Ответ 2

Я знаю, что это очень старый вопрос, но я просто сам боролся с этой проблемой и нашел, что я считаю лучшим ответом.

Способ решения этой проблемы - использовать Sub-Documents. Это делается путем вложения схем в ваши схемы

MainSchema = new mongoose.Schema({
   array1: [Array1Schema]
})

Array1Schema = new mongoose.Schema({
   array2: [Array2Schema]
})

Array2Schema = new mongoose.Schema({
   answeredBy": [...]
})

Таким образом, объект будет выглядеть так, как показано на рисунке, но теперь каждый массив заполняется субдокументами. Это позволяет расположить свой путь в поддоку, который вы хотите. Вместо использования .update вы затем используете .find или .findOne чтобы получить документ, который хотите обновить.

Main.findOne((
    {
        _id: 1
    }
)
.exec(
    function(err, result){
        result.array1.id(12).array2.id(123).answeredBy.push('success')
        result.save(function(err){
            console.log(result)
        });
    }
)

Не использовали .push() таким образом самостоятельно, поэтому синтаксис может быть неправильным, но я использовал и .set() и .remove(), и оба работают отлично.