Como atualizar vários elementos de matriz no mongodb

183

Eu tenho um documento Mongo que contém uma matriz de elementos.

Eu gostaria de redefinir o .handledatributo de todos os objetos na matriz onde .profile= XX.

O documento está no seguinte formato:

{
    "_id": ObjectId("4d2d8deff4e6c1d71fc29a07"),
    "user_id": "714638ba-2e08-2168-2b99-00002f3d43c0",
    "events": [{
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 20,
            "data": "....."
        }
        ...
    ]
}

então, tentei o seguinte:

.update({"events.profile":10},{$set:{"events.$.handled":0}},false,true)

No entanto, ele atualiza apenas o primeiro elemento da matriz correspondente em cada documento. (Esse é o comportamento definido para $ - o operador posicional .)

Como posso atualizar todos os elementos da matriz correspondentes?

LiorH
fonte
2
A atualização de um subconjunto ou de todos os itens da matriz foi adicionada ao mongodb 3.6: docs.mongodb.com/manual/reference/operator/update/…
Jaap
não deixe de conferir arrayFilters e considerar qual consulta usar para tornar a atualização eficiente. Confira a resposta de Neil Lunn: stackoverflow.com/a/46054172/337401
Jaap
cheque minha resposta
Ucdemir

Respostas:

111

Nesse momento, não é possível usar o operador posicional para atualizar todos os itens em uma matriz. Veja JIRA http://jira.mongodb.org/browse/SERVER-1243

Como alternativa, você pode:

  • Atualize cada item individualmente (events.0.handled events.1.handled ...) ou ...
  • Leia o documento, faça as edições manualmente e salve-o substituindo o antigo (marque "Atualizar se atual" se desejar garantir atualizações atômicas)
Javier Ferrero
fonte
15
se você tiver um problema semelhante, vote neste problema - jira.mongodb.org/browse/SERVER-1243
LiorH
Na verdade, eu gosto do documento lido e salvo a abordagem. Mas eu costumava Couch antes Mongo para que abordagem parece mais natural, pois não há API consulta para Couch, apenas uma API REST para documentos inteiros
adam
1
Ambas as abordagens requerem uma quantidade bastante alta de memória, certo? Se há um monte de documentos que têm de ser procurado, e tem que carregar todos eles (ou as matrizes aninhadas), a fim de atualização ... + também um pouco problemático para implementar se isso tem que ser feito de forma assíncrona ...
Ixx
13
Todas as dificuldades técnicas à parte, é surpreendente que esse recurso não esteja disponível no MongoDB. Essa restrição tira muita liberdade de personalizar o esquema do banco de dados.
Sung Cho
5
Neil Lunn stackoverflow.com/a/46054172/337401 respondeu isso para a versão 3.6. Como essa é uma pergunta popular, pode valer a pena atualizar essa resposta aceita com uma referência à resposta de Neil Lunn.
Jaap
73

Com o lançamento do MongoDB 3.6 (e disponível no ramo de desenvolvimento do MongoDB 3.5.12), agora você pode atualizar vários elementos da matriz em uma única solicitação.

Isso usa a sintaxe do operador de atualização posicional filtrada$[<identifier>] apresentada nesta versão:

db.collection.update(
  { "events.profile":10 },
  { "$set": { "events.$[elem].handled": 0 } },
  { "arrayFilters": [{ "elem.profile": 10 }], "multi": true }
)

O "arrayFilters"que passou para as opções para .update()ou mesmo .updateOne(), .updateMany(), .findOneAndUpdate()ou .bulkWrite()método especifica as condições para combinar no identificador dada na instrução de atualização. Quaisquer elementos que correspondam à condição fornecida serão atualizados.

Observando que o "multi"dado no contexto da pergunta foi usado na expectativa de que isso "atualizasse vários elementos", mas isso não foi e ainda não é o caso. Seu uso aqui se aplica a "vários documentos", como sempre foi o caso ou agora especificado de outra forma como a configuração obrigatória .updateMany()nas versões modernas da API.

