MongoDB, is there a statement similar to if then elif in sql? - mongodb

Query is to find people whose age is shown and if their grade is 5 or higher then they will get honours, othewise fail. Can I use $cond with find?
db.test.find({age:{$exists:true}},{$cond: {if: "grade":5, then:"Honours" : true, else: "Honours" : "fail"}})
I also tried
db.test.find({$and: [{age:{$exists:true}}, {grade: {$gte:"5"}}]}, {$set:{"Honours":"true"}}, {multi:true})
But neither come close. Any help/direction is very much appreciated.

Check this.
db.test.aggregate([
{
$match: {
"age": {
"$exists": true
}
}
},
{
$project: {
"Honours": {
$switch: {
branches: [
{
case: {
"$gte": [
"$grade",
5
]
},
then: "true"
}
],
default: "fail"
}
}
}
}
])

.find projection only allows include / exclude fields, neither transform them nor add new field. Use MongoDB aggregation
db.test.aggregate([
{
$match: {
age: {
$exists: true
}
}
},
{
$project: {
Honours: {
$cond: [
{
$gte: [
"$grade",
5
]
},
true,
"fail"
]
}
}
}
])
MongoPlayground

Related

MongoDB: Add date comparison to arrayFilters

Objects of my collection have a field, that is an array of objects with one of the field being a string date
{
citizens: [{
name: 'John'
birthday: '1993/07/13'
},
{
name: 'Sarah'
birthday: '1996/07/13'
},
{
name: 'Natalia',
birthday: '2015/07/13'
}]
}
{
citizens: [{
name: 'Leo'
birthday: '1994/02/08'
},
{
name: 'Paul'
birthday: '1934/09/13'
},
{
name: 'Rego',
birthday: '2019/01/29'
}]
}
I want to set to all the users older than 18 status 'adult'
Here is what I try to do:
users.updateMany({}, {
$set: { 'citizens.$[elem].status': 'adult' },
},
{
arrayFilters: [
{ 'elem.status': { $exists: false } },
{ $lt: [{ $toDate: 'elem.$birthday' }, 18yearsaAgoDate] }, <-- 18years don't mean much here, I actually use $$NOW
],
multi: true,
});
But I get 'unknown top level operator: $lt' error when run this. How do I supposed to use $lt in arrayFilter?
Thanks in advance!
Here's how you could do it in a simple update using the aggregation pipelined updates:
db.collection.updateMany({},
[
{
$set: {
citizens: {
$map: {
input: "$citizens",
in: {
$mergeObjects: [
{
status: {
$cond: [
{
$gt: [
{
$dateDiff: {
startDate: {
$toDate: "$$this.birthday"
},
endDate: "$$NOW",
unit: "year"
}
},
18
]
},
"adult",
"$$REMOVE"
]
}
},
"$$this"
]
}
}
}
}
}
])
Mongo Playground
I've used some version 5+ operators like $dateDiff as it makes the code cleaner, but you could still achieve the same results without them using $subtract and a constant for 18 years, like so:
{
$lt: [
{
$toDate: "$$this.birthday"
},
{
$subtract: [
"$$NOW",
567648000000// 18 years in miliseconds
]
}
]
}
Mongo Playground
This is an update using the arrayFilters syntax.
db.collection.updateMany(
{ },
{
$set: { "citizens.$[elem].status": "adult" }
},
{
arrayFilters: [ { "elem.status": { $exists: false } , "elem.birthday": { $lt: "2004/07/27" } } ]
}
)
Note the date value "2004/07/27" is the day 18 years ago (very close approximate value). And using string values in date comparison requires that the value is formatted in "YYYY/mm/dd".
It would have worked like this if your date was already in the right format. Since you need to format it, I think you should use an aggregation pipeline with a $merge stage:
db.collection.aggregate([
{$set: {
citizens: {
$map: {
input: "$citizens",
in: {$mergeObjects: [
{status: {
$cond: [
{$lt: [{$toDate: "$$this.birthday"}, 18yearsaAgoDate]},
"adult",
"$$REMOVE"
]
}
},
"$$this"
]
}
}
}
}
},
{ $merge : { into : "collection" } }
])
See how it works on the playground example

