188

I want update an _id field of one document. I know it's not really good practice. But for some technical reason, I need to update it.

If I try to update it I get:

db.clients.update({ _id: ObjectId("123")}, { $set: { _id: ObjectId("456")}})

Performing an update on the path '_id' would modify the immutable field '_id'

And the update is rejected. How I can update it?

SQB
  • 3,926
  • 2
  • 28
  • 49
shingara
  • 46,608
  • 11
  • 99
  • 105

8 Answers8

292

You cannot update it. You'll have to save the document using a new _id, and then remove the old document.

// store the document in a variable
doc = db.clients.findOne({_id: ObjectId("4cc45467c55f4d2d2a000002")})

// set a new _id on the document
doc._id = ObjectId("4c8a331bda76c559ef000004")

// insert the document, using the new _id
db.clients.insert(doc)

// remove the document with the old _id
db.clients.remove({_id: ObjectId("4cc45467c55f4d2d2a000002")})
Niels van der Rest
  • 31,664
  • 16
  • 80
  • 86
  • 42
    A fun issue with this appears if some field on that document has a unique index. In that situation, your example will fail because a document cannot be inserted with a duplicate value in a unique indexed field. You could fix this by doing the delete first, but that is a bad idea because if your insert fails for some reason your data is now lost. You must instead drop your index, perform the work, then restore the index. – skelly Jun 02 '14 at 19:52
  • Good point @skelly! I happened to thinking about similar problems and saw your fresh comment made just 2 hours ago. So this modifying id hassle is considered as an intrinsic problem caused by allowing user to choose ID? – RayLuo Jun 02 '14 at 21:56
  • 2
    If you get a `duplicate key error` in the `insert` line and aren't worried about the problem @skelly mentioned, the simplest solution is to just call the `remove` line first and then call the `insert` line. The `doc` should already printed on your screen so it'd be easy to recover, worst case, even if the insert fails, for simple documents. – philfreo Apr 14 '15 at 19:55
  • Using just ObjectId() without the string as parameter will generate a new unique one. – Erik Apr 30 '15 at 12:58
  • @skelly Your preferred solution, if I go by it, then what I observe is that when I'm dropping and performing work and restoring the index back, a lot of time(a few seconds are passing). The problem that I think it will create is that duplicate records for the indexed field may get inserted while the index is dropped.And upon re-establishment of the index, all sorts of problems might arise. How to deal with this? – Shankha057 Jan 31 '18 at 12:24
  • 2
    @ShankhadeepGhoshal yeah that is a risk, particularly if you are performing this against a live production system. Unfortunately I think your best option there is to take a scheduled outage and stop all writers during this process. Another option that might be a little less painful would be to force applications into a read-only mode temporarily. Reconfigure all apps that write tot he DB to only point to a secondary node. Reads will succeed but writes will fail during this time, and your DB will remain static. – skelly Feb 22 '18 at 21:29
  • Best would be to insert the objects with the New ID into a separate temporary collection. Upon completion delete the item from the original collection. Then get the item(s) in the temporary collection and insert them into the original collection, upon success delete them from the temporary collection. (you can do one by one or all at once, depends on the situation) – Raatje Apr 18 '18 at 22:26
  • I'm not sure about @skelly 's explanation , SQL is also allow alter the current id with [index], I think mongod not allow update the _id is the specification: https://docs.mongodb.com/manual/reference/command/update/ – Julian89757 Aug 03 '18 at 04:00
41

To do it for your whole collection you can also use a loop (based on Niels example):

db.status.find().forEach(function(doc){ 
    doc._id=doc.UserId; db.status_new.insert(doc);
});
db.status_new.renameCollection("status", true);

In this case UserId was the new ID I wanted to use

BrazaBR
  • 3
  • 3
