MongoDB — обновлять все записи во вложенном массиве, только если они существуют

#javascript #node.js #mongodb #mongoose #nosql

#javascript #node.js #mongodb #мангуст #nosql

Вопрос:

У меня есть многоуровневый вложенный документ (его динамический и некоторые уровни могут отсутствовать, но максимум 3 уровня). Я хочу обновить все дочерние и дочерние маршруты, если таковые имеются. Сценарий такой же, как в любом проводнике Windows, где маршрут всех вложенных папок должен изменяться при изменении маршрута родительской папки. Например. В приведенном ниже примере, если я нахожусь в route=="l1/l2a" , и его имя нужно отредактировать на «l2c», тогда я обновлю его маршрут as route="l1/l2c и обновлю маршрут всех дочерних элементов, чтобы сказать "l1/l2c/l3a" .

      {
    "name":"l1",
    "route": "l1",
    "children":
        [
            {
            "name": "l2a",
            "route": "l1/l2a",
            "children": 
                [
                    {
                    "name": "l3a",
                    "route": "l1/l2a/l3a"
                 }]
            },
            {
            "name": "l2b",
            "route": "l1/l2b",
            "children": 
                [
                    {
                    "name": "l3b",
                    "route": "l1/l2b/l3b"
                 }]
            }
      ]
     }
  

В настоящее время я могу перейти к точке, и я могу изменить ее имя и ТОЛЬКО ее маршрут следующим образом:

 router.put('/navlist',(req,res,next)=>{
newname=req.body.newName //suppose l2c
oldname=req.body.name //suppose l2a
route=req.body.route // existing route is l1/l2a
id=req.body._id


newroute=route.replace(oldname,newname); // l1/l2a has to be changed to l1/l2c
let segments = route.split('/');  
let query = { route: segments[0]};
let update, options = {};

let updatePath = "";
options.arrayFilters = [];
for(let i = 0; i < segments.length  -1; i  ){
    updatePath  = `children.$[child${i}].`;
    options.arrayFilters.push({ [`child${i}.route`]: segments.slice(0, i   2).join('/') });
} //this is basically for the nested children

updateName=updatePath 'name'
updateRoute=updatePath 'route';

update = { $setOnInsert: { [updateName]:newDisplayName,[updateRoute]:newroute } };      
NavItems.updateOne(query,update, options)
 })
  

Проблема в том, что я не могу редактировать маршруты его дочерних элементов, если таковые имеются, т.е. Это маршрут вложенной папки as l1/l2c/l3a . Хотя я пытался использовать $[] оператор следующим образом.

 updateChild = updatePath '.children.$[].route'
updateChild2 = updatePath '.children.$[].children.$[].route'
//update = { $set: { [updateChild]:'abc',[updateChild2]:'abc' } };
  

Важно, чтобы уровни были настраиваемыми, и поэтому я не знаю, есть ли «l3A» или нет. Например, может быть «l3A», но может не быть «l3B». Но мой код просто требует каждого правильного пути, иначе он выдает ошибку

 code 500 MongoError: The path 'children.1.children' must exist in the document in order to apply array updates.
  

Итак, вопрос в том, как я могу применить изменения с помощью $set к пути, который действительно существует, и как я могу отредактировать существующую часть маршрута. Если путь существует, это хорошо, и если путь не существует, я получаю СООБЩЕНИЕ ОБ ОШИБКЕ.

Комментарии:

1. является route:l1 уникальным?

2. Да, l1 уникален. На самом деле имена и маршруты не обязательно должны быть одинаковыми, но для простоты я сохранил их одинаковыми.

Ответ №1:

Обновить

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

Рабочий пример — https://mongoplayground.net/p/TKMsvpkbBMn

Структура

 [
  {
    "_id": 1,
    "name": "l1",
    "children": [
      2,
      3
    ]
  },
  {
    "_id": 2,
    "name": "l2a",
    "children": [
      4
    ]
  },
  {
    "_id": 3,
    "name": "l2b",
    "children": [
      5
    ]
  },
  {
    "_id": 4,
    "name": "l3a",
    "children": []
  },
  {
    "_id": 5,
    "name": "l3b",
    "children": []
  }
  

]

