sql >> Base de Datos >  >> NoSQL >> MongoDB

Actualizar documento y/o agregar un subdocumento

El enfoque para manejar esto no es simple, ya que mezclar "inserciones" con agregar elementos a "matrices" puede conducir fácilmente a resultados no deseados. También depende de si desea que la lógica configure otros campos, como un "contador" que indique cuántos contactos hay dentro de una matriz, que solo desea aumentar/disminuir a medida que se agregan o eliminan elementos, respectivamente.

Sin embargo, en el caso más simple, si los "contactos" solo contienen un valor singular como un ObjectId vinculando a otra colección, entonces $addToSet modificador funciona bien, siempre que no haya "contadores" involucrados:

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

Y todo está bien, ya que solo está probando para ver si un documento coincide con el "nombre del cliente", si no lo modifica. Ya sea que haya una coincidencia o no, el $addToSet El operador se encargará de valores "singulares" únicos, siendo cualquier "objeto" que sea verdaderamente único.

Las dificultades vienen cuando tienes algo como:

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

Ya está en la matriz de contactos, y luego quiere hacer algo como esto:

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

Donde su intención real es que este es el "mismo" John Smith, y es solo que la "edad" no es diferente. Idealmente, solo desea "actualizar" esa entrada de matriz y no crear una nueva matriz o un nuevo documento.

Trabajando esto con .findOneAndUpdate() dónde desea que regrese el documento actualizado puede ser difícil. Entonces, si realmente no desea el documento modificado como respuesta, entonces API de operaciones masivas de MongoDB y el controlador central son de gran ayuda aquí.

Teniendo en cuenta las declaraciones:

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

Esto es bueno ya que las operaciones masivas aquí significan que todas las declaraciones aquí se envían al servidor a la vez y solo hay una respuesta. También tenga en cuenta aquí que la lógica significa que, como máximo, solo dos operaciones modificarán algo.

En primera instancia, el $setOnInsert El modificador se asegura de que nada cambie cuando el documento es solo una coincidencia. Como las únicas modificaciones aquí están dentro de ese bloque, esto solo afecta a un documento en el que se produce una "inserción superior".

También tenga en cuenta en las siguientes dos declaraciones que no intente "upsert" nuevamente. Esto considera que la primera declaración posiblemente tuvo éxito donde tenía que serlo, o de lo contrario no importó.

La otra razón por la que no hay "upsert" es porque las condiciones necesarias para probar la presencia del elemento en la matriz conducirían a la "upsert" de un nuevo documento cuando no se cumplieran. Eso no se desea, por lo tanto, no hay "upsert".

De hecho, lo que hacen es verificar respectivamente si el elemento de la matriz está presente o no, y actualizar el elemento existente o crear uno nuevo. Por lo tanto, en total, todas las operaciones significan que usted modifica "una vez" o, como máximo, "dos veces" en el caso de que se produzca un upsert. El posible "dos veces" genera muy pocos gastos generales y ningún problema real.

También en la tercera declaración el $not El operador invierte la lógica del $elemMatch para determinar que no existe ningún elemento de matriz con la condición de consulta.

Traduciendo esto con .findOneAndUpdate() se convierte en un poco más de un problema. No solo es el "éxito" lo que importa ahora, sino que también determina cómo se devuelve el contenido final.

Entonces, la mejor idea aquí es ejecutar los eventos en "serie" y luego hacer un poco de magia con el resultado para devolver el formulario final "actualizado".

La ayuda que usaremos aquí es tanto con async.waterfall y el lodash biblioteca:

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

Eso sigue la misma lógica que antes en el sentido de que solo dos o una de esas declaraciones realmente harán algo con la posibilidad de que el "nuevo" documento devuelto sea null . La "cascada" aquí pasa un resultado de cada etapa a la siguiente, incluido el final donde también se bifurcará inmediatamente cualquier error.

En este caso el null se cambiaría por un objeto vacío {} y el _.merge() El método combinará los dos objetos en uno, en cada etapa posterior. Esto le da el resultado final, que es el objeto modificado, sin importar qué operaciones anteriores realmente hayan hecho algo.

Por supuesto, se requeriría una manipulación diferente para $pull , y también su pregunta tiene datos de entrada como un formulario de objeto en sí mismo. Pero esas son en realidad respuestas en sí mismas.

Esto al menos debería ayudarlo a comenzar a abordar su patrón de actualización.