Prevent mongoose "Model.updateOne" from updating ObjectId(_id) of the model when using "$set" - node.js

I'm updating the age and name of a character with a specific _id from an array of characters that is inside a document of model Drama.
The document I'm working with:-
{
"_id" : ObjectId("619d44d2ec2ca20ca0404b5a"),
"characters" : [
{
"_id" : ObjectId("619fdac5a03c8b10d0b8b13c"),
"age" : "23",
"name" : "Vinay",
},
{
"_id" : ObjectId("619fe1d53810a130207a409d"),
"age" : "25",
"name" : "Raghu",
},
{
"_id" : ObjectId("619fe1d53810a130207a502v"),
"age" : "27",
"name" : "Teju",
}
],
}
So to update the character Raghu I did this:-
const characterObj = {
age: "26",
name: "Dr. Raghu",
};
Drama.updateOne(
{ _id: req.drama._id, "characters._id": characterId },
{
$set: {
"characters.$": characterObj,
},
},
function(err, foundlist) {
if (err) {
console.log(err);
} else {
console.log("Update completed");
}
}
);
// req.drama._id is ObjectId("619d44d2ec2ca20ca0404b5a")
// characterId is ObjectId("619fe1d53810a130207a409d")
This updated the character but it also assigned a new ObjectId to the _id field of the character. So, I'm looking for ways on how to prevent the _id update.
Also, I know I can set the individual fields of character instead of assigning a whole new object to prevent that but it will be very tedious if my character's object has a lot of fields.
//Not looking to do it this way
$set: {
"characters.$.age": characterObj.age,
"characters.$.name": characterObj.name,
},
Thanks.

I found something here, just pre define a schema (a blueprint in a way) that affects the id
var subSchema = mongoose.Schema({
//your subschema content
},{ _id : false });
Stop Mongoose from creating _id property for sub-document array items
Or I would say, when you create a character assign it a custom id from the start, that way it will retain that id throughout.

I'm leaving this question open as I would still like to see a simpler approach. But for now, I did find one easy alternative solution for this issue which I'm will be using for some time now until I find a more direct approach.
In short - Deep merge the new object in the old object using lodash and then use the new merged object to set field value.
For example, let's update the character Raghu from my question document:-
First install lodash(Required for deep merging objects) using npm:
$ npm i -g npm
$ npm i --save lodash
Import lodash:
const _ = require("lodash");
Now update the character Raghu like this:-
const newCharacterObj = {
age: "26",
name: "Dr. Raghu",
};
Drama.findById(
{ _id: req.drama._id, "characters._id": characterId },
"characters.$",
function(err, dramaDocWithSpecificCharacter) {
console.log(dramaDocWithSpecificCharacter);
// ↓↓↓ console would log ↓↓↓
// {
// "_id" : ObjectId("619d44d2ec2ca20ca0404b5a"),
// "characters" : [
// {
// "_id" : ObjectId("619fe1d53810a130207a409d"),
// "age" : "25",
// "name" : "Raghu",
// }
// ],
// }
const oldCharacterObj = dramaDocWithSpecificCharacter.characters[0];
const mergedCharacterObjs = _.merge(oldCharacterObj, newCharacterObj);
// _.merge() returns a deep merged object
console.log(mergedCharacterObjs);
// ↓↓↓ console would log ↓↓↓
// {
// _id: 619fe1d53810a130207a409d,
// age: "26",
// name: "Dr. Raghu",
// };
Drama.updateOne(
{ _id: req.drama._id, "characters._id": characterId },
{
$set: {
"characters.$": mergedCharacterObjs,
},
},
function(err, foundlist) {
if (err) {
console.log(err);
} else {
console.log("Update completed");
}
}
);
}
);
// req.drama._id is ObjectId("619d44d2ec2ca20ca0404b5a")
// characterId is ObjectId("619fe1d53810a130207a409d")
Note: We can also use the native Object.assign() or … (spread operator) to merge objects but the downside of it is that it doesn’t merge nested objects which could cause issues if you later decide to add nested objects without making changes for deep merge.

