How rename nested key in array of object in MongoDB? - mongodb

Document Structure
{
_id: 5,
grades: [
{ grade_ : 80, mean: 75, std: 8 },
{ mean: 90, std: 5 },
{ mean: 85, std: 3 }
]
}
As per above document structure in mongodb i want rename key grade_ to grade
db.collection.update({"_id":5},{"$rename":{"grades.grade_":"grades.grade"}},{"upsert":false,"multi":true})
which gives below error
"writeError" : {
"code" : 28,
"errmsg" : "cannot use the part (grades of grades.grade_) to traverse the element ({grades: [ { grade_: 80.0, mean: 75.0, std: 8.0 }, { mean: 90.0, std: 5.0 }, { mean: 85.0, std: 3.0 } ]})"
}
I want to rename key grade_ to grade, expected output
{
_id: 5,
grades: [
{ grade : 80, mean: 75, std: 8 },
{ mean: 90, std: 5 },
{ mean: 85, std: 3 }
]
}

As per MongoDB documentation: ($rename does not work if these fields are in array elements.)
For fields in embedded documents, the $rename operator can rename these fields as well as move the fields in and out of embedded documents. $rename does not work if these fields are in array elements.
So, you need to write your custom logic to update.
db.collection.find({
"grades.grade_": { $exists : 1 }
}).forEach( function( doc ) {
for( i=0; i < doc.grades.length; i++ ) {
if(doc.grades[i].grade_ != undefined) {
doc.grades[i].grade = doc.grades[i].grade_;
delete doc.grades[i].grade_;
}
}
db.collection.update({ _id : doc._id }, doc);
});

$rename do not works in an array. So,you can use Aggregate framework's $addField to rename fields in an array.
db.collection.aggregate([
{
$addFields: {
grades: {
$map: {
input: "$grades",
as: "grade",
in: {
grade: "$$grade.grade_",
mean: "$$grade.mean",
std: "$$grade.std"
}
}
}
}
}
])
Output:
[
{
"_id": 5,
"grades": [
{"grade": 80,"mean": 75,"std": 8},
{"mean": 90,"std": 5},
{"mean": 85,"std": 3}
]
}
]

Related

find index of particular element in multi-dimensional array in MongoDB