OBSERVAÇÃO: Ironicamente, uma vez que isso é especificado no argumento "opções" para.update() métodos semelhantes, a sintaxe geralmente é compatível com todas as versões recentes do driver de lançamento.

No entanto, isso não é verdade para o mongoshell, uma vez que a maneira como o método é implementado lá ("ironicamente para compatibilidade com versões anteriores") o arrayFiltersargumento não é reconhecido e removido por um método interno que analisa as opções para fornecer "compatibilidade com versões anteriores" com versões anteriores. Versões do servidor MongoDB e uma .update()sintaxe de chamada da API "herdada" .

Portanto, se você quiser usar o comando no mongoshell ou em outros produtos "baseados em shell" (especialmente o Robo 3T), precisará de uma versão mais recente do ramo de desenvolvimento ou do release de produção a partir da 3.6 ou superior.

Consulte também positional all $[]que também atualiza "vários elementos da matriz", mas sem aplicar às condições especificadas e se aplica a todos elementos da matriz em que essa é a ação desejada.

Consulte também Atualizando uma matriz aninhada com o MongoDB para saber como esses novos operadores posicionais se aplicam a estruturas de matriz "aninhadas", onde "matrizes estão dentro de outras matrizes".

IMPORTANTE - Instalações atualizadas de versões anteriores "podem" não habilitar os recursos do MongoDB, o que também pode causar falhas na instrução. Você deve garantir que seu procedimento de atualização seja concluído com detalhes como atualizações de índice e, em seguida, execute

   db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

Ou versão superior, conforme aplicável à sua versão instalada. ou seja, "4.0"para a versão 4 e seguintes no momento. Isso ativou recursos como os novos operadores de atualização posicional e outros. Você também pode verificar com:

   db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

Para retornar a configuração atual

Neil Lunn
fonte
9
A resposta aceita deve ser atualizada e consulte esta resposta.
Jaap
2
O que é elem?
user1063287
1
Isto está certo. Observe que o RoboMongo ainda não suporta arrayFilters, portanto, execute a atualização via CLI. stackoverflow.com/questions/48322834/…
drlff 28/09
obrigado, Neil, especialmente para a parte importante, exatamente o que eu precisava
janfabian
esse código retorna ERROR no pymongo. há erro: raise TypeError ("% s deve ser verdadeiro ou falso"% (opção))) TypeError: upsert deve ser verdadeiro ou falso
Vagif
67

O que funcionou para mim foi o seguinte:

db.collection.find({ _id: ObjectId('4d2d8deff4e6c1d71fc29a07') })
  .forEach(function (doc) {
    doc.events.forEach(function (event) {
      if (event.profile === 10) {
        event.handled=0;
      }
    });
    db.collection.save(doc);
  });

Eu acho que é mais claro para iniciantes no mongo e qualquer pessoa familiarizada com o JQuery e amigos.