You can pass your payload or request body like this if we provide _id it will prevent update to nested document
"characters" : [
{
"_id" : "619fdac5a03c8b10d0b8b13c",
"age" : "updated value",
"name" : "updated value",
}, {
"_id" : "619fe1d53810a130207a409d",
"age" : "updated value",
"name" : "updated value",
}, {
"_id" : "619fe1d53810a130207a502v",
"age" : "updated value",
"name" : "updated value",
}
],
It works for me for bulk update in array object

Related

MongoDB add field to an object inside an array

I have an object user that looks like that
{
"_id" : ObjectId("5edbdf57ac52325464b054ec"),
...
"purchaseHistory" : [
{
"_id" : ObjectId("5ee7a8f6b438a1254cec3f74"),
...
},
{
"_id" : ObjectId("5ee7a8f6b438a1254cec3f88"),
...
}
]
}
What I wanna do is to add a new field to a specific object inside "purchaseHistory" by ID, for example I wanna add to "5ee7a8f6b438a1254cec3f88" a field "status": 0
What I tried is
users.findOneAndUpdate(
{
_id: ObjectId(userId),
'purchaseHistory._id': ObjectId(saleId)
},
{
$set: { 'purchaseHistory.$.status': status}
}
)
But it gives me an error, how can I do it properly?
According to the website provided by D. SM, I was able to do it this way
users.findOneAndUpdate(
{
'_id': ObjectId(userId),
'purchaseHistory._id': ObjectId(saleId)
},
{
$set: { 'purchaseHistory.$.status': status }
}
)
MongoDB provides the positional update operator for cases like this.

mongoose #set and $unset conditional

in my project i want to allow optional fields in a document, but i don't want to save any null values, i.e. i want to remove any field with null.
the values can be changed during run time.
i found how i can make a field null if the user didn't send data to update it (all the fields that the user send empty values for should get deleted).
if the user send firstName/lastName as an empty string in the form i want to remove them.
await Employee.findOneAndUpdate({ _id: employee._id },
{$set: {firstName: null, lastName: null, ...req.body.newEmployee}}, { new: true, upsert: false });
i tried to add an $unset option but i got an error
MongoError: Updating the path 'firstName' would create a conflict at 'firstName'
i thought about deleting it after the update (as a second command) but i can't find a way to tell/get all the null fields of the employee, and i can't get a fixed values to check because there are many combination for the null/not null values, especially if there will be more fields in the future.
i.e. i cant tell if (FN: null, LN: null or FN: "johny", LN: null etc..)
Update : In case if you need to keep some fields as is in existing document then try this, with this new code only the fields coming in request will either be updated or deleted doesn't alter other existing fields in document :
Your node.js code :
let removeObj = {}
Object.entries(req.body).forEach(([key, val]) => {
if (!val) { delete req[body][key]; removeObj[key] = '' };
})
let bulkArr = []
if (req.body) bulkArr.push({
updateOne: {
"filter": { "_id": ObjectId("5e02da86400289966e8ffa4f") },
"update": { $set: req.body }
}
})
if (Object.entries(removeObj).length > 0 && removeObj.constructor === Object) bulkArr.push({
updateOne: {
"filter": { "_id": ObjectId("5e02da86400289966e8ffa4f") },
"update": { $unset: removeObj }
}
})
// For mongoDB4.2 removeObj has to be an array of strings(field names)
let removeObj = []
Object.entries(req.body).forEach(([key, val]) => {
if (!val) { delete req[body][key]; removeObj.push(key) };
})
// After this, Similar to above you need to write code to exclude empty removeObj for 4.2 as well.
Query :: On mongoDB version >= 3.2 to < 4.2 .bulkWrite():
db.yourCollectionName.bulkWrite(bulkArr)
Query :: From mongoDB version 4.2 .updateOne accepts aggregation pipeline :
db.yourCollectionName.updateOne(
{ "_id": ObjectId("5e02da86400289966e8ffa4f") },
[
{
$set: req.body
},
{ $unset: removeObj }
]
)
Old Answer : You need to try .findOneAndReplace() , if you want entire doc to be replaced :
db.yourCollectionName.findOneAndReplace({_id: ObjectId("5e02da86400289966e8ffa4f")},inputObj, {returnNewDocument:true})
Your collection :
{
"_id" : ObjectId("5e02da86400289966e8ffa4f"),
"firstName" : "noName",
"lastName": 'noName',
"value" : 1.0,
"empDept": 'general',
"password":'something'
}
Your request object is like :
req.body = {
firstName: 'firstName',
lastName: null,
value : 1.0,
empDept: ''
}
your node.js code (Removes all falsy values ( "", 0, false, null, undefined )) :
let inputObj = Object.entries(req.body).reduce((a,[k,v]) => (v ? {...a, [k]:v} : a), {})
your collection after operation :
{
"_id" : ObjectId("5e02da86400289966e8ffa4f"),
"firstName" : "firstName",
"value" : 1.0,
}
your collection after operation as per updated answer :
{
"_id" : ObjectId("5e02da86400289966e8ffa4f"),
"firstName" : "firstName",
"value" : 1.0,
"password":'something'
}