I've got a document in MongoDB which has a multi-dimensional array as shown below.
{"_id":1,
"name":"Johnson",
"card":[
["",12,25,"","",52,60,"",86],
[1,17,29,"",43,"","","",89],
[3,"","",34,45,"",62,70,""]
]
}
I'm looking for a query that returns the index of a particular element in the array, for example, say 29 whose index is [1][2] but when i queried as:
> db.test.aggregate([{$project:{index:{$indexOfArray:["$card",29]}}}])
i got the result as:
{ "_id" : 1, "index" : -1 }
which is not true. I found that this query method works only for one-dimensional array and I'm unable to figure out how to find the index of multi-dimensional array in MongoDB.
Any help will be appreciated.
Thankyou
Not exactly clear on what datatype [1][2] is, so rendering the desired output is a bit of a challenge. Here is a attempt to help your question...
Test Data
db.collection.insert(
{
"name":"Johnson",
"card":[
["", 12, 25, "", "", 52, 60, "", 86],
[1, 17, 29, "", 43, "", "", "", 89],
[3, "", "", 34, 45, "", 62, 70, ""]
]
}
(Assumes a hard-coded value of 29 to search for)
Aggregate
EDIT 2021-12-09 - ADDED $project TO CAST RESULTS AS INTEGER. WAS NumberLong()
db.collection.aggregate([
{
$unwind:
{
path: "$card",
includeArrayIndex: "outerIndex"
}
},
{
$unwind:
{
path: "$card",
includeArrayIndex: "innerIndex"
}
},
{
$match:
{
"card": 29
}
},
{
$project:
{
name: 1,
card: 1,
outerIndex: { $convert: { input: "$outerIndex", to: "int" } },
innerIndex: { $convert: { input: "$innerIndex", to: "int" } }
}
}
])
Results
[
{
_id: ObjectId("61b13476c6c466d7d1ea9b5e"),
name: 'Johnson',
card: 29,
outerIndex: 1,
innerIndex: 2
}
]
Unwanted fields can be supressed with another $project stage, but I did not include it here since I was not clear on desired output.

Mongodb: push element to nested array if the condition is met

I have the following collection:
{
"_id": 11,
"outerArray": [
{ "_id" : 21,
"field": {
"innerArray" : [
1,
2,
3
]
}
},
{ "_id" : 22,
"field": {
"innerArray" : [
2,
3
]
}
},
{ "_id" : 23,
"field": {
"innerArray" : [
2
]
}
}
]
}
I need to go through all documents in collection and push to innerArray new element 4, if innerArray already contains element 1 or element 3
I tried to do it this way, and few others, similar to this one, but it didn't work as expected, it only pushes to innerArray of first element of outerArray
db.collection.updateMany(
{ "outerArray.field.innerArray": { $in: [ 1, 3 ] } },
{ $push: { "outerArray.$.field.innerArray": 4} }
)
How to make it push to all coresponding innerArrays?
Problem here is your missunderstanding a copule things.
When you do "outerArray.field.innerArray": { $in: [ 1, 3 ] } into your query, your are not getting only innerArray where has 1 or 3. You are gettings documents where exists these arrays.
So you are querying the entire document.
You have to use arrayFilter to update values when the filter is match.
So, If I've understood you correctly, the query you want is:
db.collection.update(
{}, //Empty object to find all documents
{
$push: { "outerArray.$[elem].field.innerArray": 4 }
},
{
"arrayFilters": [ { "elem.field.innerArray": { $in: [ 1, 3 ] } } ]
})
Example here
Note how the first object into update is empty. You have to put there the field to match the document (not the array, the document).
If you want to update only one document you have to fill first object (query object) with values you want, for example: {"_id": 11}.

Converting array of objects to array of array

I am new to NoSQL databases and MongoDb. I've got the following question:
I have a collection of such documents:
{
vals: [
{
value: 111,
timestamp: 1563454669669
},
{
value: 222,
timestamp: 1563454689665
},
{
value: 333,
timestamp: 1563454669658
}
.......
]
}
I would like to convert it into the following documents using aggregation pipeline:
{
vals: [
[
111,
1563454669669
],
[
222,
1563454689665
],
[
333,
1563454669658
]
.......
]
}
After years of work with relational databases, it's quite hard to understand..
You can use $map operator to transform one array into another array:
db.collection.aggregate([
{
$project: {
vals: {
$map: {
input: "$vals",
in: [ "$$this.value", "$$this.timestamp" ]
}
}
}
}
])
Mongo Playground

Trying to aggregate based on substring matches in mongodb 3.2

Let's say my collection has documents with ExpName field and Rname field. Expname are all of the type - exp_1, exp_2 etc. Rname is a character string with 4 dashes for example. "As-34rt-d3r5-4453f-er4"
I need to aggregate based on experiment name and removing the text between the last two dashes. In the example I gave above that would be "As-34rt-d3r5"
question 1) how do i incorporate this in one table?
question 2) i solved this in a dirty fashion for one exp, because it seemed like the number of characters was almost the same, so I could just take the first 13 characters which seemed like it was the the substring omitting the last two dashes. Is there a correct way to do this if the text was not so uniform?
db.getCollection('rest01').aggregate(
{$match : {ExpName : "exp_1"}},
{$group: {_id :"$ExpName",_id : {$substr : ["$RName", 0,13]}, total: { $sum:1 }}
})
Ideally I would like to have a result that says Expname, Rnamesubstring, count. This code snippet was for exp_1 one alone. Is it even possible to get it all in one result?
Here is how you could do that:
db.getCollection('rest01').aggregate({
$project: {
"ExpName": 1,
"splitRName": { $split: [ "$RName", "-" ] } // add an array with the constituents of your dash-delimited string id as a new field "splitRName"
}
}, {
$group: {
_id: { // our group id shall be made up of both...
"ExpName": "$ExpName", // ...the "ExpName" field...
"Rnamesubstring": { // and some parts of the "RName" field
$concat:
[
{ $arrayElemAt: [ "$splitRName", 0 ] },
"-",
{ $arrayElemAt: [ "$splitRName", 1 ] },
"-",
{ $arrayElemAt: [ "$splitRName", 2 ] }
]
}
},
total: { $sum: 1 }
}
})
In case you want to do it in MongoDB v3.2 (as stated in your comment), here is something that is not exactly pretty but works:
db.getCollection('rest01').aggregate({
$group: {
_id: { // our group id shall be made up of both...
"ExpName": "$ExpName", // ...the "ExpName" field...
"Rnamesubstring": {
$substr:
[
"$RName",
0,
{
$ifNull:
[
{
$arrayElemAt:
[{
$filter: {
input: {
$map: {
input: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 /* add numbers as required */ ],
as: "index",
in: {
$cond: {
if: { $eq: [ "-", { $substr: [ "$RName", "$$index", 1 ] } ] }, // if the string we look at is a dash...
then: "$$index", // ...then let's remember it
else: null // ...otherwise ignore it
}
}
}
},
as: "item",
cond: { $ne: [ null, "$$item" ] } // get rid of all null values
}
},
2 ] // we want the position of the third dash in the string (only)
},
1000 // in case of a malformed RName (wrong number of dashes or completely missing) we want the entire substring
]
}
]
}
},
total: { $sum: 1 }
}
})
Update 2: You seem to be having some data related issues as per your comments (so either missing RName values or improperly structured ones, i.e. without the required number of sections with dashes in between). I have updated the above statement for v3.2 to deal with these rows. You may want to find out, though, which rows actually cause this behaviour. They can be easily identified using the following statement:
db.getCollection('rest01').aggregate({
$project: {
_id: 1,
RName: 1,
"Rnamesubstring": {
$arrayElemAt:
[{
$filter: {
input: {
$map: {
input: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 /* add numbers as required */ ],
as: "index",
in: {
$cond: {
if: { $eq: [ "-", { $substr: [ "$RName", "$$index", 1 ] } ] }, // if the string we look at is a dash...
then: "$$index", // ...then let's remember it
else: null // ...otherwise ignore it
}
}
}
},
as: "item",
cond: { $ne: [ null, "$$item" ] } // get rid of all null values
}
},
2 ] // we want the position of the third dash in the string (only)
}
}
}, {
$match: { "Rnamesubstring": { $exists:false } }
})

