MongoDb $addFields inside arrays that $multiply two values within the array - mongodb

MongoDb version 3.4.4
How to aggregate a new key 'total', whose value is the product of 'course' and 'quantity' for each object inside array snapshot.
Sample document:
{
cur: "EUR",
snapshot: [
{
id: "24352345",
course: 58.12,
quantity: 13
},
{
id: "34552345",
course: 18.12,
quantity: 63
}
]
}
Desired result:
{
cur: "EUR",
snapshot: [
{
id: "24352345",
course: 58.12,
quantity: 13,
total: 755.56
},
{
id: "34552345",
course: 18.12,
quantity: 63,
total: 1141.56
}
]
}
first attempt:
db.mycoll.aggregate([{
$addFields: {
"snapshot.total": {
$multiply:["$snapshot.quantity", "$snapshot.course"]
}
}
}])
"errmsg" : "$multiply only supports numeric types, not array"
Second attempt:
db.mycoll.aggregate( [
{ "$addFields": {
"snapshot.total": {
"$map": {
"input": "$snapshot",
"as": "row",
"in": { "$multiply": [
{ "$ifNull": [ "$$row.quantity", 0 ] },
{ "$ifNull": [ "$$row.course", 0 ] }
]}
}
}
}}
])
The undesired value of 'total' is an array with the totals of all the objects:
{
cur: "EUR",
snapshot: [
{
id: "24352345",
course: 58.12,
quantity: 13,
total: [
755.56,
1141.56
]
},
{
id: "34552345",
course: 18.12,
quantity: 63,
total: [
755.56,
1141.56
]
}
]
}

Modify your second attempt using the $map operator to map the whole snapshot embedded document with its fields as
db.mycoll.aggregate([
{
"$addFields": {
"snapshot": {
"$map": {
"input": "$snapshot",
"as": "row",
"in": {
"id": "$$row.id",
"course": "$$row.course",
"quantity": "$$row.quantity",
"total": { "$multiply": [
{ "$ifNull": [ "$$row.quantity", 0 ] },
{ "$ifNull": [ "$$row.course", 0 ] }
]}
}
}
}
}
}
])

Related

MongoDB - Lookup match with condition array of object with string

I have two collections "datasets" and "users".
I tried to lookup datasets.assignedTo = users.id that's working fine. Also, I want to match the field of datasets.firstBillable >= users.prices.beginDate date field are matched to get the current index price value. And also check users.prices.endDate is less than or equal to users.prices.beginDate.
For example:
cgPrices: 45
https://mongoplayground.net/p/YQps9EozlAL
Collections:
db={
users: [
{
id: 1,
name: "Aravinth",
prices: [
{
beginDate: "2022-08-24T07:29:01.639Z",
endDate: "2022-08-31T07:29:01.639Z",
price: 45
}
]
},
{
id: 2,
name: "Raja",
prices: [
{
beginDate: "2022-07-25T07:29:01.639Z",
endDate: "2022-07-30T07:29:01.639Z",
price: 55
}
]
}
],
datasets: [
{
color: "braun, rose gold",
firstBillable: "2022-08-24T07:29:01.639Z",
assignedTo: 1
},
{
color: "beige, silber",
firstBillable: "2022-07-25T07:29:01.639Z",
assignedTo: 2
}
]
}
My current implementation:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: "$firstBillable"
},
pipeline: [
{
"$match": {
$expr: {
"$and": [
{
"$eq": [
"$id",
"$$assigned_to"
]
},
{
"$gte": [
"$first_billable",
"$details.prices.beginDate"
]
},
{
"$lte": [
"$first_billable",
"$details.prices.endDate"
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: {
"$first": "$details.prices.price"
}
}
}
}
])
Output i needed:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"assignedTo": 1,
"cg": 45,
"color": "braun, rose gold",
"details": 0,
"firstBillable": "2022-08-24T07:29:01.639Z"
},
{
"_id": ObjectId("5a934e000102030405000001"),
"assignedTo": 2,
"cg": 55,
"color": "beige, silber",
"details": 0,
"firstBillable": "2022-07-25T07:29:01.639Z"
}
]
https://mongoplayground.net/p/YQps9EozlAL
Concerns:
You should compare the date as Date instead of string, hence you are required to convert the date strings to Date before comparing.
In users collection, prices is an array. You need to deconstruct the array to multiple documents first before compare the date fields in price.
The query should be:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"as": "details",
let: {
assigned_to: "$assignedTo",
first_billable: {
$toDate: "$firstBillable"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$id",
"$$assigned_to"
]
}
}
},
{
$unwind: "$prices"
},
{
"$match": {
$expr: {
"$and": [
{
"$gte": [
"$$first_billable",
{
$toDate: "$prices.beginDate"
}
]
},
{
"$lte": [
"$$first_billable",
{
$toDate: "$prices.endDate"
}
]
}
]
}
}
}
]
}
},
{
"$addFields": {
"details": 0,
"cg": {
$first: "$details.prices.price"
}
}
}
])
Demo # Mongo Playground