Sort Mongodb documents by seeing if the _id is in another array

I have two collections - "users" and "follows". "Follows" simply contains documents with a "follower" field and a "followee" field that represent when a user follows another user. What I want to do is to be able to query the users but display the users that I (or whatever user is making the request) follow first. For example if I follow users "5" and "14", when I search the list of users, I want users "5" and "14" to be at the top of the list, followed by the rest of the users in the database.
If I were to first query all the users that I follow from the "Follows" collection and get an array of those userIDs, is there a way that I can sort by using something like {$in: [userIDs]}? I don't want to filter out the users that I do not follow, I simply want to sort the list by showing the users that I do follow first.
I am using nodejs and mongoose for this.
Any help would be greatly appreciated. Thank you!
Answer
db.users.aggregate([
{
$addFields: {
sortBy: {
$cond: {
if: {
$in: [ "$_id", [ 5, 14 ] ]
},
then: 0,
else: 1
}
}
}
},
{
$sort: {
sortBy: 1
}
},
{
$unset: "sortBy"
}
])
Test Here
If you don't want you on the list, then
db.users.aggregate([
{
$addFields: {
sortBy: {
$cond: {
if: {
$in: [ "$_id", [ 5, 14 ] ]
},
then: 0,
else: 1
}
}
}
},
{
$sort: {
sortBy: 1
}
},
{
$unset: "sortBy"
},
{
$match: {
"_id": { $ne: 1 }
}
}
])
Test Here
If you want to sort users first
db.users.aggregate([
{
$sort: {
_id: 1
}
},
{
$addFields: {
sortBy: {
$cond: {
if: {
$in: [
"$_id",
[
5,
14
]
]
},
then: 0,
else: 1
}
}
}
},
{
$sort: {
sortBy: 1,
}
},
{
$unset: "sortBy"
},
{
$match: {
"_id": {
$ne: 1
}
}
}
])
Test Here

How to reference added field in $match?

Given this aggregation pipeline:
[
{
$addFields: {
_myVar: "x"
}
},
{
$match: {
array: "x"
}
}
]
How can the field with value x only be set once?
For example, this does not work, it times out:
[
{
$addFields: {
_myVar: "x"
}
},
{
$match: {
$expr: {
$in: [
"$_myVar", "$array"
]
}
}
}
]
The variable needs to be available throughout the pipeline, so only using the value in the $match stage as condition is not a solution.
What is the solution?
You can do something like this here i added two fields and checking if _myArray has _myVar, this is just to explain how can you check... in your case you have to replace _myArray with your actual array against which you want t to match
[{
$addFields: {
_myVar: "x",
_myArray: ['X', 'Y', 'x']
}
}, {
$addFields: {
has: {
$in: ["$_myVar", "$_myArray"]
}
}
}, {
$match: {
has: true
}
}]

Projecting $facet output to an object

I created an aggregation ending with a facet operation, producing the following result:
[
{
"all": [
{
"all": 6948
}
],
"none": [
{
"none": 207
}
]
}
]
What I need is an output like below:
{
all: 6948,
none: 207
}
I tried some $projections but couldn't get rid of the arrays outside and inside of the result. Is this possible with $project or should I try something else?
try this code
db.collection.aggregate([
{
$unwind: "$all"
},
{
$unwind: "$none"
},
{
$project: {
_id: 0,
all: "$all.all",
none: "$none.none"
}
}
])
check in mongo playground
Use project stage after facet as
{$project:{
all:{$arrayElemAt:["$all.all",0]},
none:{$arrayElemAt:["$none.none",0]},
}}
Try this:
{
$project: {
all: {
$arrayElemAt: [
"$all",
0
]
},
none: {
$arrayElemAt: [
"$none",
0
]
}
}
},
{
$project: {
all: "$all.all",
none: "$none.none"
}
}

