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

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) {
});
});
});

Related

Finding duplicates with the same values MongoDB

I have a data set that I want to find the duplicates with the same values in multiple rows. For instance I have a data set that contains 12 different rows and I know the bottom 2 rows have similar values besides the _id and title values. How do I query to find these results? As if i dont already know these are duplicates.
My collection is 'sales'
[
{
_id: "C12",
title: "blouse",
price: 15,
units_sold: 100,
retail_price: 30,
ad_boost: 1,
rate_count: 34,
rating: 4
},
{
_id: "C10",
title: "loose floral blouse",
price: 15,
units_sold: 100,
retail_price: 30,
ad_boost: 1,
rate_count: 34,
rating: 4
}
]
Simply $group by all the fields that you identify as duplicate check key. You can $push the _id into an array for later fetching/processing.
db.collection.aggregate([
{
$group: {
_id: {
price: "$price",
units_sold: "$units_sold",
retail_price: "$retail_price",
ad_boost: "$ad_boost",
rate_count: "$rate_count",
rating: "$rating"
},
duplicate_ids: {
$push: {
_id: "$_id",
title: "$title"
}
}
}
}
])
Here is the Mongo playground for your reference.

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 insert then conditional sum followed by updating sum to another collection

I am trying to
insert a new record with a points field in to reputationActivity collection
get sum of points from reputationActivity collection where user id matches
insert the resulting sum to users collection
Here is mongo playground which does not work right now - https://mongoplayground.net/p/tHgPpODjD6j
await req.db.collection('reputationActivity').insertOne({
_id: nanoid(),
"userId": userId,//insert a new record for this user
"points": points,//insert integer points
},
function(){
req.db.collection('reputationActivity').aggregate([ { $match: { userId: userId } },
{ TotalSum: { $sum: "$points" } } ]); // sum of point for this user
req.db.collection('users').updateOne(
{_id: userId},
{$set: {"userTotalPoints": TotalSum}},// set sum in users collection
)
}
)
});
The above code gives me an error that Total sum is not defined. Is it better to do this without a callback function and if so, how?
write in one single block of command, use drop command carefully. it's used here to show start to end and illustration purposes only.
> db.reputation.drop();
true
> db.reputation.insertOne(
... {_id: 189, "userId": 122, "points": 60});
{ "acknowledged" : true, "insertedId" : 189 }
> var v_userId = 122;
> db.reputation.aggregate([
... {
... $match: {
... userId: v_userId
... }
... },
... {
... $group: {
... "_id": null, "TotalSum": {$sum: "$points"}}
... },
... ]).forEach(function(doc)
... {print("TotalSum: " +doc.TotalSum,
... "userId: " +v_userId);
... db.users.updateOne(
... {"userId":v_userId}, {$set:{"userPoints":doc.TotalSum}});
... }
... );
TotalSum: 60 userId: 122
> db.users.find();
{ "_id" : 189, "userId" : 122, "points" : 60, "userPoints" : 60 }
>

How rename nested key in array of object in 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}
]
}
]

MongoDB: Find and then modify the resulting object

is it possible in MongoDB to find some objects that match a query and then to modify the result without modifying the persistent data?
For example, let
students = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 22 },
{ name: "Carol", age: 19 },
{ name: "Dave", age: 18}
]
Now, I want to query all students that are younger than 20 and in the search result, I just want to replace "age: X" with "under20: 1" resulting in the following:
result = [
{ name: "Carol", under20: 1 },
{ name: "Dave", under20: 1}
]
without changing anything in the database.
Sure, it is possible to get the result and then call a forEach on it, but that sounds so inefficient because I have to rerun every object again, so I'm searching for an alternative. Or is there no one?
A possible solution would be to use an aggregation pipline with a $match followed by a $project:
db.students.aggregate(
[
{
$match: { age: { $lt: 20 } }
},
{
$project:
{
_id: false,
name: true,
under20: { $literal: 1 }
}
}
]);
The $literal: 1 is required as just using under20: 1 is the same as under20: true, requesting that field under20 be included in the result: which would fail as under20 does not exist in the document produced by the match.
Or to return all documents in students and conditionally generate the value for under20 a possible solution would be to use $cond:
db.students.aggregate(
[
{
$project:
{
_id: false,
name: true,
under20:
{
$cond: { if: { $lt: [ "$age", 20 ] }, then: 1, else: 0 }
}
}
}
]);