MongoDB $group by id where id name varies (conditionals?) - mongodb

I need to group all entries by id using $group. However, the id is under different objects, so is $student.id for some and $teacher.id for others. I've tried $cond, $is and any other conditional I could find but haven't had any luck. What I'd want is something like this:
lessons.aggregate([
// matches, lookups, etc.
{$group: {
"_id":{
"id": (if student exists "student.id", else if teacher exists "teacher.id"
},
// other fields
}
}}]);
How can I do this? I've scoured the MongoDB docs for hours yet nothing works. I'm new to this company and trying to debug something so not familiar with the tech yet, so apologies if this is rudimentary stuff!
Update: providing some sample data to demo what I'd want. Shortened from the real thing to fit the question. After all the matches, lookups, etc and before using $group, the data looks like this. As student.id of first and second objects are the same, I want them to be grouped.
{
student: {
_id: new ObjectId("61dc0fce904d07184b461c03"),
name: Jess W
},
duration: 30
},
{
student:{
_id: new ObjectId("61dc0fce904d07184b461c03"),
name: Jess W
},
duration: 30
},
{
teacher: {
_id: new ObjectId("61dc0f6a904d07184b461be7"),
name: Michael S
},
duration: 30
},
{
teacher: {
_id: new ObjectId("61dc1087904d07184b461c6a"),
name: Andrew J
},
duration: 30
},

