ChatGPT解决这个技术问题 Extra ChatGPT

How to update the _id of one MongoDB Document?

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?


N
Niels van der Rest

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

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.
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?
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.
Using just ObjectId() without the string as parameter will generate a new unique one.
@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.
B
BrazaBR

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


Would a snapshot() on find be advised to keep the forEach from accidentally picking up newer docs as it iterates?
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)
See stackoverflow.com/a/28083980/305324 for an alternative to snapshot. list() is the logical one, but for large databases this can exhaust the memory
Uh, this has 11 upvotes but someone says it's an infinite loop? What's the deal here?
@Andrew because contemporary pop-coder culture dictates that you should always acknowledge good input before actually verifying that said input actually works.
M
Mark

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.


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),
F
Florent Arlandis

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'}
    ])

Interesting idea... but you usually want to set the _id to a given value, rather than let MongoDB generate another one.
D
D. Schreier

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


D
Dharman

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.


d
dododo

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 loose all data that don't match to $match filter