MongoDB: aggregation, array of objects to string value - javascript

My question is pretty similar to: MongoDB Aggregation join array of strings to single string, but instead of pure Array, like: ['Batman', 'Robin'] I have Array of objects:
_id: 1,
field_value: [
{
_id: 2,
name: "Batman"
},
{
_id: 3,
name: "Robin"
}
]
I am trying to use $reduce but got error instead.
I want to receive the following result:
_id: 1,
field_value: "Batman, Robin" /** <= String value */
or at least array of property values:
_id: 1,
field_value: ["Batman", "Robin"] /** <= Array of strings (name property) */
My MongoPlayground data example

You need the same approach with $reduce, $$this represents a single field_value entity so you need $$this.name:
db.collection.aggregate([
{
$project: {
field_value: {
$reduce: {
input: "$field_value",
initialValue: "",
in: {
$concat: [
"$$value",
{ $cond: [ { $eq: [ "$$value", "" ] }, "", "," ] },
{ $toString: "$$this.name" }
]
}
}
}
}
}
])
Mongo Playground

Related

MongoDB Aggregation - How to get/update sum

For example, I have something in my database like in customers collection.
{
Max: {
shoping_list: {
food: { Pizza: 2, Ramen: 1, Sushi: 5 }
}
},
John: {
shoping_list: {
food: { Pizza: 2, Ramen: 1, Burger: 1 }
}
}
}
In my backend, I want to get the sum of food
const request = await customers.aggregate([
{
$group: {
_id: null,
Pizza: {
$sum: '$shoping_list.food.Pizza',
},
Is there a way how to update or get the sum automatically without manually writing every food from the shopping_list?
The design of the document may lead the query looks complex but still achievable.
$replaceRoot - Replace the input document with a new document.
1.1. $reduce - Iterate the array and transform it into a new form (array).
1.2. input - Transform key-value pair of current document $$ROOT to an array of objects such as: [{ k: "", v: "" }]
1.3. initialValue - Initialize the value with an empty array. And this will result in the output in the array.
1.4. in
1.4.1. $concatArrays - Combine aggregate array result ($$value) with 1.4.2.
1.4.2. With the $cond operator to filter out the document with { k: "_id" }, and we transform the current iterate object's v shoping_list.food to the array via $objectToArray.
$unwind - Deconstruct the foods array into multiple documents.
$group - Group by foods.k and perform sum for foods.v.
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
foods: {
$reduce: {
input: {
$objectToArray: "$$ROOT"
},
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$cond: {
if: {
$ne: [
"$$this.k",
"_id"
]
},
then: {
$objectToArray: "$$this.v.shoping_list.food"
},
else: []
}
}
]
}
}
}
}
}
},
{
$unwind: "$foods"
},
{
$group: {
_id: "$foods.k",
sum: {
$sum: "$foods.v"
}
}
}
])
Demo # Mongo Playground

MongoDB Aggregation - match documents with array of objects, by another array of objects filter