Вставить запрос

 db.collection.insert({"_id": 4, "name": "l3a", "children": []}); // Inserting empty array simplifies aggregation query 
  

Запрос обновления

 db.collection.update({"_id": 4}, {"$set": "name": "l3c"});
  

Агрегация

 db.collection.aggregate([
  {"$match":{"_id":1}},
  {"$lookup":{
    "from":"collection",
    "let":{"name":"$name","children":"$children"},
    "pipeline":[
      {"$match":{"$expr":{"$in":["$_id","$$children"]}}},
      {"$addFields":{"route":{"$concat":["$$name","/","$name"]}}},
      {"$lookup":{
        "from":"collection",
        "let":{"route":"$route","children":"$children"},
        "pipeline":[
          {"$match":{"$expr":{"$in":["$_id","$$children"]}}},
          {"$addFields":{"route":{"$concat":["$$route","/","$name"]}}}
        ],
        "as":"children"
      }}
    ],
    "as":"children"
  }}
])
  

Оригинал

Вы можете создать маршрут как тип и формат массива, прежде чем представлять его пользователю. Это значительно упростит вам обновления. Вы должны разбивать запросы на несколько обновлений, когда вложенные уровни не существуют (например, обновление уровня 2). Можно использовать транзакции для выполнения нескольких обновлений атомарным способом.

Что-то вроде

 [
  {
    "_id": 1,
    "name": "l1",
    "route": "l1",
    "children": [
      {
        "name": "l2a",
        "route": [
          "l1",
          "l2a"
        ],
        "children": [
          {
            "name": "l3a",
            "route": [
              "l1",
              "l2a",
              "l3a"
            ]
          }
        ]
      }
    ]
  }
]
  

обновление уровня 1

 db.collection.update({
  "_id": 1
},
{
  "$set": {
    "name": "m1",
    "route": "m1"
  },
  "$set": {
    "children.$[].route.0": "m1",
    "children.$[].children.$[].route.0": "m1"
  }
})
  

обновление уровня 2

 db.collection.update({
  "_id": 1
},
{
  "$set": {
    "children.$[child].route.1": "m2a",
    "children.$[child].name": "m2a"
  }
},
{
  "arrayFilters":[{"child.name": "l2a" }]
})


db.collection.update({
  "_id": 1
},
{
  "$set": {
    "children.$[child].children.$[].route.1": "m2a"
  }
},
{
  "arrayFilters":[{"child.name": "l2a"}]
})
  

обновление уровня 3

 db.collection.update({
  "_id": 1
},
{
  "$set": {
    "children.$[].children.$[child].name": "m3a"
    "children.$[].children.$[child].route.2": "m3a"
  }
},
{
  "arrayFilters":[{"child.name": "l3a"}]
})
  

Комментарии:

1. Обновления для дочерних элементов выдают ту же ошибку 500 и могут не решить мою основную проблему. Например. В обновлении уровня 1 «children.$[].children.$[].route.0»: «m1» создает ошибку 500 mongo. Тестовый случай — это когда «children.$[].children.$[].route. 0» не существует . НО @s7vr идея разделения маршрутов на массивы потрясающая. Это, несомненно, вносит больше ясности, когда я редактирую уже существующие значения элементов и устраняю необходимость сначала считывать значения. Хотя кажется, что общее выполнение обновления может потребовать аналогичных усилий (в отношении ошибки 500). Я искренне благодарю вас за ваше время и ОТЛИЧНЫЙ подход к редизайну.

2. Добро пожаловать — я обновил ответ, чтобы еще больше упростить ваши обновления с другим дизайном. Теперь мы можем использовать aggregation framework для заполнения ссылок. Поиграйте с ним и посмотрите, подходит ли он вашему варианту использования.

Ответ №2:

Я не думаю, что это возможно arrayFilted для обновления первого и второго уровней, но да, это возможно только для обновления третьего уровня,

Возможный способ — вы можете использовать обновление с конвейером агрегации, начиная с MongoDB 4.2,

Я просто предлагаю метод, вы можете упростить это и сократить запрос в соответствии с вашим пониманием!

Используется $map для повторения цикла дочернего массива и проверки условий с использованием $cond и объединения объектов с использованием $mergeObjects ,

 let id = req.body._id;
let oldname = req.body.name;
let route = req.body.route;
let newname = req.body.newName;

