Is there an elseif thing in MongoDB to $cond while aggregating - mongodb

So I need a custom field calculated in MongoDB as follows
if( field1 =="A") ->customfield=10
else if(field1 =="B" )->customfield=20
else (field1 =="C" ) ->customfield=15
I'm using aggregation along with the $project statement. But the $cond operator doesn't allow elseif (subbranching the else) and merely allows two static branches if and else. Using a nested elseif causes
"exception: field inclusion is not allowed inside of $expressions"
Heres my query(which gives me the error)
db.items.aggregate([ { $project :
{
name: 1,
customfield:
{
$cond: { if: { $eq: [ "$field1", "4" ] }, then: 30,
else: {
if:
{ $eq: ["$field1","8"]},
then: 25, else: 10}}
}
}},{ $sort: { customfield: 1 }},{$limit:12}]);
Is there a method or workaround to this. My apologies if this is a repeated question but I wasn't able to find a similar one.

With modern releases ( since MongoDB 3.4 ) you would use $switch, which is basically the counterpart to switch or case keywords in other language implementations:
db.items.aggregate([
{ "$project": {
"name": 1,
"customfield": {
"$switch": {
"branches": [
{ "case": { "$eq": [ "$field1", "4" ] }, "then": 30 },
{ "case": { "$eq": [ "$field1", "8" ] }, "then": 25 }
],
"default": 10
}
}
}},
{ "$sort": { customfield: 1 }},
{ "$limit":12 }
])
This avoids nesting the if..then..else conditions as can be done using $cond and shown below. But the below still shows as an example that this could always be done, even before the new operator of even the explicit if..then..else keywords since the original array notation always maintained that syntax.
Noting also that an array of conditions here is typically also a lot easier to construct programatically than creating a nested data structure for the statement as was needed with $cond.
The if..then..else keywords to the $cond operator are only a recent addition as of recent versions of MongoDB at the time of writing ( MongoDB 2.6 was the introduction of the keywords. The actual operator was available with release of the aggregation framework in MongoDB 2.2 ). The intention was for clarity but in this case it seems to has caused some confusion.
As an if..then.else operator $cond is indeed a ternary operator, just as would be implemented in many programming languages. This means as an "inline" conditional, rather than creating "blocks" of logic to the conditions, anything that does not meet the first condition belongs under else.
Therefore you "nest" the statements rather than follow blocks:
db.items.aggregate([
{ "$project": {
"name": 1,
"customfield": {
"$cond": {
"if": { "$eq": [ "$field1", "4" ] },
"then": 30,
"else": {
"$cond": {
"if": { "$eq": ["$field1","8"]},
"then": 25,
"else": 10
}
}
}
}
}},
{ "$sort": { customfield: 1 }},
{ "$limit":12 }
]);
Or even with the original array notation, which some might prefer if building the statement programatically:
db.items.aggregate([
{ "$project": {
"name": 1,
"customfield": {
"$cond": [
{ "$eq": [ "$field1", "4" ] },
30,
{ "$cond": [
{ "$eq": ["$field1","8"] },
25,
10
]}
]
}
}},
{ "$sort": { customfield: 1 }},
{ "$limit":12 }
]);
Ternary means three conditions, no more no less. So all if..then..else logic must be nested.

MongoDB 3.4 has a new thing called $switch for this exact thing!
$switch: {
branches: [
{ case: <expression>, then: <expression> },
{ case: <expression>, then: <expression> },
...
],
default: <expression>
}
https://docs.mongodb.com/manual/reference/operator/aggregation/switch/

Here is way i prefer.
Syntax
$cond: [ { <condition> }, <true>, <false> ]
Example
condition1: {
$cond: [ { $eq: ["one", 'one'] }, true, false ]
},
condition2: {
$cond: [ { $eq: ["one", 'two'] }, true, false ]
},
condition3: {
$cond: [ { $eq: ["three", 'three'] }, {three:'is equal to three'}, {three:'is not equal to three'} ]
}
output
{
condition1: true,
condition2: false,
condition3: { three: 'is equal to three' },
}

