Overwrite value and create key while update query in mongodb - mongodb

I have a mongodb collection that looks like this:
{
"_id" : ObjectId("60471bd482c0da3c0e70d26f"),
"owner" : "John",
"propAvailable" : {
"val1" : true
}
},
{
"_id" : ObjectId("60471bd482c0da3c0e76523f"),
"owner" : "Matt",
"propAvailable" : {
"val1" : {
"val2" : true
}
}
I need to run an update query on this collection that will update the value of the 'propAvailable' key such that
db.collection('props').update({'owner' : 'John'} , {$set : {'propAvailable.val1.val2' : true}});
This query works if the document already looks like the second one but gives the error:
Cannot create field 'val2' in element {'val1': true} if the document format is the first one. Is there a way to write this query so that it overwrites the boolean 'true' and replaces it with the object {'val2' : true}

You can use:
db.collection.update({
"owner": "John"
},
{
$set: {
"propAvailable.val1": {
val2: true
}
}
})
To create val2: true inside propAvailable.val1 and replace its current content.
As you can see working on the playground

If you're using Mongo version 4.2+ you can use pipelined updates to achieve this, like so:
db.collection.updateMany({
owner: "John"
},
[
{
$set: {
"propAvailable.val1": {
$mergeObjects: [
{
$cond: [
{
$eq: [
"object",
{
$type: "$propAvailable.val1"
}
]
},
"$propAvailable.val1",
{}
]
},
{
val2: true
}
]
}
}
},
])
Mongo Playground
For older mongo versions this is impossible to do in 1 query if objects potentially have additional fields under val1 you want to preserve. You will have to either read and update, or execute two different updates for each case.

Related

MongoDB - Update a field in an object of an array with multiple filter conditions

If I have the following document in my database :
{
"_id" : MainId,
"subdoc" : [
{
"_id" : SubdocId,
"userid" : "someid",
"toupdate": false,
},
{
"_id" : SubdocId2,
"userid" : "someid2",
"toupdate": false,
}
],
"extra" : [
"extraid1",
"extraid2"
]
}
How can I update the subdocument SubdocId2 where the id (SubdocId2) must match and either SubdocId2's userid is "someid2" OR value "extraid1" exists in "extra"?
The farthest I got is:
db.coll.update({
"subdoc._id":"SubdocId2", {
$or: ["extra":{$in:["extraid1"]}, "subdoc.userid":"someid2"]
}
}, {"subdoc.$.toupdate":true})
Maybe I forgot to quote up something, but I get an error (SyntaxError: invalid property id)
Please try this :
db.coll.update(
{
$and: [{ "subdoc._id": SubdocId2 }, {
$or: [{ "extra": { $in: ["extraid1"] } },
{ "subdoc.userid": "someid2" }]
}] // subdoc._id && (extra || subdoc.userid)
}, { $set: { "subdoc.$.toupdate": true } })
In your query there are couple of syntax issues & also if you don't use $set in your update part - it replace the entire document with "subdoc.$.toupdate": true. Of course if you're using mongoose that's different scenario as mongoose will internally does add $set but when executing in shell or any client you need to specify $set. Also if SubdocId2 is ObjectId() you need to convert string to ObjectId() in code before querying database.

MongoDB How to remove value from array if exist otherwise add

I have document
{
"_id" : ObjectId("5aebf141a805cd28433c414c"),
"forumId" : ObjectId("5ae9f82989f7834df037cc90"),
"userName" : "Name",
"usersLike" : [
"1","2"
],
"comment" : "Comment",
}
I want to remove value from usersLike array if the value exists, or add if the value does not exist.
Eg:
If I try to push 1 into usersLike, it should return
{
"_id" : ObjectId("5aebf141a805cd28433c414c"),
"forumId" : ObjectId("5ae9f82989f7834df037cc90"),
"userName" : "Name",
"usersLike" : [
"2"
],
"comment" : "Comment",
}
How can I query it..??
MongoDB version 4.2+ introduces pipelined update. Which means we can now use aggregation operators while updating. this gives us a lot of power.
db.collection.updateOne(
{
_id: ObjectId("597afd8200758504d314b534")
},
[
{
$set: {
usersLike: {
$cond: [
{
$in: ["1", "$usersLike"]
},
{
$setDifference: ["$usersLike", ["1"]]
},
{
$concatArrays: ["$usersLike", ["1"]]
}
]
}
}
}
]
)
Mongodb doesn't support conditional push or pull update. However you can still do it by using find:
db.collectionName.find({_id:ObjectId("597afd8200758504d314b534"),usersLike:{$in:["1"]}}).pretty()
if id exist in usersLike than pull else push.
Or you use the update query to pull as:
db.collectionName.update({
_id: ObjectId("597afd8200758504d314b534"),
usersLike: {
$in: ["1"]
}
}, {
$pull: { 'usersLike': "1" }
}, { multi: true })
And to push you can use:
db.collectionName.update({
_id:ObjectId("597afd8200758504d314b534"),
usersLike:{$nin:["1"]
}},{
$push:{'usersLike':"1"}
}, {multi: true})
Try this :
db.collectionName.update({_id:ObjectId("597afd8200758504d314b534")},{$pull:{'usersLike':"1"}}, {multi: true})
Try this
if db.collectionName.find({'_id':ObjectId("5aebf141a805cd28433c414c"),'usersLike:{'$in:['1']}}).count() > 0:
db.collectionName.update({'_id':ObjectId("5aebf141a805cd28433c414c")},{'$pull':{'usersLike':'1'}})
else:
db.collectionName.update({'_id':ObjectId("5aebf141a805cd28433c414c")},{'$addToSet':{'usersLike':'1'}})

