MongoDB - Обновить объекты в массиве документа (вложенное обновление)

144

Предположим, что мы имеем следующий сборник, в котором у меня есть несколько вопросов о:

{
    "_id" : ObjectId("4faaba123412d654fe83hg876"),
    "user_id" : 123456,
    "total" : 100,
    "items" : [
            {
                    "item_name" : "my_item_one",
                    "price" : 20
            },
            {
                    "item_name" : "my_item_two",
                    "price" : 50
            },
            {
                    "item_name" : "my_item_three",
                    "price" : 30
            }
    ]
}

1 - Я хочу увеличить цену для "item_name": "my_item_two" , и если он не существует, он должен быть добавлен к массиву "items".

2 - Как я могу обновить два поля одновременно. Например, увеличьте цену для "my_item_three" и в то же время увеличьте "общее" (с тем же значением).

Я предпочитаю делать это на стороне MongoDB, иначе мне нужно загрузить документ на стороне клиента (Python) и создать обновленный документ и заменить его на существующий в MongoDB.

UPDATE Это то, что я пробовал и работает отлично ЕСЛИ ИМЕЕТСЯ ИСТОРИЯ:

db.test_invoice.update({user_id : 123456 , "items.item_name":"my_item_one"} , {$inc: {"items.$.price": 10}})

Но если ключ не существует, он ничего не делает. Также он обновляет только вложенный объект. У этой команды нет способа обновить поле "total".

  • 1
    Я думаю, что вы не можете сделать это в монго, кроме, может быть, с большой болью, используя eval. Монго очень ограничен в данных операций.
  • 3
    @Haapala: у mongodb есть $ inc и обновление с upsert
Показать ещё 1 комментарий
Теги:

2 ответа

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

В вопросе №1 разложите его на две части. Во-первых, увеличьте любой документ, который имеет "items.item_name", равный "my_item_two" . Для этого вам придется использовать позиционный оператор "$". Что-то вроде:

 db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } , 
                {$inc : {"items.$.price" : 1} } , 
                false , 
                true);

Обратите внимание, что это только увеличит первый согласованный поддокумент в любом массиве (так что если у вас есть другой документ в массиве с "item_name", равным "my_item_two" , он не будет увеличиваться). Но это может быть то, что вы хотите.

Вторая часть сложнее. Мы можем вывести новый элемент в массив без "my_item_two" следующим образом:

 db.bar.update( {user_id : 123456, "items.item_name" : {$ne : "my_item_two" }} , 
                {$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
                false , 
                true);

По вашему вопросу №2 ответ проще. Чтобы увеличить общую сумму и цену item_three в любом документе, который содержит "my_item_three", вы можете одновременно использовать оператор $inc на нескольких полях. Что-то вроде:

db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
               {$inc : {total : 1 , "items.$.price" : 1}} ,
               false ,
               true);
  • 0
    Спасибо за ответ. Ответ на вопрос № 2 идеален и работает отлично :) Но для вопроса № 1 проблема в том, что вы должны искать документ по «user_id», а не по «items.item_name». В этом случае я не могу использовать позиционный оператор, так как я не указал массив в поисковом запросе. Также я хочу, чтобы обе части были выполнены за один раз. (если возможно)
  • 0
    Хорошие примеры. Я чувствовал, что я делал это направление очевидным из ссылок в моем ответе, но я продолжал получать сопротивление, говоря, что это не то, что он искал. Угадайте, что OP просто нужно, чтобы увидеть примеры того, как сделать несколько $ inc.
Показать ещё 7 комментариев
17

В одном запросе нет способа сделать это. Вы должны искать документ в первом запросе:

Если документ существует:

db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } , 
                {$inc : {"items.$.price" : 1} } , 
                false , 
                true);

Else

db.bar.update( {user_id : 123456 } , 
                {$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
                false , 
                true);

Не нужно добавлять условие {$ne : "my_item_two" }.

Также в многопоточной среде вы должны быть осторожны, чтобы только один поток мог выполнить второй (вставить регистр, если документ не был найден) за раз, в противном случае будут вставлены дублирующие вложенные документы.

  • 1
    нет, есть способ сделать это в одном запросе, см. выше
  • 1
    @MartijnScheffer, я не вижу способа сделать это одним запросом где-либо в этой теме. Даже «ответ» использует 2 запроса так же, как в этом ответе. Я что-то пропустил? Я также надеюсь, что есть способ сделать это в одном запросе, чтобы предотвратить любые проблемы многопоточности
Показать ещё 2 комментария

Ещё вопросы

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