Comment mettre à jour des éléments de tableaux multiples en mongodb

j'ai un document de Mongo qui contient un tableau d'éléments.

j'aimerais réinitialiser l'attribut .handled de tous les objets dans le tableau où .profile = XX.

le document se présente sous la forme suivante:

{
    "_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": "....."
        }
        ...
    ]
}

donc, j'ai essayé la suivante:

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

cependant, il ne met à jour que le premier élément de tableau apparié dans chaque document. (C'est la comportement défini pour $ - l'opérateur de position .)

Comment puis-je mettre à jour tous éléments du tableau appariés?

145
demandé sur Narendra Jadhav 2011-01-12 16:13:39

11 réponses

pour le moment, il n'est pas possible d'utiliser l'opérateur de position pour mettre à jour tous les éléments d'un tableau. Voir JIRA http://jira.mongodb.org/browse/SERVER-1243

comme un travail autour de vous pouvez:

  • mettre à jour chaque élément individuellement (événement.0.événements gérés.1.gérer ...) ou...
  • Lire le document, faire les modifications et la sauvegarder en remplaçant le plus ancien (cochez "mise à jour si Courant " si vous voulez assurer les mises à jour atomiques)
97
répondu Javier Ferrero 2018-04-24 19:49:56

ce qui a fonctionné pour moi était ceci:

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);
  });

je pense que c'est plus clair pour les newbies mongo et tous ceux qui connaissent JQuery & friends.

59
répondu Daniel Cerecedo 2015-08-25 09:36:29

avec la version de MongoDB 3.6 (et disponible dans la branche développement de MongoDB 3.5.12 ) vous pouvez maintenant mettre à jour plusieurs éléments de tableau dans une seule requête.

cela utilise la syntaxe de l'opérateur de mise à jour position filtrée $[<identifier>] introduite dans cette version:

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

le "arrayFilters" comme passé aux options pour .update() ou même .updateOne() , .updateMany() , .findOneAndUpdate() ou .bulkWrite() méthode spécifie les conditions à correspondre sur l'identifiant donné dans la déclaration de mise à jour. Tous les éléments qui correspondent à la condition donnée seront mis à jour.

notant que le "multi" comme indiqué dans le contexte de la question a été utilisé dans l'attente que cela " mettre à jour plusieurs les éléments" mais cela n'a pas été et n'est toujours pas le cas. Son usage s'applique ici à "plusieurs documents" comme cela a toujours été le cas ou maintenant spécifié comme le réglage obligatoire de .updateMany() dans les versions modernes de L'API.

NOTE assez ironiquement, puisque ceci est spécifié dans l'argument " options "pour .update() et les méthodes similaires, la syntaxe est généralement compatible avec toutes les versions récentes des pilotes de version.

cependant, ce n'est pas le cas de l'interpréteur de commandes mongo , car la façon dont la méthode y est implémentée ("ironie pour rétrocompatibilité") l'argument arrayFilters n'est pas reconnu et supprimé par une méthode interne qui analyse les options afin de fournir une" rétrocompatibilité "avec les versions antérieures du serveur MongoDB et une syntaxe D'appel API" legacy .update() .

donc si vous souhaitez utiliser la commande dans le shell mongo ou d'autres produits "basés sur le shell" (notamment Robo 3T ) vous avez besoin d'une dernière version de la branche de développement ou de la version de production à partir de 3.6 ou plus.

Voir aussi positional all $[] qui met également à jour "les éléments de tableaux multiples" mais sans s'appliquer aux conditions spécifiées et s'applique à tous les éléments dans le tableau où c'est le désiré action.

voir Aussi mise à Jour d'un Tableau Imbriqué avec MongoDB pour la façon dont les opérateurs de position s'applique à "imbriqués" tableau de structures, où "les tableaux sont dans les autres tableaux".

IMPORTANT - les installations mises à niveau des versions précédentes "peuvent" n'avoir pas activé les fonctionnalités de MongoDB, ce qui peut également causer des déclarations à l'échec. Vous devez vous assurer que votre procédure de mise à niveau est complète avec détails tels que les mises à niveau de l'index et ensuite lancer

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

cela a permis des fonctionnalités telles que les nouveaux opérateurs de mise à jour de position et d'autres. Vous pouvez aussi vérifier avec:

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

pour retourner le réglage actuel

30
répondu Neil Lunn 2018-06-09 22:34:44

cela peut aussi être fait avec une boucle while qui vérifie s'il reste des documents qui ont encore des sous-documents qui n'ont pas été mis à jour. Cette méthode préserve l'atomicité de vos mises à jour (ce que beaucoup d'autres solutions ici ne font pas).

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 }
    );
}

le nombre de fois que la boucle est exécutée sera égal au nombre maximum de fois que les sous-documents avec profile égal à 10 et handled non égal à 0 apparaissent dans aucun des documents en votre collection. Donc, si vous avez 100 documents dans votre collection et que l'un d'eux a trois sous-documents qui correspondent à query et que tous les autres documents ont moins de sous-documents correspondants, la boucle s'exécutera trois fois.