let segments = route.split('/');
  

ОБНОВЛЕНИЕ УРОВНЯ 1: игровая площадка

 // LEVEL 1: Example Values in variables
// let oldname = "l1";
// let route = "l1";
// let newname = "l4";
if(segments.length === 1) {
  let result = await NavItems.updateOne(
        { _id: id },
        [{
            $set: {
                name: newname,
                route: newname,
                children: {
                    $map: {
                        input: "$children",
                        as: "a2",
                        in: {
                            $mergeObjects: [
                                "$$a2",
                                {
                                    route: { $concat: [newname, "/", "$$a2.name"] },
                                    children: {
                                        $map: {
                                            input: "$$a2.children",
                                            as: "a3",
                                            in: {
                                                $mergeObjects: [
                                                    "$$a3",
                                                    { route: { $concat: [newname, "/", "$$a2.name", "/", "$$a3.name"] } }
                                                ]
                                            }
                                        }
                                    }
                                }
                            ]
                        }
                    }
                }
            }
        }]
    );
}
  

ОБНОВЛЕНИЕ УРОВНЯ 2: игровая площадка

 // LEVEL 2: Example Values in variables
// let oldname = "l2a";
// let route = "l1/l2a";
// let newname = "l2g";
else if (segments.length === 2) {
    let result = await NavItems.updateOne(
        { _id: id },
        [{
            $set: {
                children: {
                    $map: {
                        input: "$children",
                        as: "a2",
                        in: {
                            $mergeObjects: [
                                "$$a2",
                                {
                                    $cond: [
                                        { $eq: ["$$a2.name", oldname] },
                                        {
                                            name: newname,
                                            route: { $concat: ["$name", "/", newname] },
                                            children: {
                                                $map: {
                                                    input: "$$a2.children",
                                                    as: "a3",
                                                    in: {
                                                        $mergeObjects: [
                                                            "$$a3",
                                                            { route: { $concat: ["$name", "/", newname, "/", "$$a3.name"] } }
                                                        ]
                                                    }
                                                }
                                            }
                                        },
                                        {}
                                    ]
                                }
                            ]
                        }
                    }
                }
            }
        }]
    );
}
  

ОБНОВЛЕНИЕ УРОВНЯ 3: игровая площадка

 // LEVEL 3 Example Values in variables
// let oldname = "l3a";
// let route = "l1/l2a/l3a";
// let newname = "l3g";
else if (segments.length === 3) {
    let result = await NavItems.updateOne(
        { _id: id },
        [{
            $set: {
                children: {
                    $map: {
                        input: "$children",
                        as: "a2",
                        in: {
                            $mergeObjects: [
                                "$$a2",
                                {
                                    $cond: [
                                        { $eq: ["$$a2.name", segments[1]] },
                                        {
                                            children: {
                                                $map: {
                                                    input: "$$a2.children",
                                                    as: "a3",
                                                    in: {
                                                        $mergeObjects: [
                                                            "$$a3",
                                                            {
                                                                $cond: [
                                                                    { $eq: ["$$a3.name", oldname] },
                                                                    {
                                                                        name: newname,
                                                                        route: { $concat: ["$name", "/", "$$a2.name", "/", newname] }
                                                                    },
                                                                    {}
                                                                ]
                                                            }
                                                        ]
                                                    }
                                                }
                                            }
                                        },
                                        {}
                                    ]
                                }
                            ]
                        }
                    }
                }
            }
        }]
    );
}
  

Зачем отдельный запрос для каждого уровня?

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

Комментарии:

1. Благодарим вас за предоставление подходящего ответа в соответствии со сценарием. Здесь и там были внесены небольшие изменения 🙂 Способ итерации документа с использованием карт был для меня чем-то новым. Рад наградить награду.

Ответ №3:

вы не можете делать так, как хотите. Потому что mongo его не поддерживает. Я могу предложить вам извлечь необходимый элемент из mongo. Обновите его с помощью вашей пользовательской рекурсивной функции help. И сделайте db.collection.updateOne(_id, { $set: data })

 function updateRouteRecursive(item) {
  // case when need to stop our recursive function
  if (!item.children) {
    // do update item route and return modified item
    return item;
  }

  // case what happen when we have children on each children array
}