Project values of different columns into one field

{
"_id" : ObjectId("5ae84dd87f5b72618ba7a669"),
"main_sub" : "MATHS",
"reporting" : [
{
"teacher" : "ABC"
}
],
"subs" : [
{
"sub" : "GEOMETRIC",
"teacher" : "XYZ",
}
]
}
{
"_id" : ObjectId("5ae84dd87f5b72618ba7a669"),
"main_sub" : "SOCIAL SCIENCE",
"reporting" : [
{
"teacher" : "XYZ"
}
],
"subs" : [
{
"sub" : "CIVIL",
"teacher" : "ABC",
}
]
}
I have simplified the structure of the documents that i have.
The basic structure is that I have a parent subject with an array of reporting teachers and an array of sub-subjects(each having a teacher)
I now want to extract all the subject(parent/sub-subjects) along with the condition if they are sub-subjects or not which are taught by a particular teacher.
For eg:
for teacher ABC i want the following structure:
[{'subject':'MATHS', 'is_parent':'True'}, {'subject':'CIVIL', 'is_parent':'FALSE'}]
-- What is the most efficient query possible ..? I have tried $project with $cond and $switch but in both the cases I have had to repeat the conditional statement for 'subject' and 'is_parent'
-- Is it advised to do the computation in a query or should I get the data dump and then modify the structure in the server code? AS in, I could $unwind and get a mapping of the parent subjects with each sub-subject and then do a for loop.
I have tried
db.collection.aggregate(
{$unwind:'$reporting'},
{$project:{
'result':{$cond:[
{$eq:['ABC', '$reporting.teacher']},
"$main_sub",
"$subs.sub"]}
}}
)
then I realised that even if i transform the else part into another query for the sub-subjects I will have to write the exact same thing for the property of is_parent
You have 2 arrays, so you need to unwind both - the reporting and the subs.
After that stage each document will have at most 1 parent teacher-subj and at most 1 sub teacher-subj pairs.
You need to unwind them again to have a single teacher-subj per document, and it's where you define whether it is parent or not.
Then you can group by teacher. No need for $conds, $filters, or $facets. E.g.:
db.collection.aggregate([
{ $unwind: "$reporting" },
{ $unwind: "$subs" },
{ $project: {
teachers: [
{ teacher: "$reporting.teacher", sub: "$main_sub", is_parent: true },
{ teacher: "$subs.teacher", sub: "$subs.sub", is_parent: false }
]
} },
{ $unwind: "$teachers" },
{ $group: {
_id: "$teachers.teacher",
subs: { $push: {
subject: "$teachers.sub",
is_parent: "$teachers.is_parent"
} }
} }
])

Mongo sort by string value that is actually number

I have collection that contains objects such as this:
{
"_id" : ObjectId("57f00cf47958af95dca29c0c"),
"id" : "...",
"threadId" : "...",
"ownerEmail" : "...#...",
"labelIds" : [
...
],
"snippet" : "...",
"historyId" : "35699995",
"internalDate" : "1422773000000",
"headers" : {
"from" : "...#...",
"subject" : "....",
"to" : "...#..."
},
"contents" : {
"html" : "...."
}
}
When accessing objects, I want to sort them by iternalDate value, which was supposed to be integer, however it is a string. Is there a way to sort them when fetching even if these are strings? By alphabetic order? Or is there a way to convert them to integer painlessly?
Collation is what you need...
db.collection.find()
.sort({internalDate: 1})
.collation({locale: "en_US", numericOrdering: true})
It seems to me that the best solution here would be to parse it first as an integer. You could do it using a simple script in javascript like this, using the mongodb client for node:
db.collection.find({}, {internalDate: 1}).forEach(function(doc) {
db.collection.update(
{ _id: doc._id },
{ $set: { internalDate: parseInt(doc.internalDate) } }
)
})
you also can use the aggregate method to sort number which is actually a string.
db.collection.aggregate([{
$sort : {
internalDate : 1
}
}], {
collation: {
locale: "en_US",
numericOrdering: true
}
});
if you are using mongoose-paginate package for serverside pagination .. so don't use this package, use only mongoose-paginate-v2 for serverside pagination. this package for nodejs side
I was having this issue. I use string length to sort first and then apply the sort of my numeric value stored like a string. e.g. "1", "100", "20", "3" that should be sorted like 1, 3, 29, 100.
db.AllTours.aggregate([
{
$addFields : {
"MyStringValueSize" : { $strLenCP: "$MyValue" }
}
},
{
$sort : {
"MyStringValueSize" : 1,
"MyValue" : 1
}
}
]);
There is a new feature in version 4.0 called $toInt that can be used to parse your string and then sort. In my case I can't upgrade from 3.6.
With aggregate, this works for me.
db.collection.aggregate([<pipeline>]).collation({locale:"en_US", numericOrdering:true})
This is my solution and it worked for me
db.getCollection('myCollection').aggregate([
{
$project: {
newMonth: {
$cond: { if: {
$and: [
{$ne: ['$month', '10']},
{$ne: ['$month', '11']},
{$ne: ['$month', '12']},
]
}, then: {$concat: ['0', '$month']}, else: '$month' }
}
}
},
{
$sort: {newMonth: -1}
}
])

way to update multiple documents with different values

I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.