if you are using only object then
{
$cond: {
$and: [
{ if: { $eq: ["$$field1", 'A'] }, then: '10', else: 15 },
{ if: { $eq: ["$$field1", 'B'] }, then: '20', else: 15 },
{ if: { $eq: ["$$field1", 'C'] }, then: '10', else: 15 },
]
}
}
And if you are Using Array find Data
{
$map: {
input: "$fields", as: "field1", in: {
$cond: {
$and: [
{ if: { $eq: ["$$field1", 'A'] }, then: '10', else: 15 },
{ if: { $eq: ["$$field1", 'B'] }, then: '20', else: 15 },
{ if: { $eq: ["$$field1", 'C'] }, then: '10', else: 15 },
]
}
}
}
}

Related

$switch inside a $match MONGODB

Hi i am trying to use MONGODB query inside TIBCO jasperstudio to create a report
What I am trying to do is filter the data using two parameters #orderitemuid and #ordercatuid. My case is if I put a parameter using #orderitemuid, it will disregard the parameter for #ordercatuid. Vise versa, if I put a parameter using #ordercatuid, it will disregard the parameter for #orderitemuid. But there is also an option when using bot parameters in the query. I used a $switch inside the $match but I am getting an error. Below is the $match I am using
{
$match: {
$switch: {
branches: [
{
case: { $eq: [{ $IfNull: [$P{orderitemuid}, 0] }, 0] },
then: { 'ordcat._id': { '$eq': { '$oid': $P{ordercatuid} } } },
},
{
case: { $eq: [{ $IfNull: [$P{ordercatuid}, 0] }, 0] },
then: { '_id': { '$eq': { '$oid': $P{orderitemuid} } } },
},
],
default: {
$expr: {
$and: [
{ $eq: ['_id', { '$oid': $P{orderitemuid} }] },
{ $eq: ['ordcat_id', { '$oid': $P{ordercatuid} }] },
],
},
},
},
},
}
Thank you in advance
As mentioned in the $match docs
$match takes a document that specifies the query conditions. The query syntax is identical to the read operation query syntax; i.e. $match does not accept raw aggregation expressions. ...
And $switch is an aggregation expressions. this means it cannot be used in a $match stage without being wrapped with $expr.
You can however wrap it with $expr, this will also require you to restructure the return values a little bit, like so:
db.collection.aggregate([
{
$match: {
$expr: {
$switch: {
branches: [
{
case: {
$eq: [
{
$ifNull: [
$P{orderitemuid},
0
]
},
0
]
},
then: {
$eq: [
"$ordcat._id",
{"$oid":$P{ordercatuid}}
]
}
},
{
case: {
$eq: [
{
"$ifNull": [
$P{ordercatuid},
0
]
},
0
]
},
then: {
$eq: [
"$_id",
{"$oid":$P{orderitemuid}}
]
}
}
],
default: {
$and: [
{
$eq: [
"$_id",
{"$oid": $P{orderitemuid} }
]
},
{
$eq: [
"$ordcat_id",
{"$oid": $P{ordercatuid}}
]
}
]
}
}
}
}
}
])
Mongo Playground

MongoDB 4.2 combine aggregation pipeline update with array filters

I saw that MongoDB 4.2 introduces aggregation pipeline updates, which allows you to set document fields based on other fields in the document.
Considering the following document
{
ean: "12345",
orderedQty: 2,
fulfilledQty: 1,
"status": "pending"
}
I could use the following command to increment the fulfilledQty by 1 and if the orderedQty matches the fulfilledQty set the status accordingly:
db.collection.findOneAndUpdate({}, [
{
"$set": {
"orderedQty": {
"$add": [ "$fulfilledQty", 1 ]
}
},
"$set": {
"status": {
"$cond": {
"if": { "$eq": ["$orderedQty", "$fulfilledQty"] },
"then": "fulfilled",
"else": "pending"
}
}
}
}
])
My question: How would i perform this on an array. Say I have a document like this:
_id: "test",
items: [
{ean: "12345", orderedQty: 2, fulfilledQty: 1, "status": "pending"},
{ean: "67891", orderedQty: 1, fulfilledQty: 1, "status": "fulfilled"}
]
Given I have the params ean = 12345 and an increase value by 1. How could I target the specific array item with EAN 12345, increase the fulfilledQty by 1 and set the status? I want to only chance the status field and fulfilledQty field and leave the rest of the items array as is. So expected outcome would be:
_id: "test",
items: [
{ean: "12345", orderedQty: 2, fulfilledQty: 2, "status": "fulfilled"},
{ean: "67891", orderedQty: 1, fulfilledQty: 1, "status": "fulfilled"}
]
I found the following workflow (works only for mongodb 4.2+), but it's amazingly verboseā€¦
Given that there are two variables, an item identifier (called ean) and a quantity that was shipped (called fulfilledQty)
collection.update({}, [
{
$set: {
items: {
$map: {
input: "$items",
as: "item",
in: {
$mergeObjects: [
"$$item",
{
fulfilledQty: {
$switch: {
branches: [
{
case: {
$eq: ["$$item.ean", ean]
},
then: {
$toInt: {
$add: ["$$item.fulfilledQty", fulfilledQty]
}
}
}
],
default: "$$item.fulfilledQty"
}
}
}
]
}
}
}
}
},
{
$set: {
items: {
$map: {
input: "$items",
as: "item",
in: {
$mergeObjects: [
"$$item",
{
status: {
$cond: {
if: {
$eq: ["$$item.orderedQty", "$$item.fulfilledQty"]
},
then: "fulfilled",
else: "$$item.status"
}
}
}
]
}
}
}
}
}
]);
I used a switch statement since in my use case I have multiple different EANs. Downside is that I had to use a $map operation, so it always iterates over the whole items array.

