Upsert document and / or add sub-document

I was struggling with the asynchronous nature of MongoDB, Mongoose and JavaScript and how best to do multiple updates to the collection.

I have an Excel sheet and contact details. There are multiple customers with multiple contacts, one per line, and the customer details are the same (so the customer name can be used as a unique key - in fact in the schema defined with unique: true

).

The logic I want to achieve is this:

  • Find the client collection for the client with clientName

    as key
  • If no comparable customer name is found, create a new document for that customer (do not update, I don’t want to change anything if the customer document is already in the database)
  • Check if a contact is present in the contact array in the client document using firstName

    and lastName

    as keys
  • If no contact is found, then $push

    that contact with the array

Of course, we could easily have a situation where the client does not exist (and so is created), and then immediately, the next line of the sheet, is a different contact for the same client, so I would like to find an existing (just created) client and $push

that's the second new pin in the array.

I tried this but it doesn't work:

Client.findOneAndUpdate(
  {clientName: obj.client.clientname},
  {$set: obj.client, $push: {contacts: obj.contact}},
  {upsert: true, new: true},
  function(err, client){
    console.log(client)
  }
)

      

and I have covered other questions well, for example:

but can't get a solution ... I come to the conclusion that maybe I need to use some application logic to search, then make decisions in my code and then write rather than use one Mongoose / Mongo, but then the async issues force their ugly head.

Any suggestions?

+3


source to share


2 answers


The approach to handling this is not straightforward, as mixing upserts with adding elements to "arrays" can easily lead to unwanted results. It also depends on whether you want the logic to set other fields, such as a "counter" indicating the number of pins within the array that you only want to increase or decrease as items are added or removed accordingly.

In the simplest case, if the "contacts" only contain a singular value, such as ObjectId

associated with another collection, then this works well as long as there are no "counters": $addToSet

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

      

And that's ok as you are only testing to see if the doucment is the same as "clientName" if you don't activate it. Whether there is a match or not, the operator $addToSet

takes care of the unique "singular" values, being any "object" that is truly unique.

Difficulties arise where you have something like:

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

      

Already in the contacts array, and then you want to do something like this:

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

      

If your actual intention is that this is "the same" John Smith, and it's just that "age" is no different. Ideally, you just want to "update" so that the end of the noiter array will create a new array or new document.

Working from .findOneAndUpdate()

where you want the updated document to come back can be difficult. So unless you really want the document to change in response, the MongoDB bulk APIs and the underlying driver are most useful here .

Given the statements:

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

      

This is good, as here Bulk Operations means that all instructions here are sent to the server at once and there is only one response. Also note that the logic here means that in most cases only two operations will actually change anything.

In the first case, the modifier ensures that nothing changes when the document is just a match. Since the only modifications here are inside this block, this only affects the document in which the "upsert" occurs. $setOnInsert



Also notice the next two statements that you are not trying to "reload" yet. This holds that the first statement may have been successful where it should have been, or otherwise not mattered.

Another reason for the lack of an upsert is that the conditions required to check for the presence of an element in the array will cause the new document to be "promoted" when they are not met. This is undesirable, so there is no "upsert".

What they actually do is suitably check if the array element is present or not and either update the existing element or create a new one. Therefore, in general, all operations mean that you either modify "once", or no more than "twice" in the case when the recreation occurred. Possible "twice" creates very little overhead and is not a real problem.

Also in the third expression, the operator changes the logic to determine that there is no array element with the query condition. $not

$elemMatch


Translating with help .findOneAndUpdate()

becomes a little more problematic. Not only does success now matter, it also determines how the final content is returned.

So the best idea here is to fire events in a "series" and then work with the slightest magic with the result to return the final "refreshed" form.

The help we'll be using here is async.waterfall and lodash :

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

      

This follows the same logic as before, in that only two or one of these operators is actually going to do anything about returning a "new" document null

. "Waterfall" here transfers the result from each stage to the next, including the end, where also any error immediately goes to.

In this case, it null

will be replaced with an empty object {}

, and the method will _.merge()

combine the two objects into one, at each subsequent stage. This gives the end result, which is the modified object, regardless of what previous operations actually did anything.

Of course $pull

different manipulation is required for , and also your question has input as the object form itself. But these are actually answers in and of themselves.

This should at least start with how to approach the update pattern.

+2


source


I am not using mongoose, so I will post an update to the mongo shell; sorry about that. I think the following:

 db.clients.update({$and:[{'clientName':'apple'},{'contacts.firstName': {$ne: 'nick'}},{'contacts.lastName': {$ne: 'white'}}]}, 
                  {$set:{'clientName':'apple'}, $push: {contacts: {'firstName': 'nick', 'lastName':'white'}}},
                  {upsert: true });

      



So:

if the client apple does not exist, it is created with a contact with the given first and last name. If he exists and does not have this contact, he pushes him. If it exists and already has the given contact, nothing happens.

0


source







All Articles