can somebody tell me please if is possible to update last item in array of documents? For example in this document:
{
name: 'my name',
someArray: [
{rate: 10},
{rate: 9},
{rate: 20}
]
}
I would like to update last item with rate:20 to rate: 50.
How to update exactly the last item in Mongodb?
Thanks.
One option is using an update pipeline:
Split the array into the last item and all the rest.
update the last item
Build the array again using $concatArrays
db.collection.update(
{name: "my name"},
[
{$set: {
lastItem: {$last: "$someArray"},
rest: {$slice: ["$someArray", 0, {$subtract: [{$size: "$someArray"}, 1]}]}
}
},
{$set: {"lastItem.rate": 50}},
{$set: {
someArray: {$concatArrays: ["$rest", ["$lastItem"]]},
lastItem: "$$REMOVE",
rest: "$$REMOVE"
}
}
])
See how it works on the playground example - concatArrays
Another option is using $reduce:
Here we are iterating on the array, for each item checking if it is the last one, and if so updating it:
db.collection.update(
{name: "my name"},
[
{$set: {lastIndex: {$subtract: [{$size: "$someArray"}, 1]}}},
{$set: {
lastIndex: "$$REMOVE",
someArray: {
$reduce: {
input: "$someArray",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
{
$cond: [
{$eq: [{$size: "$$value"}, "$lastIndex"]},
{$mergeObjects: [
"$$this",
{rate: 50}
]
},
"$$this"
]
}
]
]
}
}
}
}
}
])
See how it works on the playground example - reduce
Related
After doing a $facet I receive this output:
[
{
"confirmed": [
{
"confirmed": 100
}
],
"denied": [
{
"denied": 50
}
],
"pending": [
{
"pending": 20
}
]
}
]
how can I project it into something like this?
[
{
category: "confirmed", count: 100,
category: "denied", count: 50,
category: "pending", count: 20
}
]
I need the faucet part because to extract those numbers I have to do several $match to the same data. Dont know if there is a better option.
Thank you!
What you ask is not a valid format. This is an object with duplicate keys. You may want:
[{"confirmed": 100, "denied": 50, "pending": 20}]
or
[
{category: "confirmed", count: 100},
{category: "denied", count: 50},
{category: "pending", count: 20}
]
which are both valid options
I guess you want the second option. If you want the generic solution, one option is:
db.collection.aggregate([
{$project: {res: {$objectToArray: "$$ROOT"}}},
{$project: {
res: {$map: {
input: "$res",
in: {category: "$$this.k", count: {$objectToArray: {$first: "$$this.v"}}}
}}
}},
{$project: {
res: {$map: {
input: "$res",
in: {category: "$$this.category", count: {$first: "$$this.count.v"}}
}}
}},
{$unwind: "$res"},
{$replaceRoot: {newRoot: "$res"}}
])
See how it works on the playground example - generic
If you want the literal option, just use:
db.collection.aggregate([
{$project: {
res: [
{category: "confirmed", count: {$first: "$confirmed.confirmed"}},
{category: "denied", count: {$first: "$denied.denied"}},
{category: "pending", count: {$first: "$pending.pending"}}
]
}
},
{$unwind: "$res"},
{$replaceRoot: {newRoot: "$res"}}
])
See how it works on the playground example - literal
{
"_id": "6339f99ee18b2481a04b4fe8",
"userId": "60a8a51cf2229813a45d2238",
"array1": [
{
"someId1": "6339f99ee18b2481a04b4fe9",
"customIndex": 2,
"array2": [
{
"someId2": "6339f99ee18b2481a04b4fea",
"startDate": 2022-10-10T19:56:26.000+00:00,
"endDate": 2022-10-12T19:56:26.000+00:00,
}
]
},
{
"someId1": "6345ca40112b743fd8172be0",
"customIndex": 4,
"array2": [
{
"someId2": "6345ca40112b743fd8172be1",
"startDate": 2022-10-10T19:56:26.000+00:00,
"endDate": 2022-10-27T19:56:26.000+00:00,
}
]
}
]
}
I have above structure in mongoDB and want to get only that object from array1 which matches the conditions of endDate > 2022-10-17
Here's what I try to do:
result= await Collection.find({
userId: { '$in': userIdList},
'array1.array2.endDate': { "$gte": 2022-10-17}
})
But above return the both objects from array1 even though the endDate for one object is less than 2022-10-17
How can I get the the response like below? Also, Am I using the right Mongoose calls to achieve what I am trying to achieve.
Expected response that I am trying to achieve:
{
"_id": "6339f99ee18b2481a04b4fe8",
"userId": "60a8a51cf2229813a45d2238",
"array1": [
{
"someId1": "6345ca40112b743fd8172be0",
"customIndex": 4,
"array2": [
{
"someId2": "6345ca40112b743fd8172be1",
"startDate": 2022-10-10T19:56:26.000+00:00,
"endDate": 2022-10-27T19:56:26.000+00:00,
}
]
}
]
}
If array1 can contain several such items, and array2 contain several such items, one option is using $reduce with $filter and $mergeObjects for this:
db.collection.aggregate([
{$match: {userId: {'$in': userIdList}}}
{$project: {
userId: 1,
array1: {
$reduce: {
input: "$array1",
initialValue: [],
in: {$concatArrays: [
"$$value",
[{$mergeObjects: [
"$$this",
{array2: {
$filter: {
input: "$$this.array2",
as: "innerItem",
cond: {$gte: [
"$$innerItem.endDate",
{$dateFromParts: {year: 2022, month: 10, day: 17}}
]}
}
}}
]}]
]}
}
}
}},
{$project: {
userId: 1,
array1: {$filter: {
input: "$array1",
cond: {$gt: [{$size: "$$this.array2"}, 0]}
}}
}}
])
See how it works on the playground example
Please help me. I have a collection as below
[
{
"_id":{
"$oid":"62a3673660e2f16c7a7bc088"
},
"merchant":{
"$oid":"62a3640560e2f16c7a7bc078"
},
"title":"24 Test 1",
"filter_conditions":{
"city":[
"AAA",
"BBB",
"CCC",
"DDD"
],
"state":[
],
"pincode":[
"12345"
]
}
},
{
"_id":{
"$oid":"62a3673660e2f16c7a7bc089"
},
"merchant":{
"$oid":"62a3640560e2f16c7a7bc079"
},
"title":"24 Test 2",
"filter_conditions":{
"city":[
"AAA",
"BBB"
]
}
}
]
I want to filter data based on pincode/city/state
if pincode is present match it and ignore city and state
elseif city is present match it and ignore state
else match on state
You can use an aggregation pipeline with a $filter:
If any of the fields does not exist on the doc, create it with an empty array.
Use $filter to grade the docs, so the grade for matching pincode is 100, for matching city is 10 for matching state is 1. Use $max to keep the best grade only.
Return the doc with highest grade.
db.collection.aggregate([
{$set: {
"filter_conditions.pincode": {$ifNull: ["$filter_conditions.pincode", []]},
"filter_conditions.city": {$ifNull: ["$filter_conditions.city", []]},
"filter_conditions.state": {$ifNull: ["$filter_conditions.state", []]}
}
},
{$set: {
grade: {
$max: [
{$multiply: [
{$size: {
$filter: {
input: "$filter_conditions.pincode",
as: "item",
cond: {$eq: ["$$item", "12345"]}
}
}
}, 100]
},
{$multiply: [
{$size: {
$filter: {
input: "$filter_conditions.city",
as: "item",
cond: {$eq: ["$$item", "BBB"]}
}
}
}, 10]
},
{$multiply: [
{$size: {
$filter: {
input: "$filter_conditions.state",
as: "item",
cond: {$eq: ["$$item", "AL"]}
}
}
}, 1]
}
]
}
}
},
{$sort: {grade: -1}},
{$limit: 1}
])
See how it works on the playground example
You can work with nested $cond to perform the filtering.
Concept:
Check filter_conditions.pincode is existed.
1.1. If true, check the value is existed in filter_conditions.pincode array.
1.2. Else, proceed to 2.
Check filter_conditions.city is existed.
2.1. If true, check the value is existed in filter_conditions.city array.
2.2. Else, proceed to 3.
Check if value is existed in filter_conditions.state array (default as empty array if the array is not existed).
db.collection.aggregate([
{
$match: {
$expr: {
$cond: {
if: {
$ne: [
"$filter_conditions.pincode",
undefined
]
},
then: {
$in: [
"", // pincode value
"$filter_conditions.pincode"
]
},
else: {
$cond: {
if: {
$ne: [
"$filter_conditions.city",
undefined
]
},
then: {
$in: [
"", // city value
"$filter_conditions.city"
]
},
else: {
$in: [
"", // state value
{
$ifNull: [
"$filter_conditions.state",
[]
]
}
]
}
}
}
}
}
}
}
])
Sample Mongo Playground
I have a document like:
[
{_id:1, field: {array: [1,2,3,4,1,1] }},
{_id:2, field: {array: [5,1,1,1,1,1] }},
{_id:3, field: {array: [3,2,3,4,1,2] }}
]
I want to count the array elements which eq 1.
The result is:
[
{_id: 1, count: 3},
{_id: 2, count: 5},
{_id: 3, count: 1}
]
You can try an aggregation query,
$filter to iterate loop of an array and check condition if the value is 1
$size to get total elements of the filtered array
db.collection.aggregate([
{
$project: {
count: {
$size: {
$filter: {
input: "$field.array",
cond: { $eq: ["$$this", 1] }
}
}
}
}
}
])
Playground
The second possible option,
$reduce to iterate loop of array
$cond to check if the value is equal to 1
if it is 1 then $add plus one in initialValue otherwise return the same number
db.collection.aggregate([
{
$project: {
count: {
$reduce: {
input: "$field.array",
initialValue: 0,
in: {
$cond: [
{ $eq: ["$$this", 1] },
{ $add: ["$$value", 1] },
"$$value"
]
}
}
}
}
}
])
Playground
Here is an extended solution that will get the distribution of the value of all elements of field.array.
db.foo.aggregate([
{$addFields: {distrib: {$reduce: {
input: "$field.array",
initialValue: {"1":0,"2":0,"3":0,"4":0,"5":0},
in: {
"1":{$add:["$$value.1",{$toInt:{$eq:[1,"$$this"]}}]},
"2":{$add:["$$value.2",{$toInt:{$eq:[2,"$$this"]}}]},
"3":{$add:["$$value.3",{$toInt:{$eq:[3,"$$this"]}}]},
"4":{$add:["$$value.4",{$toInt:{$eq:[4,"$$this"]}}]},
"5":{$add:["$$value.5",{$toInt:{$eq:[5,"$$this"]}}]}
}
}}
}}
]);
And typically the follow-on question is how to get distributions across multiple docs, which in a way is "easier" because $bucket works across a pipeline of documents:
db.foo.aggregate([
{$unwind: "$field.array"},
{$bucket: {
groupBy: "$field.array",
boundaries: [1,2,3,4,5,6,7,8,9,10],
output: {
n: {$sum:1}
}
}}
]);
Alternately, you can add this stage after the $addFields/$reduce stage. It yields messy arrays of 1 object of which only field n is interesting but you can easily get the value in the client-side with doc['1'][0]['n'], doc['2'][0]['n'], etc.
,{$facet: {
"1": [ {$group: {_id:null, n:{$sum:"$distrib.1"}}} ],
"2": [ {$group: {_id:null, n:{$sum:"$distrib.2"}}} ],
"3": [ {$group: {_id:null, n:{$sum:"$distrib.3"}}} ],
"4": [ {$group: {_id:null, n:{$sum:"$distrib.4"}}} ],
"5": [ {$group: {_id:null, n:{$sum:"$distrib.5"}}} ]
}}
If you really want to make the return structure simple, add this stage at the end to collapse the [0]['n'] data:
,{$addFields: {
"1": {$let:{vars:{q:{$arrayElemAt:["$1",0]}},in: "$$q.n"}},
"2": {$let:{vars:{q:{$arrayElemAt:["$2",0]}},in: "$$q.n"}},
"3": {$let:{vars:{q:{$arrayElemAt:["$3",0]}},in: "$$q.n"}},
"4": {$let:{vars:{q:{$arrayElemAt:["$4",0]}},in: "$$q.n"}},
"5": {$let:{vars:{q:{$arrayElemAt:["$5",0]}},in: "$$q.n"}}
}}
In MongoDB 5.0, the new $getField function makes this a little more straightforward:
,{$addFields: {
"1": {$getField:{input:{$arrayElemAt:["$1",0]}, field:"n"}}
...
I read that starting with MongoDB 4.2 you can do aggregation pipeline with updateMany. I don't have a good gasp of aggregation pipeline so I thought of doing following to my collection.
user{
id: ObjectId(..),
books: [
{
value: 10,
type: PDF
},
{
value: 10,
type: HARDCOVER
}
...
]
}
I am trying to lower the value of PDF book from 10 to 5 if user has PAPER_BACK or HARDCOVER in their books
db.user.updateMany(
{},
[
{$match: {'books' : { $elemMatch: {'value': '10', 'type': 'PDF'}}, 'books.type': {$in: ["PAPER_BACK", "HARDCOVER"]}}},
{$unwind: '$books'},
{$match: {"books.type": "PDF"}},
{$set: {"books.value": "5"}}
]
)
I thought the above work with the new updateMany() but it's throwing error saying $match is not allowed to be used. Am I misunderstanding completely?
As you noted $match can't be used within the pipe, nor can $unwind. Use the first arg of updateMany to perform your match.
You are querying the string '10', make sure you change to a number.
{
books: {
$elemMatch: {
"value": 10,
"type": "PDF"
}
},
"books.type": {
$in: [
"HARDCOVER",
"SOFTCOVER"
]
}
}
Then to perform the update, use can use $map like this:
db.user.updateMany(
{
books: {
$elemMatch: {
"value": 10,
"type": "PDF"
}
},
"books.type": {
$in: [
"HARDCOVER",
"SOFTCOVER"
]
}
},
[
{$set: {
books: {$map: {
input: "$books",
in: {$mergeObjects: [
"$$this",
{$cond: [
{$and: [
{$eq: ["$$this.type", "PDF"]},
{$eq: ["$$this.value", 10]}
]},
{value: 5},
null
]}
]}
}}
}}
]
)