Return first element if no match found in array

I have the following document:
{
_id: 123,
state: "AZ",
products: [
{
product_id: 1,
desc: "P1"
},
{
product_id: 2,
desc: "P2"
}
]
}
I need to write a query to return a single element from the products array where state is "AZ" and product_id is 2. If the matching product_id is not found, then return the first (or any) element from the products array.
For example: If product_id is 2 (match found), then the result should be:
products: [
{
product_id: 2,
desc: "P2"
}
]
If the product_id is 3 (not found), then the result should be:
products: [
{
product_id: 1,
desc: "P1"
}
]
I was able to meet one condition when the match is found but not sure how to satisfy the second condition in the same query:
db.getCollection('test').find({"state": "AZ"}, {_id: 0, state: 0, products: { "$elemMatch": {"product_id": "2"}}})
I tried using the aggregation pipeline as well but could not find a working solution.
Note: This is different from the following question as I need to return a default element if the match is not found:
Retrieve only the queried element in an object array in MongoDB collection
You can try below aggregation
Basically you need to $filter the products array and check for the $condition if it doesn't contain any element or equal to [] then you have to $slice with the first element of the products array.
db.collection.aggregate([
{ "$addFields": {
"products": {
"$cond": [
{
"$eq": [
{ "$filter": {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}},
[]
]
},
{ "$slice": ["$products", 1] },
{ "$filter": {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}}
]
}
}}
])
or even using $let aggregation
db.collection.aggregate([
{ "$addFields": {
"products": {
"$let": {
"vars": {
"filt": {
"$filter": {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}
}
},
"in": {
"$cond": [
{ "$eq": ["$$filt", []] },
{ "$slice": ["$products", 1] },
"$$filt"
]
}
}
}
}}
])
If you don't care which element you get back then this is the way to go (you'll get the last element in the array in case of no match since $indexOfArray will return -1):
db.getCollection('test').aggregate([{
$addFields: {
"products": {
$arrayElemAt: [ "$products", { $indexOfArray: [ "$products.product_id", 2 ] } ]
},
}
}])
If you want the first then do this instead ($max will take care of transforming -1 into index 0 which is the first element):
db.getCollection('test').aggregate([{
$addFields: {
"products": {
$arrayElemAt: [ "$products", { $max: [ 0, { $indexOfArray: [ "$products.product_id", 2 ] } ] } ]
},
}
}])
Here is a version that should work on v3.2 as well:
db.getCollection('test').aggregate([{
"$project": {
"products": {
$slice: [{
$concatArrays: [{
$filter: {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}},
"$products" // simply append the "products" array
// alternatively, you could append only the first or a specific item like this [ { $arrayElemAt: [ "$products", 0 ] } ]
]
},
1 ] // take first element only
}
}
}])

Return Sub-document only when matched but keep empty arrays