$addfield with three $cond mongodb

I have this output data from aggregation $lookup
[
{
_id: 1,
name: "Abraham",
class: "V",
question_answered: [
{
id: "quest1",
answer: "A",
score: 10,
question: {
soal: "apa judul lagu?",
correct_answer: "A",
type_question: "Essay"
}
},
{
id: "quest2",
answer: "C",
score: null,
question: {
soal: "apa judul lagu B?",
correct_answer: "B",
type_question: "Essay"
}
},
{
id: "quest3",
answer: "C",
score: 10,
question: {
soal: "apa judul lagu C?",
correct_answer: "C",
type_question: "essay_pg"
}
},
]
},
{
_id: 2,
name: "Brenda",
class: "V",
question_answered: [
{
id: "quest1",
answer: "A",
score: 10,
question: {
soal: "apa judul lagu A?",
correct_answer: "A",
type_question: "Essay"
}
},
{
id: "quest2",
answer: "C",
score: 0,
question: {
soal: "apa judul lagu B?",
correct_answer: "B",
type_question: "Essay"
}
}
]
}
]
I need to add additional field formated_status_evaluation_essay and formated_status_evaluation_essay_pg in each data that i get with some few condition if,elseif, else. i'll give one of example addfield condition, more or less like this one:
IF(question_answered.question.type_question == 'Essay' and no score is
null in every essay type question) then,
formated_status_evaluation_essay = "complete scoring".
ELSEIF(there's essay type question and have at least one null score)
then, formated_status_evaluation_essay = "Incomplete scoring"
ELSEIF(if theres no essay type question) then,
formated_status_evaluation_essay = "no question"
Same goes to formated_status_evaluation_essay_pg. The output that i expected is like this.
[
{
_id: 1,
name: "Abraham",
class: "V",
question_answered: [....],
formated_status_evaluation_essay: incomplete scoring,
formated_status_evaluation_essay_pg: complete scoring,
},
{
_id: 2,
name: "Brenda",
class: "V",
question_answered: [....],
formated_status_evaluation_essay: complete scoring,
formated_status_evaluation_essay_pg: no question,
}
]
The explanation about the output.
_id:1, get evaluation_essay incomplete because it has one object that contain null score. But the evaluation_essay_pg contain complete
scoring because essay_pg type all of it have a score.
_id:2, evaluation_essay is complete because all question with type essay have a score. But essay_pg contain no question because theres no essay_pg type in question_answer.question.type_question.
I've tried this and still confuse to code three condition like i've explained before. I put code like this in the end of $lookup aggregation.
{
'$addFields': {
'formated_status_evaluation_essay': {
'$cond': [
{
'$and': [
{'$$question_answer.question.type_soal ':
'essay'},
{'$$question_answer.nilai':{$ne:null}},
]
},
'already scoring',
'havent scoring'
]
}
}
}
i almost get what i expected but, seems still have a wrong syntax i wrote. I would be very thankfull if you guys can help me. Been working for two days still got no answer.
Try to make the code a little bit more readable by using $switch to handle the branching.
db.collection.aggregate([
{
"$addFields": {
"formated_status_evaluation_essay": {
"$filter": {
"input": "$question_answered",
"as": "q",
"cond": {
$eq: [
"$$q.question.type_question",
"Essay"
]
}
}
},
"formated_status_evaluation_essay_pg": {
"$filter": {
"input": "$question_answered",
"as": "q",
"cond": {
$eq: [
"$$q.question.type_question",
"essay_pg"
]
}
}
}
}
},
{
"$addFields": {
"formated_status_evaluation_essay": {
"$switch": {
"branches": [
{
"case": {
$and: [
{
"$allElementsTrue": [
{
"$map": {
"input": "$formated_status_evaluation_essay.score",
"as": "s",
"in": {
$ne: [
"$$s",
null
]
}
}
}
]
},
{
$ne: [
{
$size: "$formated_status_evaluation_essay"
},
0
]
}
]
},
"then": "complete scoring"
},
{
"case": {
"$anyElementTrue": [
{
"$map": {
"input": "$formated_status_evaluation_essay.score",
"as": "s",
"in": {
$eq: [
"$$s",
null
]
}
}
}
]
},
"then": "incomplete scoring"
}
],
default: "no question"
}
},
"formated_status_evaluation_essay_pg": {
"$switch": {
"branches": [
{
"case": {
$and: [
{
"$allElementsTrue": [
{
"$map": {
"input": "$formated_status_evaluation_essay_pg.score",
"as": "s",
"in": {
$ne: [
"$$s",
null
]
}
}
}
]
},
{
$ne: [
{
$size: "$formated_status_evaluation_essay_pg"
},
0
]
}
]
},
"then": "complete scoring"
},
{
"case": {
"$anyElementTrue": [
{
"$map": {
"input": "$formated_status_evaluation_essay_pg.score",
"as": "s",
"in": {
$eq: [
"$$s",
null
]
}
}
}
]
},
"then": "incomplete scoring"
}
],
default: "no question"
}
}
}
}
])
Here is the Mongo playground for your reference.

Increment value in document and nested array simultaenously (with upsert)

I want to increment a value both on document root as well as inside a nested array.
A playground example here
The schema
const UserPoints = new Schema(
{
points: {
type: Number,
},
monthly: {
type: [
new Schema(
{
year: {
type: Number,
},
month: {
type: Number,
},
points: {
type: Number,
min: 0,
},
},
{
_id: false,
timestamps: false,
}
),
],
},
},
{
timestamps: false,
}
);
What I have tried
Variables used: (currentYear = 2021, currentMonth = 7, addPoints = 5)
Note: The year, month may not exist in the document yet, so I need to make it work with "upsert".
UserPoints.findOneAndUpdate(
{
_id: userId,
"monthly.year": currentYear,
"monthly.month": currentMonth,
},
{
$inc: {
points: addPoints,
"monthly.$.points": addPoints,
},
},
{
upsert: true,
new: true,
}
).exec()
This does not work. And gives out an error:
{
ok:0
code:2
codeName:"BadValue"
name:"MongoError"
}
I would appreciate if someone can point me at the right direction to increment these values in the same operation.
Update:
The only way that I could make it work is by first making a query to check if the (year, month) exists in the "monthly" array.
Depending if it exists, I either $push the new month, or $inc the existing one's values.
Pipeline update way, requires MongoDB >=4.2
You filter by _id using index, so it will be fast also.
Query
Test code here
(put id=1 will update(add points to member points and to externa point field), id=2 will do nothing,id=3 will insert the member to the empty array and update the points, id=4 will upsert with an array contains only this member,and the points its points)
Cases (this is the order checked in the $cond also)
document doesnt exists, array will have 1 single member, and root points will have the value of the new member points
document exists , member dont exists, monthly empty
adds the member {:year ...} and increases the root point field
document exists, member exists
increase points inside the member and the root point fields
document exists, member dont exist, monthly not empty
does nothing
db.collection.update({
"_id": 4
},
[
{
"$addFields": {
"isupsert": {
"$not": [
{
"$ne": [
{
"$type": "$monthly"
},
"missing"
]
}
]
}
}
},
{
"$addFields": {
"doc": {
"$switch": {
"branches": [
{
"case": "$isupsert",
"then": {
"_id": "$_id",
"points": 5,
"monthly": [
{
"year": 2021,
"month": 7,
"points": 5
}
]
}
},
{
"case": {
"$and": [
{
"$isArray": [
"$monthly"
]
},
{
"$eq": [
{
"$size": "$monthly"
},
0
]
}
]
},
"then": {
"_id": "$_id",
"points": 5,
"monthly": [
{
"year": 2021,
"month": 7,
"points": 5
}
]
}
}
],
"default": {
"$let": {
"vars": {
"found": {
"$not": [
{
"$eq": [
{
"$size": {
"$filter": {
"input": "$monthly",
"as": "m",
"cond": {
"$and": [
{
"$eq": [
"$$m.year",
2021
]
},
{
"$eq": [
"$$m.month",
7
]
}
]
}
}
}
},
0
]
}
]
}
},
"in": {
"$cond": [
"$$found",
{
"$mergeObjects": [
"$ROOT",
{
"points": {
"$add": [
"$points",
5
]
},
"monthly": {
"$map": {
"input": "$monthly",
"as": "m",
"in": {
"$cond": [
{
"$and": [
{
"$eq": [
"$$m.year",
2021
]
},
{
"$eq": [
"$$m.month",
7
]
}
]
},
{
"$mergeObjects": [
"$$m",
{
"points": {
"$add": [
"$$m.points",
5
]
}
}
]
},
"$$m"
]
}
}
}
}
]
},
"$$ROOT"
]
}
}
}
}
}
}
},
{
"$replaceRoot": {
"newRoot": "$doc"
}
},
{
"$unset": [
"isupsert"
]
}
],
{
"upsert": true
})
you should find in array by $elemMatch
UserPoints.findOneAndUpdate(
{
_id: userId,
"monthly":{$elemMatch:{"year": currentYear}},
"monthly":{$elemMatch:{"month": currentMonth}}
},
{
$inc: {
points: addPoints,
"monthly.$.points": addPoints,
},
},
{
upsert: true,
new: true,
}
).exec()

Display the conditionnal size of an array with the others fields of a mongodb document

I have a collection of fridges and I would like to have some fields from each fridge matching a condition plus the 'conditionnal size' of the items in this fridge.
This is an example of my DB :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand:"Bosch",
size:195,
cooler:true,
color:"grey"
},
{
_id: 2,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand:"Electrolux",
size:200,
cooler:true,
color:"white"
},
]
}
I want to get fridges with these mutuals conditions ('and' condition):
brand is $in ["Bosch","Samsung"]
color is $in ["grey","white"]
In addition :
The number of items with a name $in ["beer","lettuce"]
And finally :
Removing some fields like the size and items of the result.
In our example, the excepted output would be :
{
_id:1
itemsNumber:1,
brand:"Bosch",
cooler:true,
color:"grey"
}
Explanations :
We removed the field items and size, itemsNumber counts the number of beers and lettuce from items array. And we only keep the first fridge its brand is Bosch and it's grey.
This what I have so far :
db.fridges.aggregate([
{
"$match": {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
"$project": {
"itemsNumber": {
$size: "$items" // This is not good
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Which returns me :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 2
}
]
Counting the items matching with either beer or lettuce is my main problem.
This is an executable example.
Thanks in advance !
I found out how to make it work. Thank you #joe for suggesting to use filter this was indeed the solution.
Here is the complete query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Runnable example.

Zip two array and create new array of object

hello all i'm working with a MongoDB database where each data row is like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
"userhasRate" : [
"51",
"52",
],
"ratings" : [
4,
3
],
}
and i need to change it to be like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
rate : [
{userhasrate: "51", value: 4},
{userhasrate: "52", value: 3},
]
}
I already try using this method,
db.getCollection('contens').aggregate([
{ '$group':{
'rates': {$push:{ value: '$ratings', user: '$userhasRate'}}
}
}
]);
and my result become like this
{
"rates" : [
{
"value" : [
5,
5,
5
],
"user" : [
"51",
"52",
"53"
]
}
]
}
Can someone help me to solve my problem,
Thank you
You can use $arrayToObject and $objectToArray inside $map to achieve the required output.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$objectToArray": {
"$arrayToObject": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
}
}
},
"as": "el",
"in": {
"userhasRate": "$$el.k",
"value": "$$el.v"
}
}
}
}
}
])
Alternative Method
If userhasRate contains repeated values then the first solution will not work. You can use arrayElemAt and $map along with $zip if it contains repeated values.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
},
"as": "el",
"in": {
"userhasRate": {
"$arrayElemAt": [
"$$el",
0
]
},
"value": {
"$arrayElemAt": [
"$$el",
1
]
}
}
}
}
}
}
])
Try below aggregate, first of all you used group without _id that grouped all the JSONs in the collection instead set it to "$_id" also you need to create 2 arrays using old data then in next project pipeline concat the arrays to get desired output:
db.getCollection('contens').aggregate([
{
$group: {
_id: "$_id",
rate1: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
0
]
},
value: {
$arrayElemAt: [
"$ratings",
0
]
}
}
},
rate2: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
1
]
},
value: {
$arrayElemAt: [
"$ratings",
1
]
}
}
}
}
},
{
$project: {
_id: 1,
rate: {
$concatArrays: [
"$rate1",
"$rate2"
]
}
}
}
])