MongoDB/Mongoose how to return a nested subdocument by _id

MongoDB newbie here.
I have a 'client' document that looks like this:
{
name: "myClient",
products: [{
name: "myProduct1",
environments: [{
name: "myEnvironment1",
changeLogs: [
{ "some": "fields21" },
{ "some": "fields22" }
]
},
{
name: "myEnvironment2",
changeLogs: [
{ "some": "fields11" },
{ "some": "fields12" }
]
}
]
},
{
name: "myProduct2",
environments: [{
name: "myEnv1",
changeLogs: [
{ "some": "fields1" },
{ "some": "fields2" }
]
},
{
name: "myEnv1",
changeLogs: [
{ "some": "fields3" },
{ "some": "fields4" }
]
}
]
}]
}
So a client has many products, which has many environments, which has many changeLogs. I am looking to return a list of changeLogs for a given environment, with only the environment._id to go on.
I can find the correct client document using just this _id:
db.clients.find({'products.environments._id': ObjectId("5a1bf4b410842200043d56ff")})
But this returns the entire client document. What I want is to return just the changeLogs array from the environment with _id: ObjectId("5a1bf4b410842200043d56ff")
Assuming I have the _id of the first environment of the first product, my desired output is the following:
[
{ "some": "fields21" },
{ "some": "fields22" }
]
What query would you recommend I use to achieve this?
Many thanks in advance for any help. The docs thus far have only been confusing, but I'm sure I'll get there in the end!
The idea here is to $unwind the products array so that its environments can be fed as input to $filter after a $match on the _id.
(lets assume the enviroment _id is 1)
db.collection.aggregate([
{
$unwind: "$products"
},
{
$match: {
"products.environments._id": 1
}
},
{
$project: {
"logsArray": {
$filter: {
input: "$products.environments",
as: "env",
cond: {
$eq: [
"$$env._id",
1
]
}
}
}
}
},
{
$unwind: "$logsArray"
}
])
O/P Should be like:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"logsArray": {
"changeLogs": [
{
"some": "fields21"
},
{
"some": "fields22"
}
],
"id": 1,
"name": "myEnvironment1"
}
}
]
Note: notice the last stage $unwind of logsArray which I think is just pretty-fying the ouput. Otherwise without it also the resultant is acceptable (if you agree, can remove that).
This is just another way of doing the aggregation query. This gets the desired result.
Note I am using the "name" field of the "environments" from the sample document you had provided. The "name" can be substituted with "id" as needed.
var ENV = "myEnvironment1";
db.env.aggregate( [
{ $match: {
{ $unwind: "$products" },
{ $unwind: "$products.environments" },
{ $match: { "products.environments.name": ENV} },
{ $project: { _id: 0, changeLogs: "$products.environments.changeLogs" } },
] )
The result:
{ "changeLogs" : [ { "some" : "fields21" }, { "some" : "fields22" } ] }
If the variable ENV's value is changed, then the result will be accordingly; e.g.,: ENV = "myEnv1";
{ "changeLogs" : [ { "some" : "fields1" }, { "some" : "fields2" } ] }
{ "changeLogs" : [ { "some" : "fields3" }, { "some" : "fields4" } ] }
db.clients.aggregate([
{
$unwind: "$products"
},
{
$unwind: "$products.environments"
},
{
$match: { "products.environments._id": ObjectId("5a1bf4b410842200043fffff") }
},
{
$project: { _id: 0, changeLogs: "$products.environments.changeLogs" }
}
]).pretty()
Results in:
{
"changeLogs": [
{ "some": "fields21" },
{ "some": "fields22" }
]
}
For those finding that code confusing I found it very useful to just add one aggregate method at a time, look at the results, and then add then next method to the pipeline. Once the pipeline was complete I also experimented with removing intermediary steps to see if I could get the same results with less piping.