I have a collection set with documents like :
{
"_id": ObjectId("57065ee93f0762541749574e"),
"name": "myName",
"results" : [
{
"_id" : ObjectId("570e3e43628ba58c1735009b"),
"color" : "GREEN",
"week" : 17,
"year" : 2016
},
{
"_id" : ObjectId("570e3e43628ba58c1735009d"),
"color" : "RED",
"week" : 19,
"year" : 2016
}
]
}
I am trying to build a query witch alow me to return all documents of my collection but only select the field 'results' with subdocuments if week > X and year > Y.
I can select the documents where week > X and year > Y with the aggregate function and a $match but I miss documents with no match.
So far, here is my function :
query = ModelUser.aggregate(
{$unwind:{path:'$results', preserveNullAndEmptyArrays:true}},
{$match:{
$or: [
{$and:[
{'results.week':{$gte:parseInt(week)}},
{'results.year':{$eq:parseInt(year)}}
]},
{'results.year':{$gt:parseInt(year)}},
{'results.week':{$exists: false}}
{$group:{
_id: {
_id:'$_id',
name: '$name'
},
results: {$push:{
_id:'$results._id',
color: '$results.color',
numSemaine: '$results.numSemaine',
year: '$results.year'
}}
}},
{$project: {
_id: '$_id._id',
name: '$_id.name',
results: '$results'
);
The only thing I miss is : I have to get all 'name' even if there is no result to display.
Any idea how to do this without 2 queries ?
It looks like you actually have MongoDB 3.2, so use $filter on the array. This will just return an "empty" array [] where the conditions supplied did not match anything:
db.collection.aggregate([
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$filter": {
"input": "$results",
"as": "result",
"cond": {
"$and": [
{ "$eq": [ "$$result.year", year ] },
{ "$or": [
{ "$gt": [ "$$result.week", week ] },
{ "$not": { "$ifNull": [ "$$result.week", false ] } }
]}
]
}
}
}
}}
])
Where the $ifNull test in place of $exists as a logical form can actually "compact" the condition since it returns an alternate value where the property is not present, to:
db.collection.aggregate([
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$filter": {
"input": "$results",
"as": "result",
"cond": {
"$and": [
{ "$eq": [ "$$result.year", year ] },
{ "$gt": [
{ "$ifNull": [ "$$result.week", week+1 ] },
week
]}
]
}
}
}
}}
])
In MongoDB 2.6 releases, you can probably get away with using $redact and $$DESCEND, but of course need to fake the match in the top level document. This has similar usage of the $ifNull operator:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$and": [
{ "$eq": [{ "$ifNull": [ "$year", year ] }, year ] },
{ "$gt": [
{ "$ifNull": [ "$week", week+1 ] }
week
]}
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}}
])
If you actually have MongoDB 2.4, then you are probably better off filtering the array content in client code instead. Every language has methods for filtering array content, but as a JavaScript example reproducible in the shell:
db.collection.find().forEach(function(doc) {
doc.results = doc.results.filter(function(result) {
return (
result.year == year &&
( result.hasOwnProperty('week') ? result.week > week : true )
)
]);
printjson(doc);
})
The reason being is that prior to MongoDB 2.6 you need to use $unwind and $group, and various stages in-between. This is a "very costly" operation on the server, considering that all you want to do is remove items from the arrays of documents and not actually "aggregate" from items within the array.
MongoDB releases have gone to great lengths to provide array processing that does not use $unwind, since it's usage for that purpose alone is not a performant option. It should only ever be used in the case where you are removing a "significant" amount of data from arrays as a result.
The whole point is that otherwise the "cost" of the aggregation operation is likely greater than the "cost" of transferring the data over the network to be filtered on the client instead. Use with caution:
db.collection.aggregate([
// Create an array if one does not exist or is already empty
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$cond": [
{ "$ifNull": [ "$results.0", false ] },
"$results",
[false]
]
}
}},
// Unwind the array
{ "$unwind": "$results" },
// Conditionally $push based on match expression and conditionally count
{ "$group": {
"_id": "_id",
"name": { "$first": "$name" },
"user": { "$first": "$user" },
"results": {
"$push": {
"$cond": [
{ "$or": [
{ "$not": "$results" },
{ "$and": [
{ "$eq": [ "$results.year", year ] },
{ "$gt": [
{ "$ifNull": [ "$results.week", week+1 ] },
week
]}
]}
] },
"$results",
false
]
}
},
"count": {
"$sum": {
"$cond": [
{ "$and": [
{ "$eq": [ "$results.year", year ] },
{ "$gt": [
{ "$ifNull": [ "$results.week", week+1 ] },
week
]}
] }
1,
0
]
}
}
}},
// $unwind again
{ "$unwind": "$results" }
// Filter out false items unless count is 0
{ "$match": {
"$or": [
"$results",
{ "count": 0 }
]
}},
// Group again
{ "$group": {
"_id": "_id",
"name": { "$first": "$name" },
"user": { "$first": "$user" },
"results": { "$push": "$results" }
}},
// Now swap [false] for []
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$cond": [
{ "$ne": [ "$results", [false] ] },
"$results",
[]
]
}
}}
])
Now that is a lot of operations and shuffling just to "filter" content from an array compared to all of the other approaches which are really quite simple. And aside from the complexity, it really does "cost" a lot more to execute on the server.
So if your server version actually supports the newer operators that can do this optimally, then it's okay to do so. But if you are stuck with that last process, then you probably should not be doing it and instead do your array filtering in the client.

