MongoDB - Update a parent array field using another child array field - mongodb

I've a collection like this
db.aa1.insertMany([
{ parentArr: [] },
{ parentArr: [
{childArr: [ {childField: 2}, {childField: 4} ]}
] },
{ parentArr: [
{childArr: []}
] },
{ parentArr: [
{childArr: [ {childField: 3}, {childField: 5} ]}
] },
])
Now I want the end result to be like
[
{ parentArr: [] },
{ parentArr: [ { childArr: [] } ] },
{ parentArr: [
{
childArr: [ {childField: 2}, {childField: 4} ],
parentField: [2, 4]
},
] },
{ parentArr: [
{
childArr: [ {childField: 3}, {childField: 5} ],
parentField: [3, 5]
}
] },
]
Here I've copied the childArr.childField values in the parentArr.parentField.
Now in plain JS, I could do something like this
parentArr.forEach(p => p.parentField = p.childArr ? p.childArr.map(c => c.childField) : [])
How can I achieve this using a MongoDB Query?
I've tried the following $push $set combinations, of course, one at a time.
For the example sake, I've written all push and set together.
db.myCollection.update(
{
"parentArr.childArr.0": {$exists: true}
},
{
$set: {"parentArr.$[].parentField": ["$parentArr.$[].childArr.$[].childField"]}
$set: {"parentArr.parentField": ["$parentArr.childArr.childField"]}
$push: {
"parentArr.$[].parentField": {$each: ["$parentArr.$[].childArr.$[].childField"]}
}
$push: {
"parentArr.parentField": {$each: ["$parentArr.childArr.childField"]}
}
},
{
upsert: true,
multi: true
}
)

If you're using Mongo version 4.2+ they have introduced pipeline'd updates meaning we now have more power when updating:
db.aa1.updateMany(
{
"parentArr.childArr.childField": {$exists: true}
},
[
{
$set: {
"parentArr.parentField": {
$reduce: {
input: {
$map: {
input: "$parentArr",
as: "parent",
in: {
$map: {
input: "$$parent.childArr",
as: "child",
in: "$$child.childField"
}
}
}
},
initialValue: [],
in: {$setUnion: ["$$value", "$$this"]}
}
}
}
}
]
)
If you're on an older Mongo version then you'll have to do it in code, as you already posted a relevant snippet I have no more to add.

Related

group an array into subarrays in a project stage

I want to split the following array according to the group-value. I know I can do this using $unwind and $group. Is there any way to this in a single $project-stage?
Input
{
"_id": 1,
"some_field": "some_value",
"array": [
{
"group": "a",
"subgroup": "aa",
"value": 1
},
{
"group": "b",
"subgroup": "bb",
"value": 2
},
{
"group": "a",
"subgroup": "ab",
"value": 2
}
]
}
desired output:
{
"_id": 1,
"some_field": "some_value",
"array": [
{
"group": "a",
"values": [
{
"subgroup": "aa",
"value": 1
},
{
"subgroup": "ab",
"value": 2
}
]
},
{
"group": "b",
"values": [
{
"subgroup": "bb",
"value": 2
}
]
}
]
}
Try this: https://mongoplayground.net/p/pFn3tLtAG4D
$set: {
_id: "$_id",
some_field: "$some_field",
array: {
$map: {
input: {
$setUnion: [
"$array.group"
]
},
in: {
group: "$$this",
values: {
$map: {
input: {
$filter: {
input: "$array",
as: "elem",
cond: {
$eq: [
"$$elem.group",
"$$this"
]
}
}
},
as: "vals",
in: {
subgroup: "$$vals.subgroup",
value: "$$vals.value"
}
}
}
}
}
}
}
This is far from a single project stage, but it does produce the desired output from the given input.
db.collection.aggregate([
{'$match': {'_id': 1}},
{'$unwind': '$array'},
{'$project': {'array': {'group': '$array.group', 'values': '$array'},
'some_field': 1,
'my_id': '$_id'}},
{'$unset': 'array.values.group'},
{'$group': {'_id': '$array.group',
'values': {'$push': '$array.values'},
'some_field': {'$first': '$some_field'},
'my_id': {'$first': '$my_id'}}},
{'$set': {'array': {'group': '$_id', 'values': '$values'}}},
{'$unset': 'values'},
{'$group': {'_id': '$my_id',
'array': {'$push': '$array'},
'some_field': {'$first': '$some_field'}}}
])
Try it on mongoplayground.net.
It is doable, it's definitely not clean or sexy.
My approach is to use $reduce and $mergeObjects, we'll iterate over the array and keep reconstructing the result.
The main issue that plagues this approach is this feature that doesn't allow to $concatArrays expressions, so we have to use some very ugly workarounds.
Anyways here is how you can achieve this:
db.collection.aggregate([
{
$project: {
_id: 1,
some_field: 1,
array: {
$map: {
input: {
"$objectToArray": {
$reduce: {
input: "$array",
initialValue: {},
in: {
"$mergeObjects": [
"$$value",
{
"$arrayToObject": [
[
{
k: "$$this.group",
v: {
$map: {
input: {
"$concatArrays": [
[
"$$this"
],
{
$map: {
input: {
$filter: {
input: {
"$objectToArray": "$$value"
},
as: "filterItem",
cond: {
$eq: [
"$$filterItem.k",
"$$this.group"
]
}
}
},
as: "mapItem",
in: "$$mapItem.v"
}
},
]
},
as: "map2Item",
in: {
$cond: [
{
"$isArray": "$$map2Item"
},
{
$arrayElemAt: [
"$$map2Item",
0
]
},
"$$map2Item"
]
}
}
}
}
]
]
}
]
}
}
}
},
as: "item",
in: {
group: "$$item.k",
values: "$$item.v"
}
}
}
}
}
])
Mongo Playground

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.

