MongoDB - Aktualizuj obiekty w tablicy dokumentu (aktualizacja zagnieżdżona)

204

Załóżmy, że mamy następującą kolekcję, o której mam kilka pytań:

{
    "_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 - Chcę podnieść cenę za „item_name”: „my_item_two”, a jeśli nie istnieje , należy go dołączyć do tablicy „items”.

2 - Jak mogę zaktualizować dwa pola jednocześnie. Na przykład podnieś cenę „my_item_three” i jednocześnie zwiększ „total” (o tej samej wartości).

Wolę to robić po stronie MongoDB, w przeciwnym razie muszę załadować dokument po stronie klienta (Python) i zbudować zaktualizowany dokument i zastąpić go istniejącym w MongoDB.

AKTUALIZACJA Oto, co próbowałem i działa dobrze JEŚLI Obiekt istnieje :

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

Ale jeśli klucz nie istnieje, nic nie robi. Aktualizuje także tylko zagnieżdżony obiekt. Za pomocą tego polecenia nie można również zaktualizować pola „suma”.

Majid
źródło
1
Myślę, że nie możesz tego zrobić w mongo, chyba że z dużym bólem przy użyciu eval. Mongo ma bardzo ograniczone operacje danych.
Antti Haapala
3
@Haapala: mongodb ma $ inc i aktualizację z upsert
jdi
1
@jdi tak tak, ale to niewiele tu pomaga, ale to, czego potrzebuje, to wielokrotność $ incs, warunkowo, a jeśli przedmiot nie istnieje, wówczas potrzebny jest $ push.
Antti Haapala,

Odpowiedzi:

237

W przypadku pytania nr 1 podzielmy je na dwie części. Najpierw zwiększ każdy dokument, który ma „item.item_name” równy „my_item_two”. W tym celu musisz użyć operatora pozycyjnego „$”. Coś jak:

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

Zauważ, że zwiększy to tylko pierwszy dopasowany dokument podrzędny w dowolnej tablicy (więc jeśli masz inny dokument w tablicy o nazwie „item_name” równej „my_item_two”, nie zostanie on zwiększony). Ale to może być to, czego chcesz.

Druga część jest trudniejsza. Możemy wypchnąć nowy element do tablicy bez „my_item_two” w następujący sposób:

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

Na pytanie nr 2 odpowiedź jest łatwiejsza. Aby zwiększyć sumę i cenę item_three w dowolnym dokumencie zawierającym „my_item_three”, możesz użyć operatora $ inc na wielu polach jednocześnie. Coś jak:

db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
               {$inc : {total : 1 , "items.$.price" : 1}} ,
               false ,
               true);
matulef
źródło
Dziękuję za odpowiedź. Odpowiedź na pytanie nr 2 jest idealna i działa dobrze :) Ale w przypadku pytania nr 1 problem polega na tym, że powinieneś szukać dokumentu według „user_id”, a nie „items.item_name”. W takim przypadku nie mogę użyć operatora pozycjonowania, ponieważ nie podałem tablicy w zapytaniu wyszukiwania. Chcę też, aby obie części były wykonane jednym strzałem. (jeśli to możliwe)
Majid
Ładne przykłady. Czułem, że wskazuję ten kierunek na podstawie linków w mojej odpowiedzi, ale ciągle napotykałem opór, mówiąc, że nie tego szuka. Zgadnij, że OP musiał tylko zobaczyć przykłady, jak zrobić wiele $ inc.
jdi
W przypadku pytania nr 1 powinieneś być w stanie zrobić to w dwóch częściach, dodając user_id do bitu zapytania każdej z aktualizacji (odpowiednio zmodyfikuję odpowiedź). Będę musiał przemyśleć, czy można to zrobić za jednym razem.
matulef
6
Jakie są 3 i 4 parametry w metodzie aktualizacji? i dlaczego?
skmahawar
5
@skmahawar w sprawie trzeciego i czwartego parametru, docs.mongodb.com/manual/reference/method/db.collection.update wskazuje, że te opcje dotyczą odpowiednio „upsert” i „multi”. W przypadku upsert, jeśli jest ustawiony na true, tworzy nowy dokument, gdy żaden dokument nie spełnia kryteriów zapytania. Wartością domyślną jest false, która nie wstawia nowego dokumentu, gdy nie zostanie znalezione dopasowanie. ”W przypadku wielu, jeśli ustawiona na wartość true, aktualizuje wiele dokumentów spełniających kryteria zapytania. W przypadku ustawienia na fałsz aktualizuje jeden dokument. Wartość domyślna to false
Mike Strand
24

Nie ma takiej możliwości w pojedynczym zapytaniu. Musisz przeszukać dokument przy pierwszym zapytaniu:

Jeśli dokument istnieje:

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

Jeszcze

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

Nie trzeba dodawać warunku {$ne : "my_item_two" }.

Również w środowisku wielowątkowym należy uważać, aby tylko jeden wątek mógł wykonać drugi (wstaw przypadek, jeśli dokument nie został znaleziony) naraz, w przeciwnym razie zostaną wstawione duplikaty osadzonych dokumentów.

Udit
źródło
1
nie, można to zrobić za pomocą jednego zapytania, patrz wyżej
Martijn Scheffer
1
@MartijnScheffer, nie widzę sposobu, aby to zrobić jako pojedyncze zapytanie nigdzie w tym wątku. Nawet „odpowiedź” używa 2 zapytań takich samych jak w tej odpowiedzi. Czy coś brakuje? Mam również nadzieję, że istnieje sposób, aby to zrobić w jednym zapytaniu, aby uniknąć problemów z wielowątkowością
dferraro
@Udit, jak mogę używać przedmiotów. $. Cena w stanie? gdy próbuję dodać warunek taki jak „przyrost tylko wtedy, gdy cena jest większa niż 0”, to nie działa - w zasadzie nic się nie aktualizuje. Czy mogę tego użyć w sytuacji, gdy coś mi brakuje? przedmioty. $. cena: {$ gt: 0}
dferraro
coś musiało zniknąć :)
Martijn Scheffer
3

Możemy użyć $setoperatora, aby zaktualizować zagnieżdżoną tablicę wewnątrz obiektu, aby zaktualizować wartość

db.getCollection('geolocations').update( 
   {
       "_id" : ObjectId("5bd3013ac714ea4959f80115"), 
       "geolocation.country" : "United States of America"
   }, 
   { $set: 
       {
           "geolocation.$.country" : "USA"
       } 
    }, 
   false,
   true
);
KARTHIKEYAN.A
źródło