Mongo Query to Return only a subset of SubDocuments

Using the example from the Mongo docs:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] }
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] }
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
db.survey.find(
{ id: 12345, results: { $elemMatch: { product: "xyz", score: { $gte: 6 } } } }
)
How do I return survey 12345 (regardless of even if it HAS surveys or not) but only return surveys with a score greater than 6? In other words I don't want the document disqualified from the results based on the subdocument, I want the document but only a subset of subdocuments.
What you are asking for is not so much a "query" but is basically just a filtering of content from the array in each document.
You do this with .aggregate() and $project:
db.survey.aggregate([
{ "$project": {
"results": {
"$setDifference": [
{ "$map": {
"input": "$results",
"as": "el",
"in": {
"$cond": [
{ "$and": [
{ "$eq": [ "$$el.product", "xyz" ] },
{ "$gte": [ "$$el.score", 6 ] }
]}
]
}
}},
[false]
]
}
}}
])
So rather than "contrain" results to documents that have an array member matching the condition, all this is doing is "filtering" the array members out that do not match the condition, but returns the document with an empty array if need be.
The fastest present way to do this is with $map to inspect all elements and $setDifference to filter out any values of false returned from that inspection. The possible downside is a "set" must contain unique elements, so this is fine as long as the elements themselves are unique.
Future releases will have a $filter method, which is similar to $map in structure, but directly removes non-matching results where as $map just returns them ( via the $cond and either the matching element or false ) and is then better suited.
Otherwise if not unique or the MongoDB server version is less than 2.6, you are doing this using $unwind, in a non performant way:
db.survey.aggregate([
{ "$unwind": "$results" },
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": {
"$sum": {
"$cond": [
{ "$and": [
{ "$eq": [ "$results.product", "xyz" ] },
{ "$gte": [ "$results.score", 6 ] }
]},
1,
0
]
}
}
}},
{ "$unwind": "$results" },
{ "$match": {
"$or": [
{
"results.product": "xyz",
"results.score": { "$gte": 6 }
},
{ "matched": 0 }
}},
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": { "$first": "$matched" }
}},
{ "$project": {
"results": {
"$cond": [
{ "$ne": [ "$matched", 0 ] },
"$results",
[]
]
}
}}
])
Which is pretty horrible in both design and perfomance. As such you are probably better off doing the filtering per document in client code instead.
You can use $filter in mongoDB 3.2
db.survey.aggregate([{
$match: {
{ id: 12345}
}
}, {
$project: {
results: {
$filter: {
input: "$results",
as: "results",
cond:{$gt: ['$$results.score', 6]}
}
}
}
}]);
It will return all the sub document that have score greater than 6. If you want to return only first matched document than you can use '$' operator.
You can use $redact in this way:
db.survey.aggregate( [
{ $match : { _id : 12345 }},
{ $redact: {
$cond: {
if: {
$or: [
{ $eq: [ "$_id", 12345 ] },
{ $and: [
{ $eq: [ "$product", "xyz" ] },
{ $gte: [ "$score", 6 ] }
]}
]
},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
] );
It will $match by _id: 12345 first and then it will "$$PRUNE" all the subdocuments that don't have "product":"xyz" and don't have score greater or equal 6. I added the condition ($cond) { $eq: [ "$_id", 12345 ] } so that it wouldn't prune the whole document before it reaches the subdocuments.