how can I modify a field name / key in a nested array of objects in mongodb? - mongodb

I have a mongodb collection with a number of objects like this:
{
"_id" : "1234",
"type" : "automatic",
"subtypes" : [
{
"_id" : "dfgd",
"name" : "test subtype",
"subjetRequired" : true,
},
{
"_id" : "dfgd",
"name" : "test subtype2",
"subjetRequired" : false,
}
],
"anotherField" : "some value"
}
As you can see, one of the keys in the subtypes array is incorrectly spelled - "subjetRequired" instead of "subjectRequired".
I want to correct that key name. How can I do that.
I'll preface this by saying I've not worked with mongodb very much in the past.
After a lot of researching, the best I could come up with is the following (which doesn't work):
function remap(doc) {
subtypes = doc.subtypes;
var count = 0;
subtypes.forEach(function(subtype){
db.taskType.update({"_id": subtype._id}, {
$set: {"subtypes.subjectRequired" : subtype.subjetRequired},
$unset: {"subtypes.subjetRequired": 1}
});
}
)
}
db.taskType.find({"subtypes.subjetRequired":{$ne:null}}).forEach(remap);
This doesn't work.
I know the loop is correct, as if I replace the other logic with print statements I can access and print the fields who's names I want to modify.
What am I doing wrong here?

You can use this update and avoid using any code, it's also stable so you can execute it multiple times with no fear.
db.collection.updateMany({
"subtypes.subjetRequired": {
$exists: true
}
},
[
{
$set: {
subtypes: {
$map: {
input: "$subtypes",
in: {
$mergeObjects: [
"$$this",
{
subjectRequired: "$$this.subjetRequired",
}
]
}
}
}
}
},
{
$unset: "subtypes.subjetRequired"
}
])
Mongo Playground

I could modify your loop to override the whole array of subtypes:
function remap(doc) {
correctSubtypes = doc.subtypes.map(({ subjetRequired, ...rest }) => ({
...rest,
subjectRequired: subjetRequired,
}));
var count = 0;
db.taskType.findByIdAndUpdate(doc._id, {
$set: {
subtypes: correctSubtypes,
},
});
}

Related

Overwrite value and create key while update query in 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.

Trim string values of whitespace, from an array of sub-documents with string field

On all documents of my collection I want to perform a $trim operation to a specific field of an object that is in an array.
Example:
{
"_id" : ObjectId("53857680f7b2eb611e843a32"),
"company": Testcompany
"customer" :
"name": Testuser,
"addresses" : [
{
"_id" : ObjectId("5d6d2f96e3fdc8001077ac6c"),
"street" : "Teststreet. ",
"houseNr" : "133",
},
{
"_id" : ObjectId("5d6d2f96e3fdc8001077ac7b"),
"street" : " Simplestreet. ",
"houseNr" : "12",
}
],
}
In the example, I want to $trim all values of the field: "customer.addresses.street"
To answer the upcoming questions:
I know the article you mentioned (Removing white spaces (leading and trailing) from string value) but theres no example how to do it within an array.
My problem is, how to access the attributes within an array, heres the example of plain values:
[{ $set: { category: { $trim: { input: "$category" } } } }],
Yes, I want to update the values of all documents within the collection
One possible way to do:
db.YOUR_COLLECTION.find({}).forEach(
function(doc) {
db.Trim.update(
{ "_id":doc._id },
{
"$set": {
"customer.addresses":doc.customer.addresses.map(
function(child) {
return Object.assign(
child,
{ street: child.street.trim() }
)
}
)
}
}
)
}
)
Obs: Solution with Javascript Executed in MongoShell.
You can use $map and $trim in an updateMany aggregation pipeline like this :
db.YOUR_COLLECTION.updateMany({"customer.addresses":{$ne:null}},[
{
$set: {
"customer.addresses":
{
$map: {
input: "$customer.addresses",
as: "address",
in: { $trim: { input: "$$address" } }
}
}
}
}
])

Filter sub-document array using substring as criteria

My collection:
{
title: 'Computers',
maincategories:[
{
title: 'Monitors',
subcategories:[
{
title: '24 inch',
code: 'AFG'
}
]
}
]
}
I want query the code. The code is just the first part so I want to have all subcategories that contains the given search. So AFG101 would return this subcategories.
My query:
module.exports = (req, res) => {
var q = {
'maincategories.subcategories': {
$elemMatch: {
code: 'AFG101'
}
}
};
var query = mongoose.model('TypeCategory').find(q, {'maincategories.$': 1, 'title': 1});
query.exec((err, docs) => {
res.status(200).send(docs);
});
};
My problem:
How do I search for a part of a string? AFG101 should return all subcategories with property code containing any part of the string. So in this case, AFG would be a hit. Same as in this sql question: MySQL: What is a reverse version of LIKE?
How do I project the subcategories. Current query returns all subcategories. I only want to returns those hitting.
The best way to do this is in MongoDB 3.4 using the $indexOfCP string aggregation operator.
let code = "afg101";
db.collection.aggregate([
{ "$project": {
"title": 1,
"maincategories": {
"$map": {
"input": "$maincategories",
"as": "mc",
"in": {
"$filter": {
"input": "$$mc.subcategories",
"as": "subcat",
"cond": {
"$gt": [
{
"$indexOfCP": [
code,
{ "$toLower": "$$subcat.code" }
]
},
-1
]
}
}
}
}
}
}}
])
which returns:
{
"_id" : ObjectId("582cba57e6f570d40d77b3a8"),
"title" : "Computers",
"maincategories" : [
[
{
"title" : "24 inch",
"code" : "AFG"
}
]
]
}
You can read my other answers to similar question 1, 2 and 3.
From 3.2 backward, the only way to do this is with mapReduce.
db.collection.mapReduce(
function() {
var code = 'AFG101';
var maincategories = this.maincategories.map(function(sdoc) {
return {
"title": sdoc.title,
"subcategories": sdoc.subcategories.filter(function(scat) {
return code.indexOf(scat.code) != -1;
}
)};
});
emit(this._id, maincategories);
},
function(key, value) {},
{ "out": { "inline": 1 }
})
which yields something like this:
{
"results" : [
{
"_id" : ObjectId("582c9a1aa358615b6352c45a"),
"value" : [
{
"title" : "Monitors",
"subcategories" : [
{
"title" : "24 inch",
"code" : "AFG"
}
]
}
]
}
],
"timeMillis" : 15,
"counts" : {
"input" : 1,
"emit" : 1,
"reduce" : 0,
"output" : 1
},
"ok" : 1
}
Well, just like your question has two parts, I could think of two separate solution, however I don't see a way to join them together.
For first part $where can be used to do a reverse regex, but it's dirty, it's an overkill and it can't use any indexes, since $where runs on each documents.
db.TypeCategory.find({$where:function(){for(var i in this.maincategories)
{for(var j in this.maincategories[i].subcategories)
{if("AFG101".indexOf(this.maincategories[i].subcategories[j].code)>=0)
{return true}}}}},{"maincategories.subcategories.code":1})
Even if you use this option, it would need couple of boundary check and it cannot project two level of nested array. MongoDB doesn't support such projection (yet).
For that purpose we might go for aggregation
db.TypeCategory.aggregate([{$unwind:"$maincategories"},
{$unwind:"$maincategories.subcategories"},
{$match:{"maincategories.subcategories.code":"AFG"}},
{$group:{_id:"$_id","maincategories":{$push:"$maincategories"}}}
])
However I don't think there is a way to do reverse regex check in aggregation, but I might be wrong too. Also this aggregation is costly since there are two unwinds which can lead to overflow the memory limit for aggregation for a really large collection.
You can use $substr and do it
db.getCollection('cat').aggregate([
{"$unwind" : "$maincategories"},
{"$unwind" : "$maincategories.subcategories"},
{"$project" :
{"maincategories" : 1,
"title":1,"sub" : {"$substr" :["$maincategories.subcategories.code",0,3]}}},
{"$match" : {"sub" : "AFG"}},
{"$project" :
{"maincategories" : 1,
"title":1}
}
])

How to add key to $addToSet in mongoDB

I want to add a key inside mongodb add function. I am doing this right now.
$addToSet : {
"msges":{
time:{"from":uname,"title":title,"msg":msg,"read":false}
}
}
time is a variable that is coming from the paramater. It has time inside it as hh:mm:ss A. But when the query runs, instead of time as key, string "time" gets print as key. Any ideas what should I do?
Enclose your variable in [] :
$addToSet: {
"msges": {
[time]: { "from": uname, "title": title, "msg": msg, "read": false }
}
}
For instance :
var myfield = "custom_field";
db.test.update({
_id: 1
}, {
$addToSet: {
letters: [{
[myfield]: 1
}, {
[myfield]: 2
}]
}
})
It gives :
{ "_id" : 1, "letters" : [ [ { "custom_field" : 1 }, { "custom_field" : 2 } ] ] }

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.