cette méthode évite le danger de saboter d'autres données qui peuvent être mises à jour par un autre processus pendant que ce script exécute. Il minimise également la quantité de données transférées entre le client et le serveur.

17
répondu Sean 2016-09-07 04:29:06

il s'agit en fait de la question de longue date à http://jira.mongodb.org/browse/SERVER-1243 où il y a en fait un certain nombre de défis à une syntaxe claire qui supporte" tous les cas " où des correspondances multi-tableaux sont trouvées. En fait, il existe déjà des méthodes qui "aident" à résoudre ce problème, comme opérations en vrac qui ont été mises en œuvre après ce poste initial.

il est il n'est toujours pas possible de mettre à jour plus d'un élément de tableau apparié dans une seule déclaration de mise à jour, donc même avec une mise à jour "multi", tout ce que vous pourrez mettre à jour est juste un élément mathématique dans le tableau pour chaque document dans cette déclaration unique.

la meilleure solution possible à l'heure actuelle est de trouver et de mettre en boucle tous les documents appariés et de traiter les mises à jour en bloc qui permettront au moins de nombreuses opérations d'être envoyées dans une seule demande avec une réponse unique. Vous pouvez utilisez optionnellement .aggregate() pour réduire le contenu du tableau retourné dans le résultat de la recherche à seulement ceux qui correspondent aux conditions pour la sélection de mise à jour:

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();

la partie .aggregate() fonctionne lorsqu'il y a un identificateur "unique" pour le tableau ou que tout le contenu de chaque élément forme un élément "unique" lui-même. Cela est dû à l'opérateur" set "dans $setDifference utilisé pour filtrer tout false valeurs retournées de l'opération $map utilisée pour traiter le tableau pour les correspondances.

si votre contenu de tableau n'a pas d'éléments uniques, vous pouvez essayer une approche alternative avec $redact :

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

où il est limite est que si "manipulé" était en fait un champ destiné à être présent à d'autres niveaux de document, alors vous allez probablement obtenir des résultats inattendus, mais est très bien ce champ n'apparaît que dans un seul document et correspond à l'égalité.

les versions futures (post 3.1 MongoDB ) au moment de la rédaction auront une opération $filter qui est plus simple:

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

et toutes les versions supportant .aggregate() peuvent utiliser l'approche suivante: $unwind , mais l'utilisation de cet opérateur en fait l'approche la moins efficace en raison de l'expansion des réseaux dans le pipeline:

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

dans tous les cas où la version MongoDB supporte un" curseur " de la sortie agrégée, alors il s'agit juste de choisir une approche et d'itérer les résultats avec le même bloc de code montré pour traiter les déclarations de mise à jour en bloc. Les opérations en vrac et les" curseurs " de la production agrégée sont introduits dans la même version ( MongoDB 2.6 ) et donc travaillent habituellement main dans la main pour le traitement.

dans les versions encore plus anciennes alors il est probablement mieux d'utiliser .find() pour retourner le curseur, et filtrer l'exécution des instructions juste le nombre de fois où l'élément de tableau est assorti pour les itérations .update() :

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 }});
    });
});

si vous êtes obstinément déterminé à faire des mises à jour" multi " ou à juger que pour être finalement plus efficace que le traitement des mises à jour multiples pour chaque document apparié, alors vous pouvez toujours déterminer le nombre maximum de correspondances de tableau possibles et juste exécuter un mise à jour" multi " que de nombreuses fois, jusqu'à ce qu'il n'y ait plus de documents à mettre à jour.

une approche valide pour les versions 2.4 et 2.2 de MongoDB pourrait aussi utiliser .aggregate() pour trouver cette valeur:

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 })
}

quoi qu'il en soit, il y a certaines choses que vous faites pas voulez faire dans la mise à jour:

  1. ne pas" One shot "mettre à jour le tableau: où si vous pensez qu'il pourrait être plus efficace de mettre à jour l'ensemble du contenu du tableau dans le code et puis juste $set l'ensemble du tableau dans chaque document. Cela peut sembler plus rapide à traiter, mais il n'y a aucune garantie que le contenu du tableau n'a pas changé depuis qu'il a été lu et que la mise à jour est effectuée. Bien que $set soit toujours un opérateur atomique, il ne mettra à jour le tableau qu'avec ce qu'il" pense " être les données correctes, et il est donc probable d'écraser tout changement qui se produit entre la lecture et l'écriture.

  2. ne calculez pas les valeurs de l'indice pour mettre à jour: où similaire à l'approche "one shot" vous venez de travailler sur cette position 0 et la position 2 (et ainsi de suite ) sont les éléments pour mettre à jour et coder ces avec et déclaration finale comme:

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

    encore une fois le problème ici est la "présomption" que les valeurs de l'indice trouvées lorsque le document a été lu sont le même indice les valeurs dans le tableau au moment de la mise à jour. Si de nouveaux éléments sont ajoutés au tableau d'une manière qui change l'ordre, alors ces positions ne sont plus valides et les mauvais éléments sont en fait mis à jour.