Daniel Cerecedo
fonte
Eu estou usando db.posts.find({ 'permalink':permalink }).forEach( function(doc) {...e eu estou ficandoOops.. TypeError: Object # has no method 'forEach'
Squirrl
3
@Squirrl poderia estar desatualizado na versão mongodb? O documento é claro sobre como aplicar a função forEach em um cursor, mas não informa desde que versão é suportada. docs.mongodb.org/manual/reference/method/cursor.forEach
Daniel Cerecedo
@Squirrl trydb.posts.find(...).toArray().forEach(...)
marmor
Não podemos fazer isso sem usar Javascript? Quero executar essa atualização diretamente de um shell mongo sem usar a API Javascript.
Meliodas 12/03
1
Você pode escrever esta consulta no driver mongodb para java ou com spring-data-mongodb? Obrigado, kris
chiku
18

Isso também pode ser realizado com um loop while, que verifica se há algum documento que ainda tenha subdocumentos que não foram atualizados. Esse método preserva a atomicidade de suas atualizações (o que muitas das outras soluções aqui não).

var query = {
    events: {
        $elemMatch: {
            profile: 10,
            handled: { $ne: 0 }
        }
    }
};

while (db.yourCollection.find(query).count() > 0) {
    db.yourCollection.update(
        query,
        { $set: { "events.$.handled": 0 } },
        { multi: true }
    );
}

O número de vezes que o loop é executado será igual ao número máximo de vezes que os subdocumentos são profileiguais a 10 e handlediguais a 0 em qualquer um dos documentos da sua coleção. Portanto, se você tiver 100 documentos em sua coleção e um deles tiver três subdocumentos correspondentesquery e todos os outros documentos tiverem menos subdocumentos correspondentes, o loop será executado três vezes.

Esse método evita o risco de sobrecarregar outros dados que podem ser atualizados por outro processo enquanto esse script é executado. Também minimiza a quantidade de dados sendo transferidos entre cliente e servidor.

Sean
fonte
13

De fato, isso se relaciona com a questão de longa data em http://jira.mongodb.org/browse/SERVER-1243, na qual existem de fato vários desafios a uma sintaxe clara que suporta "todos os casos" em que há várias correspondências de matriz. encontrado. De fato, já existem métodos que "ajudam" em soluções para esse problema, como Operações em Massa que foram implementadas após esta postagem original.

Ainda não é possível atualizar mais de um elemento de matriz combinada em uma única instrução de atualização, portanto, mesmo com uma atualização "multi", tudo o que você poderá atualizar é apenas um elemento matemático na matriz para cada documento naquele único declaração.

A melhor solução possível no momento é encontrar e fazer o loop de todos os documentos correspondentes e processar as atualizações em massa que permitirão pelo menos que muitas operações sejam enviadas em uma única solicitação com uma resposta única. Opcionalmente, você pode usar .aggregate()para reduzir o conteúdo da matriz retornado no resultado da pesquisa apenas para aqueles que correspondem às condições da seleção de atualização:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$setDifference": [
               { "$map": {
                   "input": "$events",
                   "as": "event",
                   "in": {
                       "$cond": [
                           { "$eq": [ "$$event.handled", 1 ] },
                           "$$el",
                           false
                       ]
                   }
               }},
               [false]
            ]
        }
    }}
]).forEach(function(doc) {
    doc.events.forEach(function(event) {
        bulk.find({ "_id": doc._id, "events.handled": 1  }).updateOne({
            "$set": { "events.$.handled": 0 }
        });
        count++;

        if ( count % 1000 == 0 ) {
            bulk.execute();
            bulk = db.collection.initializeOrderedBulkOp();
        }
    });
});

if ( count % 1000 != 0 )
    bulk.execute();

A .aggregate()parte funcionará quando houver um identificador "exclusivo" para a matriz ou todo o conteúdo de cada elemento formar um elemento "exclusivo". Isso ocorre devido ao operador "set" $setDifferenceusado para filtrar quaisquer falsevalores retornados da $mapoperação usada para processar a matriz para correspondências.

Se o conteúdo da sua matriz não tiver elementos exclusivos, você pode tentar uma abordagem alternativa com $redact:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$redact": {
        "$cond": {
            "if": {
                "$eq": [ { "$ifNull": [ "$handled", 1 ] }, 1 ]
            },
            "then": "$$DESCEND",
            "else": "$$PRUNE"
        }
    }}
])

Onde a limitação é que, se "manipulado" era de fato um campo destinado a estar presente em outros níveis de documento, é provável que você obtenha resultados inesperados, mas é bom que esse campo apareça apenas em uma posição de documento e corresponda à igualdade.

Versões futuras (MongoDB post 3.1) até o momento da gravação terão uma $filteroperação mais simples:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$filter": {
                "input": "$events",
                "as": "event",
                "cond": { "$eq": [ "$$event.handled", 1 ] }
            }
        }
    }}
])

E todas as versões com suporte .aggregate()podem usar a seguinte abordagem $unwind, mas o uso desse operador torna a abordagem menos eficiente devido à expansão da matriz no pipeline:

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "events": { "$push": "$events" }
    }}        
])