If the fields exist "exclusive only", then you can simply combine them:
{ $group: {_id: {student: "$student.id", teacher: "$teacher.id"} }, // other fields }
concatenate them should also work:
{ $group: {_id: {$concat: [ "$student.id", "$teacher.id" ] }, // other fields }

You can just use $ifNull and chain them, like so:
db.collection.aggregate([
{
$group: {
_id: {
"$ifNull": [
"$student._id",
{
$ifNull: [
"$teacher._id",
"$archer._id"
]
}
]
}
}
}
])

Related

after aggregation how to check two fields are equal inside a document in mongodb

{
id: 1,
name: "sree",
userId: "001",
paymentData: {
user_Id: "001",
amount: 200
}
},
{
id: 1,
name: "sree",
userId: "001",
paymentData: {
user_Id: "002",
amount: 200
}
}
I got this result after unwind in aggregation any way to check user_Id equal to userId
Are you looking to only retrieve the results when they are equal (meaning you want to filter out documents where the values are not the same) or are you looking to add a field indicating whether the two are equal?
In either case, you append subsequent stage(s) to the aggregation pipeline to achieve your desired result. If you want to filter the documents, the new stage may be:
{
$match: {
$expr: {
$eq: [
"$userId",
"$paymentData.user_Id"
]
}
}
}
See how it works in this playground example.
If instead you want to add a field that compares the two values, then this stage may be what you are looking for:
{
$addFields: {
isEqual: {
$eq: [
"$userId",
"$paymentData.user_Id"
]
}
}
}
See how it works in this playground example.
You could also combine the two as in:
{
$addFields: {
isEqual: {
$eq: [
"$userId",
"$paymentData.user_Id"
]
}
}
},
{
$match: {
isEqual: true
}
}
Playground demonstration here

MongoDB - Unable to add timestamp fields to subdocuments in an array

I recently updated my subschemas (called Courses) to have timestamps and am trying to backfill existing documents to include createdAt/updatedAt fields.
Courses are stored in an array called courses in the user document.
// User document example
{
name: "Joe John",
age: 20,
courses: [
{
_id: <id here>,
name: "Intro to Geography",
units: 4
} // Trying to add timestamps to each course
]
}
I would also like to derive the createdAt field from the Course's Mongo ID.
This is the code I'm using to attempt adding the timestamps to the subdocuments:
db.collection('user').updateMany(
{
'courses.0': { $exists: true },
},
{
$set: {
'courses.$[elem].createdAt': { $toDate: 'courses.$[elem]._id' },
},
},
{ arrayFilters: [{ 'elem.createdAt': { $exists: false } }] }
);
However, after running the code, no fields are added to the Course subdocuments.
I'm using mongo ^4.1.1 and mongoose ^6.0.6.
Any help would be appreciated!
Using aggregation operators and referencing the value of another field in an update statement requires using the pipeline form of update, which is not available until MongoDB 4.2.
Once you upgrade, you could use an update like this:
db.collection.updateMany({
"courses": {$elemMatch: {
_id:{$exists:true},
createdAt: {$exists: false}
}}
},
[{$set: {
"courses": {
$map: {
input: "$courses",
in: {
$mergeObjects: [
{createdAt: {
$convert: {
input: "$$this._id",
to: "date",
onError: {"error": "$$this._id"}
}
}},
"$$this"
]
}
}
}
}
}
])

How to return a specific element of an array in a document?

I have a table containing documents set up as follows:
_id: 1,
name: { first: 'John', last: 'Doe' },
tools: [ 'Tool1', 'Tool2', 'Tool3' ],
skills: [
{ type: 'carpentry',
years: 3 },
{ type: 'plumbing',
year: 5 },
{ type: 'electrical',
year: 8 }
]
}
I need to write a script that can search each document in the table and return the value of a specific skill, for example: Find the number of years John Doe has in plumbing.
Since I don't need the full document, db.table.find({skills: {$elemMatch: {type:'plumbing'}}}) feels unnecessary and would still require me to search the document to find the value I'm looking for. Is there a way to just return the part of the document I'm looking for?
The desired output would be {type: 'plumbing', year: 5} so that I could then manipulate that data into another field in the document.
Try this-
db.collection.aggregate([
{
"$unwind": "$skills"
},
{
"$match": {
"skills.type": "plumbing"
}
},
{
"$project": {
skills: 1
}
}
])
Mongo Playground
OR try this if you only want year.
Mongo Playground 2

MongoDB get full doc after match, group, and sort

Order:
{
order_id: 1,
order_time: ISODate(...),
customer_id: 456,
products: [
{
product_id: 1,
product_name: "Pencil"
},
{
product_id: 2,
product_name: "Scissors"
},
{
product_id: 3,
product_name: "Tape"
}
]
}
I have a collection with a whole bunch of documents like the above. I would like to query for the latest order for each customer who ordered Scissors.
That is, where there exists a "products.product_name" which equals "Scissors", group by customer_id, give me the full document where the "order_time" is the "max" for that group.
To find the documents, I could do like find({ 'products.product_name' : "Scissors" }) but then I get all of the order with Scissors, I only want the most recent.
So, I am looking at aggregation... Mongo's "$group" aggregation stage seems to require that you do some kind of actual aggregation inside like sum or max or whatever. I am guessing there's some combination of $match, $group, and $sort to use here but I can't seem to quite get it working.
Something close:
db.storcap.aggregate(
[
{
$match: { 'products.product_name' : "Scissors" }
},
{
$sort: { created_at:-1 }
},
{
$group: {
_id: "$customer_id",
}
}]
)
But this doesn't return the full doc and I am not sure that it's doing the sorting and grouping right.
You can use $first operator to get most recent order (are ordered desc) and special variable $$ROOT to get whole object in a final result:
db.storcap.aggregate([
{
$match: { 'products.product_name' : "Scissors" }
},
{
$sort: { created_at:-1 }
},
{
$group: {
_id: "$customer_id",
lastOrder: { $first: "$$ROOT" }
}
}
])

mongodb - Find every document with the same field but different case

I'm having trouble with my database because I have documents representing my users with the field email with different cases (due to the ability to create ghost user, waiting for them to register). When the user registers, I use the lowered version of his email and overwrite the previous entry. The problem is that 'ghost' email has not been lowered.
If Foo#bar.com ghost is created, Foo#bar.com register, he will be known as 'foo#bar.com', so Foo#bar.com will just pollute my database.
I looking for a way in order to find the duplicates entries, remove the irrelevant one (by hand) before I push my fix about case. Ideas?
Thank you!
Try this:
db.users.aggregate([
{ $match: {
"username": { $exists: true }
}},
{ $project: {
"username": { "$toLower": [ "$username" ]}
}},
{ $group: {
_id: "$username",
total: { $sum : 1 }
}},
{ $match: {
total: { $gte: 2 }
}},
{ $sort: {
total: -1
}}
]);
This will find every user with a username, make the user names lower case, then group them by username and display the usernames that have a count greater than 1.
You can use projection and toLower function to achieve what you are looking for. Assuming that your attribute name is "email" in your collection document, here is an example of how to achieve this:
db.yourcollection.aggregate([
{ $project: {
"email": { "$toLower" : [ "$email" ] }
}},
{ $match: {
"email": /foo#bar.com/
}}
]);