ainsi, jusqu'à ce qu'il y ait une syntaxe raisonnable déterminée pour permettre le traitement de plusieurs éléments de tableau appariés dans une seule déclaration de mise à jour, l'approche de base est soit de mettre à jour chaque élément de tableau apparié dans une déclaration individuelle ( idéalement dans Bulk) ou de travailler essentiellement sur les éléments du tableau maximum pour mettre à jour ou garder la mise à jour jusqu'à ce qu'il n'y ait plus de résultats modifiés. En tout cas, vous devriez "toujours" traiter mises à jour de position $ sur l'élément du tableau apparié, même si cela ne met à jour qu'un élément par déclaration.

les opérations en vrac sont en fait la solution " généralisée "au traitement de toutes les opérations qui s'avèrent être des" opérations multiples", et depuis lors sont plus d'applications pour cela que la simple mise à jour d'éléments de tableaux multiples avec la même valeur, alors il a bien sûr déjà été mis en œuvre, et il est actuellement la meilleure approche pour résoudre ce problème.

13
répondu Blakes Seven 2017-07-24 21:46:25

je suis étonné que cela n'ait toujours pas été abordé à mongo. Dans l'ensemble, mongo ne semble pas être très bon lorsqu'il s'agit de sous-tableaux. Vous ne pouvez pas compter les sous-tableaux simplement par exemple.

J'ai utilisé la première solution de Javier. Lisez le tableau dans les événements puis bouclez la boucle et construisez l'ensemble 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});

cela peut être résumé dans une fonction en utilisant un rappel pour le test conditionnel

8
répondu ljelewis 2013-08-08 07:59:10

j'ai été à la recherche d'une solution à cela en utilisant le plus récent pilote pour C# 3.6 et voici la solution que j'ai finalement réglé. La clé ici est d'utiliser "$[]" qui selon MongoDB est nouveau à partir de la version 3.6. Voir https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up. S [] pour plus d'informations.

voici le code:

{
   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});
}

pour plus voir mon billet original ici: Supprimer l'élément de tableau de TOUS les documents à l'aide de MongoDB C# pilote

2
répondu C0d3 0n3 2018-06-06 00:27:30

j'ai essayé ce qui suit et son fonctionnement amende.

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

// fonction de rappel en cas de nodejs

1
répondu Pranay Saha 2017-09-04 16:45:27

en fait, la commande save est seulement sur l'instance de la classe de Document. Qui ont beaucoup de méthodes et d'attributs. Vous pouvez donc utiliser la fonction lean () pour réduire la charge de travail. Reportez-vous ici. https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

un autre problème avec la fonction de sauvegarde, qui va rendre les données de conflit avec multi-enregistrer en même temps. Modèle .Mise à jour fera des données de manière cohérente. Ainsi, pour mettre à jour multi items dans le tableau de document. Utilisez votre langage de programmation familier et essayer quelque chose comme ceci, j'utilise mongoose que:

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))
})
0
répondu user3176403 2017-07-29 18:35:11

en utilisant Mongo 3.6, données de SpringBoot avec Mongo réactif. J'ai eu un problème où ma requête ne mettait à jour qu'un élément du tableau. Le paramètre " $ " ne fait référence qu'à un seul élément. Pour faire référence à l'utilisation multiple" $[]".

@Autowired private ReactiveMongoTemplate template;

public Mono<RoomData> findAndUnsetUserFromRemovedTeamId(String roomId, String teamId){
    Query query = new Query();
    query.addCriteria(
        Criteria.where("id").is(roomId).and("userRoomDatas").elemMatch(Criteria.where("teamId").is(teamId)));
    Update update = new Update();
    update.set("userRoomDatas.$[].teamId", roomId);
    update.set("userRoomDatas.$[].isSpectator", true);
    update.set("userRoomDatas.$[].isReady", false);

    FindAndModifyOptions options = FindAndModifyOptions.options();
    options.returnNew(true);
    return template.findAndModify(query, update, options, RoomData.class);
}

donc dans mon code j'avais besoin de remplacer une valeur UserData si leur teamId était égale à celle qui était supprimée. La requête interroge tous les userRoomData dont la valeur teamId est égale à celle qui est supprimée. La mise à jour définit les nouvelles valeurs pour teamId/ispectator / isReady. Et le FindAndModifyOptions est utilisé pour retourner la nouvelle valeur au lieu de l'ancienne.

donc dans l'ensemble, la clé ici est " $ [] "au lieu de"$". Maintenant, dites que vous avez plusieurs RoomDatas que chacun a plusieurs UserDatas qui ont besoin d'avoir des valeurs changées. Vous devez utiliser la méthode updateMulti de Réactivemongotemplate et passer dans les bons parmes.

0
répondu BWC semaJ 2018-09-10 03:40:54

je voulais juste ajouter une autre solution qui a fonctionné pour moi et c'est assez simple. Ici, c'est juste un tableau de tags (chaînes de caractères) alors pour mettre à jour un tag appelé "test" en "changed", faites juste ceci:

myDocuments.find({tags: "test" }, {fields: {_id: 1}}).forEach(function (doc) {
    myDocuments.update(
        {_id: doc._id, tags: "test"}, 
        {$set:{'tags.$': "changed"}});
    });
-1
répondu cfs 2015-03-01 22:51:38