Em todos os casos em que a versão do MongoDB suporta um "cursor" da saída agregada, é apenas uma questão de escolher uma abordagem e iterar os resultados com o mesmo bloco de código mostrado para processar as instruções de atualização em massa. Operações em massa e "cursores" da saída agregada são introduzidos na mesma versão (MongoDB 2.6) e, portanto, geralmente trabalham lado a lado no processamento.

Em versões ainda anteriores, provavelmente é melhor usar apenas .find()para retornar o cursor e filtrar a execução das instruções apenas o número de vezes que o elemento da matriz corresponde às .update()iterações:

db.collection.find({ "events.handled": 1 }).forEach(function(doc){ 
    doc.events.filter(function(event){ return event.handled == 1 }).forEach(function(event){
        db.collection.update({ "_id": doc._id },{ "$set": { "events.$.handled": 0 }});
    });
});

Se você está determinado a fazer atualizações "múltiplas" ou considera-as mais eficientes do que processar várias atualizações para cada documento correspondente, sempre pode determinar o número máximo de correspondências possíveis de matriz e apenas executar uma atualização "múltipla" que muitos vezes, até que basicamente não haja mais documentos para atualizar.

Uma abordagem válida para as versões MongoDB 2.4 e 2.2 também pode ser usada .aggregate()para encontrar este valor:

var result = db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "count": { "$sum": 1 }
    }},
    { "$group": {
        "_id": null,
        "count": { "$max": "$count" }
    }}
]);

var max = result.result[0].count;

while ( max-- ) {
    db.collection.update({ "events.handled": 1},{ "$set": { "events.$.handled": 0 }},{ "multi": true })
}

Seja qual for o caso, há certas coisas que você não deseja fazer na atualização:

  1. Não "atualize" a matriz: onde, se você acha que seria mais eficiente atualizar todo o conteúdo da matriz no código e apenas $seta matriz inteira em cada documento. Isso pode parecer mais rápido de processar, mas não há garantia de que o conteúdo da matriz não tenha sido alterado desde que foi lido e a atualização foi realizada. Embora $setainda seja um operador atômico, ele somente atualizará a matriz com o que "pensa" como sendo os dados corretos e, portanto, provavelmente substituirá quaisquer alterações que ocorram entre leitura e gravação.

  2. Não calcule os valores do índice para atualizar: onde semelhante à abordagem "one shot", você apenas elabora que posição 0e posição 2(e assim por diante) são os elementos para atualizar e codificá-los com uma declaração eventual como:

    { "$set": {
        "events.0.handled": 0,
        "events.2.handled": 0
    }}

    Novamente, o problema aqui é a "presunção" de que esses valores de índice encontrados quando o documento foi lido são os mesmos valores de índice na matriz no momento da atualização. Se novos itens forem adicionados à matriz de uma maneira que altere a ordem, essas posições não serão mais válidas e os itens errados serão de fato atualizados.

Portanto, até que exista uma sintaxe razoável determinada para permitir que vários elementos da matriz correspondidos sejam processados ​​na instrução de atualização única, a abordagem básica é atualizar cada elemento da matriz correspondida em uma declaração individual (idealmente em massa) ou essencialmente calcular os elementos máximos da matriz para atualizar ou continuar atualizando até que nenhum resultado modificado seja retornado. De qualquer forma, você deve "sempre" processar atualizações posicionais$ no elemento de matriz correspondente, mesmo que isso esteja atualizando apenas um elemento por instrução.

As operações em massa são, de fato, a solução "generalizada" para processar qualquer operação que funcione como "várias operações" e, como existem mais aplicativos para isso do que apenas atualizar elementos de matriz múltiplos com o mesmo valor, é claro que ela foi implementada já, e atualmente é a melhor abordagem para resolver esse problema.

Blakes Seven
fonte
8

Estou surpreso que isso ainda não tenha sido tratado no mongo. No geral, o mongo não parece ser ótimo quando se lida com sub-matrizes. Você não pode contar sub-matrizes simplesmente por exemplo.