Issue: updating value inside the subdocument of a subdocument

I am having some trouble trying to update some fields of a subdocument inside a subdocument in mongodb.
First of all, let's find exactly the document I need to update to see how the document structure looks like:
// query:
db.getCollection('collection').find({
application: ObjectId("568b3a2feaa4171d03734776"),
_id: ObjectId("568b3a2feaa4171d03734779"),
status: 'sent',
'mailingList.recipients.email': 'example#example.com' // an index
},
{
'mailingList.$.recipients': true
});
// query result:
{
"_id" : ObjectId("568b3a2feaa4171d03734779"),
"mailingList" : [
{
"id" : 55,
"recipients" : [
{
"metadata" : {
"name" : "Example",
"surname" : "Example"
},
"email" : "example#example.com",
"message" : {
"events" : []
}
}
]
}
]
}
What I would like to achieve is exactly being able to update any of the fields in the object inside recipients[]: let's say, for example, email. I've tried the following so far, using the $set operator:
db.getCollection('collection').update({
application: ObjectId("568b3a2feaa4171d03734776"),
_id: ObjectId("568b3a2feaa4171d03734779"),
status: 'sent',
'mailingList.recipients.email': 'example#example.com'
},
{
$set: {
'mailingList.$.recipients.email': 'newmail#example.com'
}
});
but I get the following error:
cannot use the part (recipients of mailingList.1.recipients.email) to
traverse the element ({recipients: [ { metadata: { name: "Example",
surname: "Example" }, email: "example#example.com", message: { events:
[] } } ]})
What am I missing? I had already worked with embedded subdocuments before (not like this where there is a subdocument inside another subdocument) and using $set was enough to update any field inside a single subdocument, i.e:
$set: {
'mailingList.$.email': 'newmail#gmail.com'
}

mongodb deep update - mongoose .id() causing performance issue