Averaging across list indicies during aggregation pipeline?

I currently have a MongoDB aggregation pipeline that ends with the following type of synthetic documents
[
{
'_id': '2019-09-10',
'grouped_foos':
[
{... 'foo': [1, 78, 100]},
{... 'foo': [8, 66, 98]},
{... 'foo': [99, 5, 33]},
{... 'foo': [120, 32, 2]}
]
},
{
'_id': '2019-09-09',
'grouped_foos':
[
{... 'foo': [10, 27]},
{... 'foo': [19, 66]}
]
},
{
'_id': '2019-09-08',
'grouped_foos':
[
{... 'foo': [1]}
]
}
]
I would like to continue this pipeline and average the indices of the foo lists together to form documents that look like
[
{
'_id': '2019-09-10',
'avg_foo': [57, 45.25, 58.25]
},
{
'_id': '2019-09-09',
'avg_foo': [14.5, 46.5]
},
{
'_id': '2019-09-08',
'avg_foo': [1]
}
]
Is this type of averaging possible during aggregation? Do I potentially need to $unwind the lists with indexing and assign new _id for uniqueness to make documents that look like
[
{
'_id': UUID,
'old_id': '2019-09-10',
'foo': 1,
'index': 0
},
{
'_id': UUID,
'old_id': '2019-09-10',
'foo': 78,
'index': 1
},
........
]
Basically you can try with $unwind but easier and faster approach would be to use $reduce to $map and $sum all the rows from grouped_foos. Then you'll be able to run another $map and use $divide to get the average.
db.collection.aggregate([
{
$project: {
size: { $size: "$grouped_foos" },
foo_sum: {
$reduce: {
input: "$grouped_foos",
initialValue: [],
in: {
$map: {
input: { $range: [ 0, { $size: "$$this.foo" }, 1 ] },
as: "index",
in: {
$add: [
{ $arrayElemAt: [ "$$this.foo", "$$index" ] },
{ $ifNull: [ { $arrayElemAt: [ "$$value", "$$index" ] }, 0 ] }
]
}
}
}
}
}
}
},
{
$project: {
_id: 1,
avg_foo: {
$map: {
input: "$foo_sum",
in: {
$divide: [ "$$this", "$size" ]
}
}
}
}
}
])
Mongo Playground

MongoDB. Aggregate the sum of two arrays sizes

With MongoDB 3.4.10 and mongoose 4.13.6 I'm able to count sizes of two arrays on the User model:
User.aggregate()
.project({
'_id': 1,
'leftVotesCount': { '$size': '$leftVoted' },
'rightVotesCount': { '$size': '$rightVoted' }
})
where my Users are (per db.users.find())
{ "_id" : ObjectId("5a2b21e63023c6117085c240"), "rightVoted" : [ 2 ],
"leftVoted" : [ 1, 6 ] }
{ "_id" : ObjectId("5a2c0d68efde3416bc8b7020"), "rightVoted" : [ 2 ],
"leftVoted" : [ 1 ] }
Here I'm getting expected result:
[ { _id: '5a2b21e63023c6117085c240', leftVotesCount: 2, rightVotesCount: 1 },
{ _id: '5a2c0d68efde3416bc8b7020', leftVotesCount: 1, rightVotesCount: 1 } ]
Question. How can I get a cumulative value of leftVotesCount and rightVotesCount data? I tried folowing:
User.aggregate()
.project({
'_id': 1,
'leftVotesCount': { '$size': '$leftVoted' },
'rightVotesCount': { '$size': '$rightVoted' },
'votesCount': { '$add': [ '$leftVotesCount', '$rightVotesCount' ] },
'votesCount2': { '$sum': [ '$leftVotesCount', '$rightVotesCount' ] }
})
But votesCount is null and votesCount2 is 0 for both users. I'm expecting votesCount = 3 for User 1 and votesCount = 2 for User 2.
$leftVotesCount, $rightVotesCount become available only on the next stage. Try something like:
User.aggregate()
.project({
'_id': 1,
'leftVotesCount': { '$size': '$leftVoted' },
'rightVotesCount': { '$size': '$rightVoted' }
})
.project({
'_id': 1,
'leftVotesCount': 1,
'rightVotesCount': 1
'votesCount': { '$add': [ '$leftVotesCount', '$rightVotesCount' ] },
'votesCount2': { '$sum': [ '$leftVotesCount', '$rightVotesCount' ] }
})
You can't reference the project variables created in the same project stage.
You can wrap the variables in a $let expression.
User.aggregate().project({
"$let": {
"vars": {
"leftVotesCount": {
"$size": "$leftVoted"
},
"rightVotesCount": {
"$size": "$rightVoted"
}
},
"in": {
"votesCount": {
"$add": [
"$$leftVotesCount",
"$$rightVotesCount"
]
},
"leftVotesCount": "$$leftVotesCount",
"rightVotesCount": "$$rightVotesCount"
}
}
})
It turned out that $add supports nested expressions, so I was able to solve the issue by excluding intermediate variables:
User.aggregate().project({
'_id': 1,
'votesCount': { '$add': [ { '$size': '$leftVoted' }, { '$size': '$rightVoted' } ] }
});
// [ {_id: '...', votesCount: 3}, {_id: '...', votesCount: 2} ]