Eu usei a primeira solução de Javier. Leia a matriz em eventos, faça um loop e construa o conjunto exp:

var set = {}, i, l;
for(i=0,l=events.length;i<l;i++) {
  if(events[i].profile == 10) {
    set['events.' + i + '.handled'] = 0;
  }
}

.update(objId, {$set:set});

Isso pode ser abstraído em uma função usando um retorno de chamada para o teste condicional

lukenofurther
fonte
Obrigado por isso! Não posso acreditar que esse recurso ainda não seja suportado nativamente! Utilizado para incrementar todos os itens de uma sub-matriz, para outros lendo ... para atualizar todos os itens, basta remover a instrução if.
Zaheer
9
Esta não é uma solução segura. Se um registro for adicionado enquanto você estiver executando a atualização, os dados serão corrompidos.
Merc
4

Eu estava procurando uma solução para isso usando o driver mais recente para o C # 3.6 e aqui está a correção em que eu finalmente resolvi. A chave aqui é usar "$ []" que, de acordo com o MongoDB, é novo na versão 3.6. Consulte https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up. S [] para mais informações.

Aqui está o código:

{
   var filter = Builders<Scene>.Filter.Where(i => i.ID != null);
   var update = Builders<Scene>.Update.Unset("area.$[].discoveredBy");
   var result = collection.UpdateMany(filter, update, new UpdateOptions { IsUpsert = true});
}

Para obter mais contexto, consulte meu post original aqui: Remova o elemento da matriz de TODOS os documentos usando o driver MongoDB C #

C0d3 0n3
fonte
4

O tópico é muito antigo, mas eu vim procurando respostas aqui, portanto, fornecendo uma nova solução.

Com o MongoDB versão 3.6+, agora é possível usar o operador posicional para atualizar todos os itens em uma matriz. Veja a documentação oficial aqui .

A consulta a seguir funcionaria para a pergunta feita aqui. Também verifiquei com o driver Java-MongoDB e funciona com sucesso.

.update(   // or updateMany directly, removing the flag for 'multi'
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},  // notice the empty brackets after '$' opearor
   false,
   true
)

Espero que isso ajude alguém como eu.

ersnh
fonte
1

Eu tentei o seguinte e está funcionando bem.

.update({'events.profile': 10}, { '$set': {'events.$.handled': 0 }},{ safe: true, multi:true }, callback function);

// função de retorno de chamada no caso de nodejs

Pranay Saha
fonte
Esse código apenas atualiza o primeiro item correspondente na matriz. Resposta errada.
7279 Hamlett
Funciona para a v2.6. Você pode estar em uma versão mais antiga? Aqui está o documento docs.mongodb.com/manual/reference/method/db.collection.update/…
Jialin Zou
1

Você pode atualizar todos os elementos no MongoDB

db.collectioname.updateOne(
{ "key": /vikas/i },
{ $set: { 
 "arr.$[].status" : "completed"
} }
)

Ele atualizará todo o valor "status" para "complete" na matriz "arr"

Se apenas um documento

db.collectioname.updateOne(
 { key:"someunique", "arr.key": "myuniq" },
 { $set: { 
   "arr.$.status" : "completed", 
   "arr.$.msgs":  {
                "result" : ""
        }
   
 } }
)

Mas se não for um, e você também não quiser que todos os documentos da matriz sejam atualizados, precisará percorrer o elemento e o bloco if

db.collectioname.find({findCriteria })
  .forEach(function (doc) {
    doc.arr.forEach(function (singlearr) {
      if (singlearr check) {
        singlearr.handled =0
      }
    });
    db.collection.save(doc);
  });
VIKAS KOHLI
fonte
0

Na verdade, o comando save é apenas na instância da classe Document. Que possuem muitos métodos e atributos. Portanto, você pode usar a função lean () para reduzir a carga de trabalho. Consulte aqui. https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