Use mongo positional operator ($) and find in multiple arrays

Let me extend the example from the docs to:
{
_id: 4,
tags: ['good', 'great', 'nice'],
grades: [
{ grade: 80, mean: 75, std: 8 },
{ grade: 85, mean: 90, std: 5 },
{ grade: 90, mean: 85, std: 3 }
]
}
Now I want to update all grade=85 which have also an tag good. I would try to use:
db.students.update(
{ tags: "good", "grades.grade": 85 },
{ $set: { "grades.$.std" : 6 } }
)
However, mongo will set the $ to 0 because the tag good is the first in the tag list. This causes the data to change to:
{
_id: 4,
tags: ['good', 'great', 'nice'],
grades: [
{ grade: 80, mean: 75, std: 6 }, //←wrong
{ grade: 85, mean: 90, std: 5 },
{ grade: 90, mean: 85, std: 3 }
]
}
Question: Is there a way to tell mongo and/or mongoose to use the position in grades to reference the grade?
It happened because you use two arrays in a single query document. But unfortunately, it is not supported in MongoDB.
MongoDB documentation
The query document should only contain a single condition on the array
field being projected. Multiple conditions may override each other
internally and lead to undefined behavior.
Under these requirements, the following query is incorrect:
db.collection.find( { <array>: <value>, <someOtherArray>: <value2> },
{ "<array>.$": 1 } )
You can try to write something like that:
db.test.find({tags: "good", "grades.grade": 85 }).forEach(function(doc){ db.test.update(
{_id: doc._id, "grades.grade": 85 },
{ $set: { "grades.$.std" : 88 }},
{multi: true})})
With mongoose you should create two seperate models. One for Students and an other one for Grades. Then let Students contain references to Grades.
This will allow to update single Grades easily.
Something like:
var studentSchema = mongoose.Schema({
tags: [String],
grades: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Grade'
}
});
var Student = mongoose.model('Student', studentSchema);
var gradeSchema = mongoose.Schema({
grade: Number,
mean: Number,
std: Number
});
var Grade = mongoose.model('Grade', studentSchema);
Your update code could look like this:
Student.find({tags: { $in: ['good'] }}).populate('grades', { grade: 85 }).exec(function(err, students) {
students.grades.forEach(function(grade) {
grade.update(null, { std: 6 }, function(err, numAffected) {
});
});
});