Mongodb array $push and $pull

I was looking to pull some value from array and simultaneously trying to update it.
userSchema.statics.experience = function (id,xper,delet,callback) {
var update = {
$pull:{
'profile.experience' : delet
},
$push: {
'profile.experience': xper
}
};
this.findByIdAndUpdate(id,update,{ 'new': true},function(err,doc) {
if (err) {
callback(err);
} else if(doc){
callback(null,doc);
}
});
};
i was getting error like:
MongoError: exception: Cannot update 'profile.experience' and 'profile.experience' at the same time
I found this explanation:
The issue is that MongoDB doesn’t allow multiple operations on the
same property in the same update call. This means that the two
operations must happen in two individually atomic operations.
And you can read that posts:
Pull and addtoset at the same time with mongo
multiple mongo update operator in a single statement?
In case you need replace one array value to another, you can use arrayFilters for update.
(at least, present in mongo 4.2.1).
db.your_collection.update(
{ "_id": ObjectId("your_24_byte_length_id") },
{ "$set": { "profile.experience.$[elem]": "new_value" } },
{ "arrayFilters": [ { "elem": { "$eq": "old_value" } } ], "multi": true }
)
This will replace all "old_value" array elements with "new_value".
Starting from MongoDB 4.2
You can try to update the array using an aggregation pipeline.
this.updateOne(
{ _id: id },
[
{
$set: {
"profile.experience": {
$concatArrays: [
{
$filter: {
input: "$profile.experience",
cond: { $ne: ["$$this", delet] },
},
},
[xper],
],
},
},
},
]
);
Following, a mongoplayground doing the work:
https://mongoplayground.net/p/m1C1LnHc0Ge
OBS: With mongo regular update query it is not possible.
Since Mongo 4.2 findAndModify supports aggregation pipeline which will allow atomically moving elements between arrays within the same document. findAndModify also allows you to return the modified document (necessary to see which array elements were actually moved around).
The following includes examples of:
moving all elements from one array onto the end of a different array
"pop" one element of an array and "push" it to another array
To run the examples, you will need the following data:
db.test.insertMany( [
{
"_id": ObjectId("6d792d6a756963792d696441"),
"A": [ "8", "9" ],
"B": [ "7" ]
},
{
"_id": ObjectId("6d792d6a756963792d696442"),
"A": [ "1", "2", "3", "4" ],
"B": [ ]
}
]);
Example 1 - Empty array A by moving it into array B:
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696441") },
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, "$A" ] } } },
{ $set: { "A": [] } }
],
new: true
});
Resulting in:
{
"_id": {
"$oid": "6d792d6a756963792d696441"
},
"A": [],
"B": [
"7",
"8",
"9"
]
}
Example 2.a - Pop element from array A and push it onto array B
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"A": {$exists: true, $type: "array", $ne: [] }},
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, [ { $first: "$A" } ] ] } } },
{ $set: { "A": { $slice: ["$A", 1, {$max: [{$subtract: [{ $size: "$A"}, 1]}, 1]}] } }}
],
new: true
});
Resulting in:
{
"_id": {
"$oid": "6d792d6a756963792d696442"
},
"A": [
"2",
"3",
"4"
],
"B": [
"1"
]
}
Example 2.b - Pop element from array A and push it onto array B but in two steps with a temporary placeholder:
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"temp": { $exists: false } },
update: [
{ $set: { "temp": { $first: "$A" } } },
{ $set: { "A": { $slice: ["$A", 1, {$max: [{$subtract: [{ $size: "$A"}, 1]}, 1]}] } }}
],
new: true
});
// do what you need to do with "temp"
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"temp": { $exists: true } },
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, [ "$temp" ] ] } } },
{ $unset: "temp" }
],
new: true
});