Outro problema com a função de salvar, que cria dados de conflito com o salvamento múltiplo ao mesmo tempo. Model.Update fará com que os dados sejam consistentes. Então, para atualizar vários itens na matriz do documento. Use sua linguagem de programação familiar e tente algo parecido com isto, eu uso o mangusto nisso:

User.findOne({'_id': '4d2d8deff4e6c1d71fc29a07'}).lean().exec()
  .then(usr =>{
    if(!usr)  return
    usr.events.forEach( e => {
      if(e && e.profile==10 ) e.handled = 0
    })
    User.findOneAndUpdate(
      {'_id': '4d2d8deff4e6c1d71fc29a07'},
      {$set: {events: usr.events}},
      {new: true}
    ).lean().exec().then(updatedUsr => console.log(updatedUsr))
})
user3176403
fonte
0

O operador $ [] seleciona toda a matriz aninhada. Você pode atualizar todos os itens da matriz com '$ []'

.update({"events.profile":10},{$set:{"events.$[].handled":0}},false,true)

Referência

Ucdemir
fonte
Você pode explicar por que você inclui o "falso, verdadeiro" no final aqui? Não consigo encontrá-lo na documentação.
garson 7/02
Resposta incorreta: o operador com todas as posições $[] apenas atualiza todos os campos na matriz especificada. O que funciona é o operador posicional filtrado que opera $[identifier]nos campos da matriz que correspondem às condições especificadas. Deve ser usado com o arrayFilters Refrence: docs.mongodb.com/manual/release-notes/3.6/#arrayfilters e docs.mongodb.com/manual/reference/operator/update/
Lawrence Eagles
0

Esteja ciente de que algumas respostas neste tópico sugerindo o uso de $ [] estão ERRADAS.

db.collection.update(
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},
   {multi:true}
)

O código acima atualizará "manipulado" para 0 para todos os elementos na matriz "events", independentemente de seu valor "profile". A consulta {"events.profile":10}é apenas para filtrar o documento inteiro, não os documentos da matriz. Nessa situação, é necessário usar $[elem]com arrayFilterspara especificar a condição dos itens da matriz, para que a resposta de Neil Lunn esteja correta.

Wenda Hu
fonte
0

Atualize o campo da matriz em vários documentos no mongo db.

Use $ pull ou $ push com a atualização de muitas consultas para atualizar os elementos da matriz no mongoDb.

Notification.updateMany(
    { "_id": { $in: req.body.notificationIds } },
    {
        $pull: { "receiversId": req.body.userId }
    }, function (err) {
        if (err) {
            res.status(500).json({ "msg": err });
        } else {
            res.status(200).json({
                "msg": "Notification Deleted Successfully."
            });
        }
    });
Irfan
fonte
0

Primeiro: seu código não funcionou porque você estava usando o operador posicional $que identifica apenas um elemento a ser atualizado em uma matriz, mas nem especifica explicitamente sua posição na matriz.

O que você precisa é do operador posicional filtrado $[<identifier>]. Ele atualiza todos os elementos que correspondem a uma condição de filtro de matriz.

Solução:

db.collection.update({"events.profile":10}, { $set: { "events.$[elem].handled" : 0 } },
   {
     multi: true,
     arrayFilters: [ { "elem.profile": 10 } ]
})

Visite mongodb doc aqui

O que o código faz:

  1. {"events.profile":10} filtra sua coleção e retorna os documentos correspondentes ao filtro

  2. O $setoperador de atualização: modifica os campos correspondentes dos documentos em que atua.

  3. {multi:true}Faz .update()modifica todos os documentos correspondentes ao filtro, portanto, se comportando comoupdateMany()

  4. { "events.$[elem].handled" : 0 } and arrayFilters: [ { "elem.profile": 10 } ] Essa técnica envolve o uso da matriz posicional filtrada com arrayFilters. a matriz posicional filtrada aqui $[elem]atua como um espaço reservado para todos os elementos nos campos da matriz que correspondem às condições especificadas no filtro da matriz.

Filtros de matriz

Lawrence Eagles
fonte