MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Upsert Document et/ou ajouter un sous-document

L'approche pour gérer cela n'est pas simple, car mélanger des "upserts" avec l'ajout d'éléments à des "tableaux" peut facilement conduire à des résultats indésirables. Cela dépend également si vous souhaitez que la logique définisse d'autres champs tels qu'un "compteur" indiquant le nombre de contacts dans un tableau, que vous souhaitez uniquement incrémenter/décrémenter lorsque des éléments sont ajoutés ou supprimés respectivement.

Dans le cas le plus simple cependant, si les "contacts" ne contenaient qu'une valeur singulière telle qu'un ObjectId lien vers une autre collection, puis le $addToSet le modificateur fonctionne bien, tant qu'il n'y a pas de "compteurs" impliqués :

Client.findOneAndUpdate(
    { "clientName": clientName },
    { "$addToSet": { "contacts":  contact } },
    { "upsert": true, "new": true },
    function(err,client) {
        // handle here
    }
);

Et tout va bien car vous testez uniquement pour voir si un document correspond au "clientName", sinon mettez-le en place. Qu'il y ait une correspondance ou non, le $addToSet s'occupera des valeurs "singulières" uniques, étant tout "objet" qui est vraiment unique.

Les difficultés surviennent lorsque vous avez quelque chose comme :

{ "firstName": "John", "lastName": "Smith", "age": 37 }

Déjà dans le tableau des contacts, et ensuite vous voulez faire quelque chose comme ceci :

{ "firstName": "John", "lastName": "Smith", "age": 38 }

Là où votre intention réelle est qu'il s'agit du "même" John Smith, et c'est juste que "l'âge" n'est pas différent. Idéalement, vous voulez simplement "mettre à jour" cette entrée de tableau et ne pas créer un nouveau tableau ou un nouveau document.

Travailler avec .findOneAndUpdate() où vous voulez que le document mis à jour revienne peut être difficile. Donc, si vous ne voulez pas vraiment le document modifié en réponse, alors le API d'opérations groupées de MongoDB et le pilote principal sont les plus utiles ici.

Considérant les déclarations :

var bulk = Client.collection.initializeOrderedBulkOP();

// First try the upsert and set the array
bulk.find({ "clientName": clientName }).upsert().updateOne({
    "$setOnInsert": { 
        // other valid client info in here
        "contacts": [contact]
    }
});

// Try to set the array where it exists
bulk.find({
    "clientName": clientName,
    "contacts": {
        "$elemMatch": {
            "firstName": contact.firstName,
            "lastName": contact.lastName
         }
    }
}).updateOne({
    "$set": { "contacts.$": contact }
});

// Try to "push" the array where it does not exist
bulk.find({
    "clientName": clientName,
    "contacts": {
        "$not": { "$elemMatch": {
            "firstName": contact.firstName,
            "lastName": contact.lastName
         }}
    }
}).updateOne({
    "$push": { "contacts": contact }
});

bulk.execute(function(err,response) {
    // handle in here
});

C'est bien car les opérations en bloc ici signifient que toutes les instructions ici sont envoyées au serveur en même temps et qu'il n'y a qu'une seule réponse. Notez également ici que la logique signifie ici qu'au plus deux opérations modifieront réellement quoi que ce soit.

Dans le premier cas, le $setOnInsert Le modificateur s'assure que rien n'est changé lorsque le document est juste une correspondance. Comme les seules modifications ici se trouvent dans ce bloc, cela n'affecte qu'un document où une "upsert" se produit.

Notez également que sur les deux déclarations suivantes, vous n'essayez pas de "upsert" à nouveau. Cela considère que la première déclaration a peut-être réussi là où elle devait l'être, ou n'a pas eu d'importance.

L'autre raison de l'absence de "upsert" est que les conditions nécessaires pour tester la présence de l'élément dans le tableau conduiraient à la "upsert" d'un nouveau document lorsqu'elles ne sont pas remplies. Ce n'est pas souhaité, donc pas de "upsert".

En fait, ils vérifient respectivement si l'élément du tableau est présent ou non, et mettent à jour l'élément existant ou en créent un nouveau. Par conséquent, au total, toutes les opérations signifient que vous modifiez "une fois" ou au plus "deux fois" dans le cas où un upsert s'est produit. Le "deux fois" possible crée très peu de frais généraux et aucun problème réel.

Également dans la troisième déclaration, le $not renverse la logique de $elemMatch pour déterminer qu'aucun élément de tableau avec la condition de requête n'existe.

Traduire ceci avec .findOneAndUpdate() devient un peu plus problématique. Non seulement c'est le "succès" qui compte maintenant, mais il détermine également la manière dont le contenu éventuel est renvoyé.

Donc, la meilleure idée ici est d'exécuter les événements en "série", puis de travailler un peu de magie avec le résultat afin de renvoyer le formulaire final "mis à jour".

L'aide que nous utiliserons ici est à la fois avec async.waterfall et le lodash bibliothèque :

var _ = require('lodash');   // letting you know where _ is coming from

async.waterfall(
    [
        function(callback) {
            Client.findOneAndUpdate(
               { "clientName": clientName },
               {
                  "$setOnInsert": { 
                      // other valid client info in here
                      "contacts": [contact]
                  }
               },
               { "upsert": true, "new": true },
               callback
            );
        },
        function(client,callback) {
            Client.findOneAndUpdate(
                {
                    "clientName": clientName,
                    "contacts": {
                       "$elemMatch": {
                           "firstName": contact.firstName,
                           "lastName": contact.lastName
                       }
                    }
                },
                { "$set": { "contacts.$": contact } },
                { "new": true },
                function(err,newClient) {
                    client = client || {};
                    newClient = newClient || {};
                    client = _.merge(client,newClient);
                    callback(err,client);
                }
            );
        },
        function(client,callback) {
            Client.findOneAndUpdate(
                {
                    "clientName": clientName,
                    "contacts": {
                       "$not": { "$elemMatch": {
                           "firstName": contact.firstName,
                           "lastName": contact.lastName
                       }}
                    }
                },
                { "$push": { "contacts": contact } },
                { "new": true },
                function(err,newClient) {
                    newClient = newClient || {};
                    client = _.merge(client,newClient);
                    callback(err,client);
                }
            );
        }
    ],
    function(err,client) {
        if (err) throw err;
        console.log(client);
    }
);

Cela suit la même logique qu'auparavant en ce sens que seules deux ou une de ces instructions feront quoi que ce soit avec la possibilité que le "nouveau" document renvoyé soit null . La "cascade" ici transmet un résultat de chaque étape à la suivante, y compris la fin où toute erreur se dirigera immédiatement vers.

Dans ce cas, le null serait remplacé par un objet vide {} et le _.merge() combinera les deux objets en un seul, à chaque étape ultérieure. Cela vous donne le résultat final qui est l'objet modifié, quelles que soient les opérations précédentes qui ont réellement fait quoi que ce soit.

Bien sûr, il y aurait une manipulation différente requise pour $pull , et votre question contient également des données d'entrée sous forme d'objet en soi. Mais ce sont en fait des réponses en elles-mêmes.

Cela devrait au moins vous permettre de commencer à aborder votre modèle de mise à jour.