mongoose level collection schema
{
sublevel: [{
"deeplevel": [{
}],
"deeplevel2": [{
}],
}]
}
//routes.js
dlDoc = { "dl1": "dl1" };
db.levels.update({ _id: ObjectId(levelId), "sublevel._id": ObjectId(sublevelId) }, { $push: { "sublevel.$.deeplevel1": dlDoc } }, function (err, updatedDoc) {
if (updatedDoc) {
res.json({ "status": 1 });
res.end();
//calling external apis to update more
result = externalApiResult();
db.levels.findById(level._id, 'sublevel._id sublevel.deeplevel1', function (err, levelFound) {
levelFound.sublevel.id(sublevelId).deeplevel.id(dlDoc._id)['result'] = result;
levelFound.save(function (err, savedDoc) {
});
})
}
});
I need to create and update the level collection's deeplevel1. in my case, the deeplevel1 will be inserted more than 2000 sub docs per day. when i want to update using mongoose .id(deepLevelId) function it is causing me performance issues in the server. is there any way to find the position of deeplevel document inserted once i update the level collection?. so that i can use the position in the update query. And please tell me a best way to update my deeplevel subdocuments without causing performance issues and works fast. thanks in advance.
db.collection.findOne() will be like this.
{
"_id" : ObjectId("563b35d1f07cc9d80a26436b"),
"name":"name",
sublevel:[{
"_id" : ObjectId("569bede3c717b670097519c7"),
"name" : "name",
"deeplevel2" : [{
"_id" : ObjectId("569c559329cc880c18989349"),
"logTime" : new Date("1/18/2016 03:30:48"),
"areaId" : ObjectId("568a6129de552e7dba98d547"),
}, {
"_id" : ObjectId("569c561929cc880c18989354"),
"logTime" : new Date("1/18/2016 03:30:48"),
"areaId" : ObjectId("568a6129de552e7dba98d547"),
}, {
"_id" : ObjectId("569c5945626ffb680e4512e9"),
"logTime" : new Date("1/18/2016 03:30:48"),
"areaId" : ObjectId("568a6129de552e7dba98d547"),
}, {
"_id" : ObjectId("569c594d626ffb680e4512eb"),
"logTime" : new Date("1/18/2016 03:30:48"),
"areaId" : ObjectId("568a6129de552e7dba98d547"),
}
]
}]
}

Fetch sub-document Mongodb only match with criteria

I have data in mongodb like this:
{
"_id" : ObjectId("55a12bf6ea1956ef37fe4247"),
"tempat_lahir" : "Paris",
"tanggal_lahir" : ISODate("1985-07-10T17:00:00.000Z"),
"gender" : true,
"family" : [
{
"nama" : "Robert Deniro",
"tempat_lahir" : "Bandung",
"tanggal_lahir" : ISODate("2015-07-09T17:00:00.000Z"),
"pekerjaan" : "IRT",
"hubungan" : "XXX",
"tanggungan" : false,
"_id" : ObjectId("55a180f398c9925299cb6e90"),
"meta" : {
"created_at" : ISODate("2015-07-11T20:59:25.242Z"),
"created_ip" : "127.0.0.1",
"modified_at" : ISODate("2015-07-12T15:54:39.682Z"),
"modified_ip" : "127.0.0.1"
}
},
{
"nama" : "Josh Groban",
"tempat_lahir" : "Jakarta",
"tanggal_lahir" : ISODate("2015-06-30T17:00:00.000Z"),
"pekerjaan" : "Balita",
"hubungan" : "Lain-Lain",
"tanggungan" : true,
"_id" : ObjectId("55a29293c65b144716ca65b2"),
"meta" : {
"created_at" : ISODate("2015-07-12T16:15:15.675Z"),
"created_ip" : "127.0.0.1"
}
}
]
}
when i try to find data in sub-document, with this code:
person.findOne({ _id: req.params.person, {'family.nama': new RegExp('robert', 'gi') }}, function(err, data){
// render code here
});
It show all data in Family Data,
Can we fetch or display a data only match with criteria/keyword, for example only "Robert Deniro" row
Thank You
In 'regular' MongoDB, you can use the $ operator for that. I'm not sure if it works with Mongoose, but it's worth a try:
person.findOne({
_id : req.params.person,
'family.nama' : new RegExp('robert', 'gi')
}, {
// Only include the subdocument(s) that matched the query.
'family.$' : 1
}, function(err, data){
// render code here
});
If you need any of the properties from the parent document (tempat_lahir, tanggal_lahir or gender; _id will always be included), you need to add them to the projection object explicitly.
One caveat: the $ operator will only return the first matching document from the array. If you need it to return multiple documents, you can't use this method and (AFAIK) have to postprocess the results after they are returned from the database.
It solved with this code:
var options = {
family: {
$elemMatch: { nama: req.query.keyword }
},
};
person.findOne({ _id: req.params.person, 'family.nama': keyword }, options, function(err, data){
//render code here
});
Thanks to #hassansin & #robertklep

Resources