19
votes

I need to insert a document if it doesn't exist. I know that the "upsert" option can do that, but I have some particular needs.

First I need to create the document with its _id field only, but only if it doesn't exist already. My _id field is a number generated by me (not an ObjectId). If I use the "upsert" option then I get "Mod on _id not allowed"

db.mycollection.update({ _id: id }, { _id: id }, { upsert: true });

I know that we can't use the _id in a $set.

So, my question is: If there any way to a "create if doesn't exists" atomically in mongodb?

EDIT: As proposed by @Barrie this works (using nodejs and mongoose):

var newUser = new User({ _id: id });
newUser.save(function (err) {               
    if (err && err.code === 11000) {            
            console.log('If duplicate key the user already exists', newTwitterUser);
        return;
    }
    console.log('New user or err', newTwitterUser);
});

But I still wonder if it is the best way to do it.

4
try to use a save operation? i.e. db.collection.save({"_id": your_id}) - Felix Yan
Save fails with a duplicate key error if already exists - aartiles

4 Answers

30
votes

I had the same problem, but found a better solution for my needs. You can use that same query style if you simply remove the _id attribute from the update object. So if at first you get an error with this:

db.mycollection.update({ _id: id }, {$set: { _id: id, name: 'name' }}, { upsert: true });

instead use this:

db.mycollection.update({ _id: id }, {$set: { name: 'name' }}, { upsert: true });

This is better because it works for both insert and update.

12
votes

UPDATE: Upsert with _id can be done without $setOnInsert, as explaind by @Barrie above.

The trick is to use $setOnInsert:{_id:1} with upsert, that way the _id is only written to if it's an insert, and never for updates.

Only, there was a bug preventing this from working until v2.6 - I just tried it on 2.4 and it's not working.

The workaround I use is having another ID field with a unique index. Eg. $setOnInsert:{myId:1}.

5
votes

You can just use insert(). If the document with the _id you specify already exists, the insert() will fail, nothing will be modified - so "create if it doesn't exist" is what it's already doing by default when you use insert() with a user-created _id.

0
votes

Please note that $setOnInsert don't work easily when you upsert a simple key => value object (not $set or other). I need to use that (in PHP):

public function update($criteria , $new_object, array $options = array()){
    // In 2.6, $setOnInsert with upsert == true work with _id field
    if(isset($options['upsert']) && $options['upsert']){
        $firstKey = array_keys($new_object)[0];
        if(strpos($firstKey, '$')===0){
            $new_object['$setOnInsert']['_id'] = $this->getStringId();
        }
        //Even, we need to check if the object exists
        else if($this->findOne($criteria, ['_id'])===null){
            //In this case, we need to set the _id
            $new_object['_id'] = $this->getStringId();
        }

    }
    return parent::update($criteria, $new_object, $options);
}