Thomas Watson
Thomas Watson

Reputation: 6627

How do I upsert a MongoDB document only in certain circumstances?

I'm trying to update a user-document in a MongoDB collection. If the user doesn't exist it should be created, but if it already exist it should only be updated given the query parses certain checks.

This is what I first tried:

var ts = new Date('2015-01-01T00:00:00.000Z');
var query = { name: 'bob', updatedAt: { $lt: ts } };
var update = { $set: { age: 42, updatedAt: ts }, $inc: { updates: 1 } };
db.users.update(query, update, { upsert: true });

The problem with that solution is that it will try to create the user twice if its updatedAt property is less than the given ts date.

How can I make sure it's created only if the name part of the query doesn't match any documents, but don't do anything if the updatedAt part doesn't?

Upvotes: 0

Views: 566

Answers (1)

Sylvain Leroux
Sylvain Leroux

Reputation: 52000

Basically:

  • The user does not exists, create it with the current time as the "last update time". This could be done using a simple upset and $setOnInsert;
  • The user already exists and need to be updated. This is a simple update.

Simply said, you need two update statements. But, you may wrap them in the same update command:

db.runCommand({
  update: 'users',
  updates: [
    { q: { name: 'bob' },
      u: { $setOnInsert: { updatedAt: now, updates: 0 }},
      upsert: true },
    { q: { name: 'bob', updatedAt: { $lt: now } },
      u: { $set: { updatedAt: now }, $inc: { updates: 1 } },
      upsert: false },
  ]
})

Producing on the first run (empty collection):

> var now = new Date()
> db.runCommand({   update: 'users',   updates: [     { q: { name: 'bob' },       u: { $setOnInsert: { updatedAt: now, updates: 0 }},       upsert: true },     { q: { name: 'bob', updatedAt: { $lt: now } },       u: { $set: { updatedAt: now }, $inc: { updates: 1 } },       upsert: false },   ] })
{
    "ok" : 1,
    "nModified" : 0,
    "n" : 1,
    "upserted" : [
        {
            "index" : 0,
            "_id" : ObjectId("556f672cd418ed1506eb2ca3")
        }
    ]
}

Then, when run again:

> var now = new Date()
> db.runCommand({   update: 'users',   updates: [     { q: { name: 'bob' },       u: { $setOnInsert: { updatedAt: now, updates: 0 }},       upsert: true },     { q: { name: 'bob', updatedAt: { $lt: now } },       u: { $set: { updatedAt: now }, $inc: { updates: 1 } },       upsert: false },   ] })
{ "ok" : 1, "nModified" : 1, "n" : 2 }

Please note that the update command is not atomic. But there is no way for an other client to see a partially created or updated user document as either the first update statement create the fully populated document, or it already exists (and is left untouched) until the second statement that updates it completely.

It is even safe if the user is concurrently created between the two statements -- it will then be transparently updated if needed.

Upvotes: 3

Related Questions