I have documents that consist of an array of objects, and each object in this array consists of another array of objects.
For simplicity, irrelevant fields of the documents were omitted.
It looks like this (2 documents):
{
title: 'abc',
parts: [
{
part: "verse",
progressions: [
{
progression: "62a4a87da7fdbdabf787e47f",
key: "Ab",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
{
progression: "62adf477ed11cbbe156d5769",
key: "C",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
],
_id: "62b5aaa0c9e9fe8a7d7240d2"
},
{
part: "chorus",
progressions: [
{
progression: "62a4a51b4693c43dce9be09c",
key: "E",
_id: "62b5aaa0c9e9fe8a7d7240d9"
}
],
_id: "62b5aaa0c9e9fe8a7d7240d8"
}
],
}
{
title: 'def',
parts: [
{
part: "verse",
progressions: [
{
progression: "33a4a87da7fopvvbf787erwe",
key: "E",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
{
progression: "98opf477ewfscbbe156d5442",
key: "Bb",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
],
_id: "12r3aaa0c4r5me8a7d72oi8u"
},
{
part: "bridge",
progressions: [
{
progression: "62a4a51b4693c43dce9be09c",
key: "C#",
_id: "62b5aaa0c9e9fe8a7d7240d9"
}
],
_id: "62b5aaa0rwfvse8a7d7240d8"
}
],
}
The parameters that the client sends with a request are an array of objects:
[
{ part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
{ part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
]
I want to retrieve, through mongodb aggregation, the documents that at least one of objects in the input array above is matching them:
In this example, documents that have in their parts array field, an object that has the value 'verse' in the part property and one of the progressions id's ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] in the progression property in one of the objects in the progressions property, or documents that have in their parts array field, an object that has the value 'chorus' in the part property and one of the progressions id's ['62adf477ed11cbbe156d5769'] in the progression property in one of the objects in the progressions property.
In this example, the matching document is the first one (with the title 'abc'), but in actual use, there might be many matching documents.
I tried to create an aggregation pipeline myself (using the mongoose 'aggregate' method):
// parsedProgressions = [
// { part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
// { part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
// ]
songs.aggregate([
{
$addFields: {
"tempMapResults": {
$map: {
input: parsedProgressions,
as: "parsedProgression",
in: {
$cond: {
if: { parts: { $elemMatch: { part: "$$parsedProgression.part", "progressions.progression": mongoose.Types.ObjectId("$$parsedProgression.progression") } } },
then: true, else: false
}
}
}
}
}
},
{
$addFields: {
"isMatched": { $anyElementTrue: ["$tempMapResults"] }
}
},
{ $match: { isMatched: true } },
{ $project: { title: 1, "parts.part": 1, "parts.progressions.progression": 1 } }
]);
But it didn't work - as I understand it, because the $elemMatch can be used only in the $match stage.
Anyway, I guess I overcomplicated the aggregation pipeline, so I will be glad if you can fix my aggregation pipeline/offer a better working one.
This is not a simple case as these are both nested arrays and we need to match both the part and the progressions, which are not on the same level
One option looks complicated a bit, but keeps your data small:
In order to make things easier, $set a new array field called matchCond which includes an array called progs containing the parts.progressions. To each sub-object inside it insert the matching progressions input array. We do need to be careful here and handle the case where there is no matching progressions input arrayprogressions input array, as this is the case for the "bridge" part on the second document.
Now we just need to check if for any of these progs items, the progression field is matching one option in input array. This is done using $filter, and $rediceing the number of results.
Just match document which have results and format the answer
db.collection.aggregate([
{
$set: {
matchCond: {
$map: {
input: "$parts",
as: "parts",
in: {progs: {
$map: {
input: "$$parts.progressions",
in: {$mergeObjects: [
"$$this",
{input: {progressions: []}},
{input: {$first: {
$filter: {
input: inputData,
as: "inputPart",
cond: {$eq: ["$$inputPart.part", "$$parts.part"]}
}
}}}
]}
}
}}
}
}
}
},
{$set: {
matchCond: {
$reduce: {
input: "$matchCond",
initialValue: 0,
in: {$add: [
"$$value",
{$size: {
$filter: {
input: "$$this.progs",
as: "part",
cond: {$in: ["$$part.progression", "$$part.input.progressions"]}
}
}
}
]
}
}
}
}
},
{$match: {matchCond: {$gt: 0}}},
{$project: {title: 1, parts: 1}}
])
See how it works on the playground example
Another option is to use $unwind, which looks simple, but will duplicate your data, thus, likely to be slower:
db.collection.aggregate([
{$addFields: {inputData: inputData, cond: "$parts"}},
{$unwind: "$cond"},
{$unwind: "$cond.progressions"},
{$unwind: "$inputData"},
{$match: {
$expr: {
$and: [
{$eq: ["$cond.part", "$inputData.part"]},
{$in: ["$cond.progressions.progression", "$inputData.progressions"]}
]
}
}
},
{$project: {title: 1, parts: 1}}
])
See how it works on the playground example - unwind
There are several options between these two...

Mongoose - renaming object key within array

I have this one schema
{
_id: "123456",
id: "123",
inventory: [
{
id: "foo",
count: 0
},
{
id: "bar",
count: 3
}
]
}
I wanted every "count" keys in the inventory array to be "price" which will look like this at the end:
{
_id: "123456",
id: "123",
inventory: [
{
id: "foo",
price: 0
},
{
id: "bar",
price: 3
}
]
}
And I've tried this
Model.updateOne({ id: "123" }, { $unset: { inventory: [{ count: 1 }] } } )
But it seems to be deleting the "inventory" field itself
The first thing here is to try to use $rename but how the docs explain:
$rename does not work if these fields are in array elements.
So is necessary to look for another method. So you can use this update with aggregation query:
This query uses mainly $map, $arrayToObject and $objectToArray. The trick here is:
Create a new field called inventory (overwrite existing one)
Iterate over every value of the array with $map, and then for each object in the array use $objectToArray to create an array and also iterate over that second array using again $map.
Into this second iteration create fields k and v. Field v will be the same (you don't want to change the value, only the key). And for field k you have to change only the one whose match with your condition, i.e. only change from count to price. If this condition is not matched then the key remain.
db.collection.update({},
[
{
$set: {
inventory: {
$map: {
input: "$inventory",
in: {
$arrayToObject: {
$map: {
input: {$objectToArray: "$$this"},
in: {
k: {
$cond: [
{
$eq: ["$$this.k","count"]
},
"price",
"$$this.k"
]
},
v: "$$this.v"
}
}
}
}
}
}
}
}
])
Example here

How do you push to an array inside of an object, inside of an array with MongoDB?

I need to be able to push new objects to fameTotals. Having a bit of trouble, thanks!
This is what the document would look like in my database:
{
clanName: null,
clanTag: null,
players: [
{
name: null,
tag: null,
fameTotals: [ //PUSH TO THIS ARRAY
{fame: 0, clanTrophies: 0, date: ''}
]
}
]
}
I've been trying to figure it out with updateOne() and the $push operator.
Check this example
db.collection.update({
_id: 1
},
{
$push: {
players: {
name: "yyy",
tag: "bbbb",
fameTotals: [
{
fame: 1,
clanTrophies: 1,
date: "27-7-21"
}
]
}
}
})
Mongoplayground

mongoDB project array element after filter

I have the following (simplified) aggregation:
Model.aggregate([
{
$lookup: {
from: 'orders',
localField: '_id',
foreignField: 'customer',
as: 'orders',
},
},
{
$project: {
openOrders: {
$filter: {
input: '$orders',
as: 'order',
cond: { $eq: ['$$order.status', 'open'] },
},
},
},
},
])
which returns the following:
{
_id: ...,
openOrders: [
[Object], [Object]
],
}
Those [Object]'s are simply the returned objects, persisted in the database, with all their fields.
I don't find a way to project/filter out those objects' fields and instead return only their _id's:
{
_id: ...,
openOrders: [
_id: ...,
_id: ....
],
}
EDIT: I'd rather prefer the following expected output:
{
_id: ...,
openOrders: [
{ _id: ... },
{ _id: ... }
],
}
I tried adding a new $project stage at various points of the aggregation with no success. Can someone help me?
You should add a $project stage like below:
{
$project: {
openOrders: 'openOrders._id'
}
}
This will give the output like:
{
_id: ...,
openOrders: [
_id1,
_id2,
...
],
}
instead of
{
_id: ...,
openOrders: [
_id: ...,
_id: ....
],
}
I suggest this type of querying because, if you actually see openOrders, it's just the array of _ids, so adding only one _id field inside the array doesn't make sence.
If you still want the output to be like the array of object, then you can use the below:
{
$project: {
'openOrders._id': 1
}
}
As you need an array of _id's like this
openOrders: [ _id: ..., _id: .... ]
but not an array of _id's in objects :
openOrders: [ {_id: ...}, {_id: ....} ]
You need to use $reduce instead of $filter :
Try below query :
db.collection.aggregate([
{
$project: {
openOrders: {
$reduce: {
input: "$orders", // Same like `$filter` use reduce to iterate on array
initialValue: [], // consider an initial value
in: { // If condition is met, push value to array else return holding array as is.
$cond: [ { $eq: [ "$$this.status", "open" ] },
{ $concatArrays: [ "$$value", [ "$$this._id" ] ] },
"$$value"
]
}
}
}
}
}
])
Test : mongoplayground
Note : In javaScript - if you're printing a JSON with objects, you need to print it with JSON.stringify(yourJSON) - which makes it a string, So that you don't see [Object], [Object] in console rather you would see actual objects.
Update :
If you need an array of objects with _id field just add another $project stage at the end, but I would highly suggest to use $reduce and get an array for your scenario :
{ $project: { "openOrders._id": 1 } } // which would just extract `_id` fields in each objects
Test : mongoplayground

Categories

Resources