MongoDB - Atualizar objetos na matriz de um documento (atualização aninhada)

204

Suponha que temos a seguinte coleção, sobre a qual tenho poucas perguntas:

{
    "_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 - Quero aumentar o preço de "item_name": "my_item_two" e, se não existir , deve ser anexado à matriz "items".

2 - Como posso atualizar dois campos ao mesmo tempo. Por exemplo, aumente o preço de "my_item_three" e, ao mesmo tempo, aumente o "total" (com o mesmo valor).

Prefiro fazer isso no lado do MongoDB, caso contrário, tenho que carregar o documento no lado do cliente (Python) e construir o documento atualizado e substituí-lo pelo existente no MongoDB.

ATUALIZAÇÃO Isto é o que eu tentei e funciona bem se o objeto existe :

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

Mas se a chave não existe, não faz nada. Também atualiza apenas o objeto aninhado. Não há como este comando atualizar o campo "total" também.

Majid
fonte
1
Eu acho que você não pode fazer isso no mongo, exceto talvez com muita dor usando o eval. O Mongo é muito limitado em operações de dados.
Antti Haapala
3
@Haapala: mongodb tem $ inc e atualização com upsert
jdi 9/12
1
@ jdi yes yes, mas isso não ajuda muito aqui, mas o que ele precisa é de vários $ incs, condicionalmente, e se o item não existir, é necessário um $ push.
Antti Haapala

Respostas:

237

Para a pergunta 1, vamos dividir em duas partes. Primeiro, incremente qualquer documento que tenha "items.item_name" igual a "my_item_two". Para isso, você precisará usar o operador posicional "$". Algo como:

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

Observe que isso apenas incrementará o primeiro subdocumento correspondente em qualquer matriz (portanto, se você tiver outro documento na matriz com "item_name" igual a "my_item_two", ele não será incrementado). Mas isso pode ser o que você deseja.

A segunda parte é mais complicada. Podemos enviar um novo item para uma matriz sem um "my_item_two" da seguinte maneira:

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

Para sua pergunta nº 2, a resposta é mais fácil. Para incrementar o total e o preço do item_three em qualquer documento que contenha "my_item_three", você pode usar o operador $ inc em vários campos ao mesmo tempo. Algo como:

db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
               {$inc : {total : 1 , "items.$.price" : 1}} ,
               false ,
               true);
matulef
fonte
Obrigado pela resposta. A resposta para a pergunta 2 é perfeita e funciona bem :) Mas para a pergunta 1, o problema é que você deve procurar o documento por "user_id" e não por "items.item_name". Nesse caso, não posso usar o Operador Posicional, pois não especifiquei a matriz na consulta de pesquisa. Também quero que as duas partes sejam feitas de uma só vez. (se possível)
Majid
Bons exemplos. Eu senti como se estivesse tornando essa direção aparente a partir dos links na minha resposta, mas continuei tendo resistência dizendo que não era o que ele estava procurando. Acho que o OP só precisava ver exemplos de como fazer vários $ inc.
Jdi
Para a pergunta 1, você ainda poderá fazer isso em duas partes, adicionando o user_id ao bit de consulta de cada uma das atualizações (modificarei a resposta de acordo). Terá que pensar mais sobre se é possível fazer de uma só vez.
matulef
6
Quais são os terceiro e quarto parâmetros no método de atualização? e porque?
Skmahawar # 27/15
5
@skmahawar em relação ao terceiro e quarto parâmetros, docs.mongodb.com/manual/reference/method/db.collection.update indica que essas opções são para "upsert" e "multi", respectivamente. Para upsert, se definido como true, cria um novo documento quando nenhum documento corresponde aos critérios de consulta. O valor padrão é false, que não insere um novo documento quando nenhuma correspondência é encontrada. "Para multi, se definido como true, atualiza vários documentos que atendem aos critérios de consulta. Se definido como false, atualiza um documento. O valor padrão é falsa.
Mike Strand
24

Não há como fazer isso em uma única consulta. Você precisa pesquisar o documento na primeira consulta:

Se o documento existir:

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

Outro

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

Não há necessidade de adicionar condição {$ne : "my_item_two" }.

Também no ambiente multithread, você deve tomar cuidado para que apenas um thread possa executar o segundo (inserir caixa, se o documento não for encontrado) por vez, caso contrário, serão inseridos documentos incorporados duplicados.

Udit
fonte
1
não, há uma maneira de fazer isso em uma única consulta, veja acima
Martijn Scheffer
1
@ MartijnScheffer, não vejo uma maneira de fazer isso como uma única consulta em qualquer lugar deste segmento. Até a "resposta" está usando 2 consultas iguais às desta resposta. Estou esquecendo de algo? Eu também estou esperando há uma maneira de fazer isso em uma consulta para evitar quaisquer problemas de multi threading
dferraro
@ Edit, como posso usar os itens. $. Price dentro da condição? quando tento adicionar uma condição como "incrementar apenas se o preço for maior que 0", ela não funciona - basicamente nada é atualizado. Posso usar isso na condição where ou estou perdendo alguma coisa? items. $. price: {$ gt: 0}
dferraro 14/17
algo deve ter desapareceu :)
Martijn Scheffer
3

Podemos usar o $setoperador para atualizar a matriz aninhada dentro do objeto arquivado, atualizar o valor

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