Patrick Wolf
  • 2,530
  • 2
  • 28
  • 27
  • 1
    Would a snapshot() on find be advised to keep the forEach from accidentally picking up newer docs as it iterates? – John Flinchbaugh May 14 '14 at 21:04
  • This code snippet never completes. It keeps iterating over and over the collection for ever. Snapshot doesn't do what you expect (you can test it by taking a 'snapshot' adding a document to the collection, then seeing that that new document is in the snapshot) – Patrick Jan 16 '15 at 10:01
  • See http://stackoverflow.com/a/28083980/305324 for an alternative to snapshot. `list()` is the logical one, but for large databases this can exhaust the memory – Patrick Jan 22 '15 at 08:15
  • 4
    Uh, this has 11 upvotes but someone says it's an infinite loop? What's the deal here? – Andrew May 14 '15 at 02:17
  • 4
    @Andrew because contemporary pop-coder culture dictates that you should always acknowledge good input before actually verifying that said input actually works. – csvan Dec 30 '15 at 02:17
5

In case, you want to rename _id in same collection (for instance, if you want to prefix some _ids):

db.someCollection.find().snapshot().forEach(function(doc) { 
   if (doc._id.indexOf("2019:") != 0) {
       print("Processing: " + doc._id);
       var oldDocId = doc._id;
       doc._id = "2019:" + doc._id; 
       db.someCollection.insert(doc);
       db.someCollection.remove({_id: oldDocId});
   }
});

if (doc._id.indexOf("2019:") != 0) {... needed to prevent infinite loop, since forEach picks the inserted docs, even throught .snapshot() method used.

Mark
  • 51
  • 1
  • 2
  • `find(...).snapshot is not a function` but other than that, great solution. Also, if you want to replace `_id` with your custom id, you can check if `doc._id.toString().length === 24` to prevent the infinite loop (assuming your custom IDs aren't also [24 characters long](https://stackoverflow.com/questions/25356211/collection-id-length-in-mongodb)), – Dan Dascalescu Jan 28 '19 at 19:34
1

Here I have a solution that avoid multiple requests, for loops and old document removal.

You can easily create a new idea manually using something like:_id:ObjectId() But knowing Mongo will automatically assign an _id if missing, you can use aggregate to create a $project containing all the fields of your document, but omit the field _id. You can then save it with $out

So if your document is:

{
"_id":ObjectId("5b5ed345cfbce6787588e480"),
"title": "foo",
"description": "bar"
}

Then your query will be:

    db.getCollection('myCollection').aggregate([
        {$match:
             {_id: ObjectId("5b5ed345cfbce6787588e480")}
        }        
        {$project:
            {
             title: '$title',
             description: '$description'             
            }     
        },
        {$out: 'myCollection'}
    ])
Florent Arlandis
  • 866
  • 1
  • 10
  • 29
1

You can also create a new document from MongoDB compass or using command and set the specific _id value that you want.

D. Schreier
  • 1,700
  • 1
  • 22
  • 34
Talha Noyon
  • 824
  • 7
  • 13
0

As a very small improvement to the above answers i would suggest using

let doc1 = {... doc};

then

db.dyn_user_metricFormulaDefinitions.deleteOne({_id: doc._id});

This way we don't need to create extra variable to hold old _id.

Dharman
  • 30,962
  • 25
  • 85
  • 135
GPuri
  • 495
  • 4
  • 11
0

Slightly modified example of @Florent Arlandis above where we insert _id from a different field in a document:

 > db.coll.insertOne({ "_id": 1, "item": { "product": { "id": 11 } },   "source": "Good Store" })
 { "acknowledged" : true, "insertedId" : 1 }
 > db.coll.aggregate( [ { $set: { _id : "$item.product.id" }}, { $out: "coll" } ]) // inserting _id you want for the current collection
 > db.coll.find() // check that _id is changed
 { "_id" : 11, "item" : { "product" : { "id" : 11 } }, "source" : "Good Store" }

Do not use $match filter + $out as in @Florent Arlandis's answer since $out fully remove data in collection before inserting aggregate result, so effectively you will lose all data that don't match to $match filter

SQB
  • 3,926
  • 2
  • 28
  • 49
dododo
  • 3,872
  • 1
  • 14
  • 37
0

You could try using aggregation with an $out stage, leaving all ids untouched except for the one you want to modify.

db.clients.aggregate([
    {$addFields: {
        _id: {$function: {
            body: function(id) {
                // not sure if this exact condition will work but you get the gist
                return id === ObjectId("123") ? ObjectId("456") : id;
            },
            args: ["$_id"],
            lang: "js"
        }}
    }},
    {$out: "clients"}
]);
SQB
  • 3,926
  • 2
  • 28
  • 49