MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Upsert Document e/ou adicionar um Sub-Document


A abordagem para lidar com isso não é simples, pois misturar "upserts" com a adição de itens a "arrays" pode facilmente levar a resultados indesejados. Também depende se você deseja que a lógica defina outros campos, como um "contador" indicando quantos contatos existem em uma matriz, que você deseja apenas aumentar/diminuir à medida que os itens são adicionados ou removidos, respectivamente.

No caso mais simples, no entanto, se os "contatos" contiverem apenas um valor singular, como um ObjectId link para outra coleção, então o $addToSet modificador funciona bem, desde que não haja "contadores" envolvidos:
Client.findOneAndUpdate(
    { "clientName": clientName },
    { "$addToSet": { "contacts":  contact } },
    { "upsert": true, "new": true },
    function(err,client) {
        // handle here
    }
);

E tudo bem, pois você está apenas testando para ver se um documento corresponde ao "clientName", se não for upsert. Se há uma correspondência ou não, o $addToSet O operador cuidará de valores "singulares" únicos, sendo qualquer "objeto" verdadeiramente único.

As dificuldades surgem onde você tem algo como:
{ "firstName": "John", "lastName": "Smith", "age": 37 }

Já no array de contatos, e então você quer fazer algo assim:
{ "firstName": "John", "lastName": "Smith", "age": 38 }

Onde sua real intenção é que este seja o "mesmo" John Smith, e é que a "idade" não é diferente. Idealmente, você deseja apenas "atualizar" essa entrada de matriz e não criar uma nova matriz ou um novo documento.

Trabalhando com .findOneAndUpdate() onde você deseja que o documento atualizado retorne pode ser difícil. Então, se você realmente não quer o documento modificado em resposta, então o API de operações em massa do MongoDB e o driver principal são de maior ajuda aqui.

Considerando as afirmações:
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
});

Isso é bom, pois as operações em massa aqui significam que todas as instruções aqui são enviadas ao servidor de uma só vez e há apenas uma resposta. Observe também aqui que a lógica significa aqui que no máximo apenas duas operações realmente modificarão qualquer coisa.

Em primeira instância, o $setOnInsert modificador garante que nada seja alterado quando o documento for apenas uma correspondência. Como as únicas modificações aqui estão dentro desse bloco, isso afeta apenas um documento onde ocorre um "upsert".

Observe também nas próximas duas declarações que você não tente "upert" novamente. Isso considera que a primeira declaração foi possivelmente bem sucedida onde tinha que ser, ou então não importava.

A outra razão para não haver "upsert" é porque as condições necessárias para testar a presença do elemento no array levariam ao "upsert" de um novo documento quando não fossem atendidas. Isso não é desejado, portanto, não há "upsert".

O que eles fazem de fato é verificar se o elemento da matriz está presente ou não e atualizar o elemento existente ou criar um novo. Portanto, no total, todas as operações significam que você modifica "uma vez" ou no máximo "duas vezes" no caso de ocorrer um upsert. O possível "duas vezes" cria muito pouca sobrecarga e nenhum problema real.

Também na terceira declaração, o $not operador inverte a lógica do $elemMatch para determinar que não existe nenhum elemento de matriz com a condição de consulta.

Traduzindo isso com .findOneAndUpdate() torna-se um pouco mais de um problema. Não é apenas o "sucesso" que importa agora, mas também determina como o conteúdo eventual é retornado.

Então a melhor ideia aqui é rodar os eventos em "série", e então fazer um pouco de mágica com o resultado para retornar o final do formulário "atualizado".

A ajuda que usaremos aqui é com async.waterfall e o 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);
    }
);

Isso segue a mesma lógica de antes, pois apenas duas ou uma dessas instruções realmente farão alguma coisa com a possibilidade de que o "novo" documento retornado seja null . A "cascata" aqui passa um resultado de cada estágio para o próximo, incluindo o final para onde também qualquer erro será imediatamente ramificado.

Neste caso, o null seria trocado por um objeto vazio {} e o _.merge() O método combinará os dois objetos em um, em cada estágio posterior. Isso lhe dá o resultado final que é o objeto modificado, não importa quais operações anteriores realmente fizeram alguma coisa.

Claro, haveria uma manipulação diferente necessária para $pull , e também sua pergunta tem dados de entrada como um formulário de objeto em si. Mas essas são, na verdade, respostas em si mesmas.

Isso deve pelo menos começar a abordar seu padrão de atualização.