130

as the title says, I want to perform a find (one) for a document, by _id, and if doesn't exist, have it created, then whether it was found or was created, have it returned in the callback.

I don't want to update it if it exists, as I've read findAndModify does. I have seen many other questions on Stackoverflow regarding this but again, don't wish to update anything.

I am unsure if by creating (of not existing), THAT is actually the update everyone is talking about, it's all so confuzzling :(

numbers1311407
  • 33,686
  • 9
  • 90
  • 92
Discipol
  • 3,137
  • 4
  • 22
  • 41

4 Answers4

210

Beginning with MongoDB 2.4, it's no longer necessary to rely on a unique index (or any other workaround) for atomic findOrCreate like operations.

This is thanks to the $setOnInsert operator new to 2.4, which allows you to specify updates which should only happen when inserting documents.

This, combined with the upsert option, means you can use findAndModify to achieve an atomic findOrCreate-like operation.

db.collection.findAndModify({
  query: { _id: "some potentially existing id" },
  update: {
    $setOnInsert: { foo: "bar" }
  },
  new: true,   // return new doc if one is upserted
  upsert: true // insert the document if it does not exist
})

As $setOnInsert only affects documents being inserted, if an existing document is found, no modification will occur. If no document exists, it will upsert one with the specified _id, then perform the insert only set. In both cases, the document is returned.

numbers1311407
  • 33,686
  • 9
  • 90
  • 92
  • Your code is used in mongodb shell, but how to use it in node.js code? – Gank Jun 03 '14 at 03:41
  • 3
    @Gank if you're using the mongodb native driver for node the syntax would be more like `collection.findAndModify({_id:'theId'}, , {$setOnInsert:{foo: 'bar'}}, {new:true, upsert:true}, callback)`. See [the docs](http://mongodb.github.io/node-mongodb-native/api-generated/collection.html) – numbers1311407 Jun 03 '14 at 13:12
  • 8
    Is there a way to know if the document has been updated or inserted ? – doom Sep 10 '14 at 23:07
  • @numbers1311407 This works great when POSTing new documents. But what about when PUTing already existing ones? Do you have any good soultions to my problem here: http://stackoverflow.com/questions/26055958/mongodb-unique-constraints-on-a-date-range – Anders Östman Sep 29 '14 at 09:43
  • If I understand what you're saying, the answer is just to remove the `$setOnInsert`. The purpose of this modifier is to *not* update existing records. If you want to affect existing records, the normal `update` will do that. – numbers1311407 Sep 29 '14 at 14:21
  • @numbers1311407 Take your example above: You query for a _id, if not found, create a new document with the combination of `upsert` and `$setOnInsert`. But what if i later want to **change** the `_id` of this particular document, and **still avoid collision** with other potentially existing `_id`'s. A normal update will not ensure that no collission is made, and if this operation is split into a find (for looking after _id's) followed by update, we lose the atomicity. *(I know that most of the time there is no reason for change the _id, but for the sake of the problem, just pretend there is)* – Anders Östman Oct 22 '14 at 09:56
  • @AndersÖstman `_id` is immutable and cannot be updated, atomically or no. To answer your question for a field which *can* be changed, you could use `$set` which would always be applied and takes precedence over the query (`query: {foo: 1}, update: {$setOnInsert: {...}, $set: {foo: 2}}`). – numbers1311407 Oct 22 '14 at 14:33
  • Oh, the _id actually IS immutable. =) I never felt the need to change it and therefore actually did not know that this could not be done. – Anders Östman Oct 22 '14 at 14:52
  • can we use "findAndModify" in the Bulk Operations? – techie_28 May 29 '15 at 10:25
  • @techie_28 No, `findAndModify` works on a single doc and doesn't accept a `multi` param like update does. – numbers1311407 May 29 '15 at 12:47
  • I want to check if an email address exists in a collection and insert if it doesnt. I got this bulk.find({email:"xyc@yahoo.com"}).upsert().update({$setOnInsert:{status:"I",points:"0"} }) If that is correct can we make bulk query operations dynamically? – techie_28 May 29 '15 at 21:50
  • I didn't understand what you were talking about at first. I'm not very familiar with the bulk API. From looking at the docs it appears that it should work as written (though maybe you'd use `updateOne`). I also cannot say whether or not it would be atomic. It's a bit outside the scope of this question I think. – numbers1311407 May 30 '15 at 01:26
  • 4
    If you want **to check if** the query above(`db.collection.findAndModify({query: {_id: "some potentially existing id"}, update: {$setOnInsert: {foo: "bar"}}, new: true, upsert: true})`) **insert(upsert)ed** a document, you should consider using `db.collection.updateOne({_id: "some potentially existing id"}, {$setOnInsert: {foo: "bar"}}, {upsert: true})`. It returns `{"acknowledged": true, "matchedCount": 0, "modifiedCount": 0, "upsertedId": ObjectId("for newly inserted one")}` if the doc inserted, `{"acknowledged": true, "matchedCount": 1, "modifiedCount": 0}` if the doc already exists. – Константин Ван Jul 18 '16 at 11:38
  • 8
    seems to be deprecated in flavor of `findOneAndUpdate`, `findOneAndReplace` or `findOneAndDelete` – ravshansbox Nov 04 '16 at 12:45
  • 8
    One needs to be careful here, though. This only works if the selector of the findAndModify/findOneAndUpdate/updateOne uniquely identifies one document by _id. Otherwise the upsert is split up on the server into a query and an update/insert. The update will still be atomic. But the query and the update together will not be executed atomically. – Anon Apr 25 '17 at 20:14
  • Since, in the solution, update is not carried out. How can we do this: If document exists then update, but if it does not exist then create and set one more field. – ashwin mahajan Aug 09 '17 at 10:29
29

Driver Versions > 2

Using the latest driver (> version 2), you'll use findOneAndUpdate as findAndModify was deprecated. The new method takes 3 arguments, the filter, the update object (which contains your default properties, that should be inserted for a new object), and options where you have to specify the upsert operation.

Using the promise syntax, it looks like this:

const result = await collection.findOneAndUpdate(
  { _id: new ObjectId(id) },
  {
    $setOnInsert: { foo: "bar" },
  },
  {
    returnOriginal: false,
    upsert: true,
  }
);

const newOrUpdatedDocument = result.value;
Community
  • 1
  • 1
hansmaad
  • 18,417
  • 9
  • 53
  • 94
6

Its a bit dirty, but you can just insert it.

Be sure that the key has a unique index on it (if you use the _id it's ok, it's already unique).

In this way if the element is already present it will return an exception that you can catch.

If it isn't present, the new document will be inserted.

Updated: a detailed explanation of this technique on the MongoDB Documentation

Joe
  • 46,419
  • 33
  • 155
  • 245
Madarco
  • 2,084
  • 18
  • 26
  • ok, thats a good idea, but for the pre-existing value it will return an error but NOT the value itself, right? – Discipol May 03 '13 at 14:41
  • 3
    This is actually one of the recommended solutions for isolated sequences of operations (find then create if not found, presumably) http://docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations/ – numbers1311407 May 03 '13 at 14:43
  • @Discipol if you want to do a set of atomic operations, you should first lock the Document, then modify it, and at the end release it. This will require more queries, but you can optimize for the best case scenario and do only 1-2 queries most of the times. see: http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/ – Madarco May 03 '13 at 15:21
-2

Here's what I did (Ruby MongoDB driver):

$db[:tags].update_one({:tag => 'flat'}, {'$set' => {:tag => 'earth' }}, { :upsert => true })}

It will update it if it exists, and insert it if it doesn't.

Vidar
  • 1,008
  • 14
  • 16