MongoDB filter recursively nested Array by object field - mongodb

I want to filter a nested object array by a field in mongoDb Compass.
Here is an example collection.
{
"_id": "MENU_ORDER_POS_1",
"menuVersion": "MENU_ORDER",
"menuType": "MAIN",
"status": "ENABLED",
"children": [
{
"_id": "MENU_ORDER_CHILD_1",
"menuVersion": "MENU_ORDER",
"status": "ENABLED",
"children": [
{
"_id": "MENU_ORDER_CHILD_1.1",
"menuVersion": "MENU_ORDER",
"status": "ENABLED",
"children": []
},
{
"_id": "MENU_ORDER_CHILD_1.2",
"menuVersion": "MENU_ORDER",
"status": "DISABLED",
"children": []
}
]
},
{
"_id": "MENU_ORDER_CHILD_2",
"menuVersion": "MENU_ORDER",
"status": "DISABLED",
"children": []
},
{
"_id": "MENU_ORDER_CHILD_3",
"menuVersion": "MENU_ORDER",
"status": "DISABLED",
"children": []
}
]
},
{
"_id": "MENU_ORDER_POS_2",
"menuVersion": "PANEL",
"menuType": "",
"defaultPath": "",
"eventNameToNavigate": "",
"status": "ENABLED",
"children": []
}
Expected output:
{
"_id": "MENU_ORDER_POS_1",
"menuVersion": "MENU_ORDER",
"menuType": "MAIN",
"status": "ENABLED",
"children": [
{
"_id": "MENU_ORDER_CHILD_1",
"menuVersion": "MENU_ORDER",
"status": "ENABLED",
"children": [
{
"_id": "MENU_ORDER_CHILD_1.1",
"menuVersion": "MENU_ORDER",
"status": "ENABLED",
"children": []
}
]
}
]
}
Aggregate stages
First I need to get all the documents with menuVersion = "MENU_ORDER" and status = "ENABLED".
Then I need to filter all nested items in the children Array, note that this array is recursive and can have multiple children object inside an item. I need to get all the children items that matches status = "ENABLED"
Note The children array can have elements with the children object and this can be infinite, it is multilevel and all the items in the nested array must meet the filter criteria
I figure aggregate is the best choice here, using match and filter should do the job but I can't figure how to make it infinite recursive to filter all N child in the array.
So far I have written the filter part.
{
children: {
$filter: {
input: "$children",
as: "children",
cond: {
$eq: "children.status", "ENABLED" }
}
}
}

Your approach of using $filter is correct. You just need to refer the variable you set (i.e. children) using $$. Repeat it again with the inner array.
db.collection.aggregate([
{
$set: {
children: {
"$filter": {
"input": "$children",
"as": "c",
"cond": {
$and: [
{
$eq: [
"$$c.menuVersion",
"MENU_ORDER"
]
},
{
$eq: [
"$$c.status",
"ENABLED"
]
}
]
}
}
}
}
},
{
$set: {
children: {
"$map": {
"input": "$children",
"as": "c",
"in": {
"$mergeObjects": [
"$$c",
{
"children": {
"$filter": {
"input": "$$c.children",
"as": "innerChild",
"cond": {
$eq: [
"$$innerChild.status",
"ENABLED"
]
}
}
}
}
]
}
}
}
}
}
])
Mongo Playground

Related

Merge documents from 2 collections in MongoDB & preserve property of a field

I have two collections, 1. temporaryCollection, 2. permanentCollection, I would like to take data from temporaryCollection and update in permanentCollection. To see the expected result see updatedPermanentCollection below.
Fields that are taken from Temporary collection and updated in Permanent collection are:
emailAddresses
phoneNumbers
ContactName
ContactNumber
For your info, the fields that are changed in Temporary collection
contacts[0]['emailAddresses']
contacts[0]['ContactName']
contacts[0]["phoneNumbers"]
contacts[0]["ContactNumber"]
Field that are that should not be changed after updation in UpdatedPermanentCollection is
contacts._id
Note: contacts is an Array of objects, for simplicity I have shown just one object.
I am currently using the below query which updates the permanentCollection but also overrides the contacts._id field. I don't want the contacts._id field to be overridden.
Here is my MongoDB Query
db.temporaryCollection.aggregate([
{
$match: {
userID: ObjectId("61d1efea2c0fab00340f47c8"),
},
},
{
$merge: {
into: "permanentCollection",
on: "userID",
whenMatched: "merge",
whenNotMatched: "insert",
},
},
]);
1. temporaryCollection
{
"_id": { "$oid": "61d1f04266289f003452d705" },
"userID": { "$oid": "61d1efea2c0fab00340f47c8" },
"contacts": [
{
"emailAddresses": [
{ "id": "6884", "label": "email1", "email": "addedemail#gmail.com" }
],
"phoneNumbers": [
{
"label": "other",
"id": "4594",
"number": "+918984292930"
},
{
"label": "other",
"id": "4595",
"number": "+911234567890"
}
],
"_id": { "$oid": "61d1f04266289f003452d744" },
"ContactName": "Sample User 1 Name Changed",
"ContactNumber": "+918984292930",
"recordID": "833"
}
],
"userNumber": "+911234567890",
"__v": 7
}
2. permanentCollection
{
"_id": { "$oid": "61d1f04266289f003452d701" },
"userID": { "$oid": "61d1efea2c0fab00340f47c8" },
"contacts": [
{
"emailAddresses": [],
"phoneNumbers": [
{
"label": "other",
"id": "4594",
"number": "+918984292929"
},
{
"label": "other",
"id": "4595",
"number": "+911234567890"
}
],
"_id": { "$oid": "61d1f04266289f003452d722" },
"ContactName": "Sample User 1",
"ContactNumber": "+918984292929",
"recordID": "833"
}
],
"userNumber": "+911234567890",
"__v": 7
}
3. updatedPermanentCollection (Expected result)
{
"_id": { "$oid": "61d1f04266289f003452d701" },
"userID": { "$oid": "61d1efea2c0fab00340f47c8" },
"contacts": [
{
"emailAddresses": [
{ "id": "6884", "label": "email1", "email": "addedemail#gmail.com" }
],
"phoneNumbers": [
{
"label": "other",
"id": "4594",
"number": "+918984292930"
},
{
"label": "other",
"id": "4595",
"number": "+911234567890"
}
],
"_id": { "$oid": "61d1f04266289f003452d722" },
"ContactName": "Sample User 1 Name Changed",
"ContactNumber": "+918984292930",
"recordID": "833"
}
],
"userNumber": "+911234567890",
"__v": 7
}
Try with this aggregation query.
db.temporarCollection.aggreagate(
[
{
"$lookup": {
"from": "permanantCollection",
"let": {
"user_id": "$userID"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$$user_id", "$userID"
]
}
}
}
],
"as": "pcontacts"
}
}, {
"$unwind": {
"path": "$pcontacts",
"preserveNullAndEmptyArrays": true
}
}, {
"$project": {
"contacts": {
"$map": {
"input": "$contacts",
"as": "contact",
"in": {
"tcontact": "$$contact",
"pcontact": {
"$first": {
"$filter": {
"input": "$pcontacts.contacts",
"as": "pcontact",
"cond": {
"$eq": [
"$$pcontact.recordID", "$$contact.recordID"
]
}
}
}
}
}
}
},
"userNumber": 1,
"userID": 1,
"_id": 0
}
}, {
"$project": {
"contacts": {
"$map": {
"input": "$contacts",
"as": "contact",
"in": {
"emailAddresses": "$$contact.tcontact.emailAddresses",
"phoneNumbers": "$$contact.tcontact.phoneNumbers",
"ContactName": "$$contact.tcontact.ContactName",
"ContactNumber": "$$contact.tcontact.ContactNumber",
"recordID": {
"$let": {
"vars": {},
"in": {
"$cond": {
"if": "$$contact.pcontact.recordID",
"then": "$$contact.pcontact.recordID",
"else": "$$contact.tcontact.recordID"
}
}
}
},
"_id": {
"$let": {
"vars": {},
"in": {
"$cond": {
"if": "$$contact.pcontact._id",
"then": "$$contact.pcontact._id",
"else": "$$contact.tcontact._id"
}
}
}
}
}
}
},
"userNumber": 1,
"userID": 1
}
}, {
"$merge": {
"into": "pc",
"on": "userID",
"whenMatched": "replace",
"whenNotMatched": "insert"
}
}
])
It is not a fully optimized query but it works.
Try to add $unset to db query.
db.temporaryCollection.aggregate([
{
$unset: "_id"
},
{
$match: {
userID: ObjectId("61d1efea2c0fab00340f47c8"),
},
},
{
$merge: {
into: "permanentCollection",
on: "userID",
whenMatched: "merge",
whenNotMatched: "insert",
},
},
]);

mongodb aggregate nested arrays filter not empty

But now I don't know how to filter
I'm aggregating the filtered dataļ¼š
[
{
"_id": "61cea071cfa3c96b9a4d2657",
"name": "Utils",
"children": [
{
"name": "Code",
"_id": "61cebb4e6c4a5c643494d1a1",
"children": [{name:"jahn"}]
},
{
"name": "Image",
"_id": "61ceb8ad6c4a5c643494d11e",
"children": []
}
]
},
{
"_id": "61cea071cfa3c96b9a4d2111",
"name": "Names",
"children": [
{
"name": "que",
"_id": "61cebb4e6c4a5c643494d1a1",
"children": [
]
},
{
"name": "filter",
"_id": "61cebb4e6c4a5c643494d1a1",
"children": [
{name:"jahn"}
]
}
]
},
]
Looking forward to your help
How to filter out children when children are empty and not displayed
Desired result :
[
{
"_id": "61cea071cfa3c96b9a4d2657",
"name": "Utils",
"children": [
{
"name": "Code",
"_id": "61cebb4e6c4a5c643494d1a1",
"children": [
{name:"jahn"}
]
}
]
},
{
"children": [
{
"children": [
{name:"jahn"}
]
}
]
},
]
I want if children in children, if it's empty it doesn't show the whole object
If you tried to filter for the second level children array, you can use $filter.
db.collection.aggregate([
{
$project: {
_id: 1,
name: 1,
children: {
"$filter": {
"input": "$children",
"cond": {
"$ne": [
"$$this.children",
[]
]
}
}
}
}
}
])
Sample Mongo Playground
Note:
"$ne": [
"$$this.children",
[]
]
Can be replaced with:
"$ne": [
{
$size: "$$this.children"
},
0
]

insert multiple objects into nested arrays with condition

I'm a mongo beginner and struggling to insert multiple objects into multiple nested array in one document.
The document looks like this:
[
{
"id": 1,
"name": "myObject",
"sections": [
{
"id": "section1",
"items": [
{
"id": 1,
"name": "item1.1",
"scores": [
{
"userId": 13,
"score": 10
}
]
},
{
"id": 2,
"name": "item1.2",
"scores": [
{
"userId": 66,
"score": 10
}
]
}
]
},
{
"id": "section2",
"items": [
{
"id": 3,
"name": "item2.1",
"scores": [
{
"userId": 13,
"score": 20
}
]
}
]
}
]
}
]
I now want to insert new scores for userId=10 for every item in every section.
The score is of course different for every item.
let's assume scores like this (all for userId=10)
[
{
"sectionId": "section1"
"itemName: "item1.1"
"score: 10,
"userId": 10
},
{
"sectionId": "section1"
"itemName: "item1.2"
"score: 15,
"userId": 10
},
{
"sectionId": "section2"
"itemName: "item2.1"
"score: 33,
"userId": 10
}
]
Added for clarification
the updated document should look like the following.
{
"id": 1,
"name": "myObject",
"sections": [
{
"id": "section1",
"items": [
{
"id": 1,
"name": "item1.1",
"scores": [
{
"userId": 13,
"score": 10
},
{ // <-- newly added score
"userId": 10,
"score": 10
}
]
},
{
"id": 2,
"name": "item1.2",
"scores": [
{
"userId": 66,
"score": 10
},
{ // <- newly added score
"userId": 10,
"score": 15
}
]
}
]
}
// the remaining document is omitted for brevity but the above should also be applied to this sections
so far I have been able to achieve what I want for one single score like this
db.collection.update({
id: 1
},
{
$push: {
"sections.$[item].items.$[score].scores": {
"userId": 10,
"score": 13
},
}
},
{
arrayFilters: [
{
"score.userId": {
$ne: 10
}
},
{
"item.name": {
$eq: "item1.1"
}
}
]
})
This inserts a score for userId=10 in itemName=item1.1 if no score for userId=10 exists.
But I'm struggling on how to insert multiple scores into multiple items.
I saw that you can merge objects together, so maybe this would be an option although it kinda fells like an overkill.
So how can I insert all my scores for the different items in one atomic operation?
EDIT: Added clarification about the desired result.
Query
pipeline update, requires MongoDB >= 4.2
reduce on the data that you want to insert, with initial value the sections
nested 3 maps that always do the same
if its not the key-value i want, keep the old value
else (merge {:newkey (map ...)})
if userId exists updates its score, else insert new userID and score
query assumes that there is a score array even if empty, i mean it
only creates new userId+score, not sections items etc
*you can avoid the set/unset and use driver variables in all the places where data is used
PlayMongo
db.collection.update({},
[
{
"$set": {
"data": [
{
"sectionId": "section1",
"itemName": "item1.1",
"userId": 10,
"score": 20
},
{
"sectionId": "section1",
"itemName": "item1.1",
"userId": 13,
"score": 30
},
{
"sectionId": "section2",
"itemName": "item2.1",
"score": 33,
"userId": 10
}
]
}
},
{
"$set": {
"sections": {
"$reduce": {
"input": "$data",
"initialValue": "$sections",
"in": {
"$let": {
"vars": {
"data": "$$this"
},
"in": {
"$map": {
"input": "$$value",
"in": {
"$cond": [
{
"$ne": [
"$$section.id",
"$$data.sectionId"
]
},
"$$section",
{
"$mergeObjects": [
"$$section",
{
"items": {
"$map": {
"input": "$$section.items",
"in": {
"$cond": [
{
"$ne": [
"$$item.name",
"$$data.itemName"
]
},
"$$item",
{
"$mergeObjects": [
"$$item",
{
"scores": {
"$let": {
"vars": {
"user_exist": {
"$in": [
"$$data.userId",
"$$item.scores.userId"
]
}
},
"in": {
"$cond": [
{
"$not": [
"$$user_exist"
]
},
{
"$concatArrays": [
"$$item.scores",
[
{
"userId": "$$data.userId",
"score": "$$data.score"
}
]
]
},
{
"$map": {
"input": "$$item.scores",
"in": {
"$cond": [
{
"$ne": [
"$$score.userId",
"$$data.userId"
]
},
"$$score",
{
"$mergeObjects": [
"$$score",
{
"score": "$$data.score"
}
]
}
]
},
"as": "score"
}
}
]
}
}
}
}
]
}
]
},
"as": "item"
}
}
}
]
}
]
},
"as": "section"
}
}
}
}
}
}
}
},
{
"$unset": [
"data"
]
}
])

Get the $size (length) of a nested array and calculate the difference to a stored value on the parent object - using aggregate

Let's consider that I have the following documents (ignoring the _id):
[
{
"Id": "Store1",
"Info": {
"Location": "Store1 Street",
"PhoneNumber": 111
},
"MaxItemsPerShelf": 3,
"Shelf": [
{
"Id": "Shelf1",
"Items": [
{
"Id": "Item1",
"Name": "bananas"
},
{
"Id": "Item2",
"Name": "apples"
},
{
"Id": "Item3",
"Name": "oranges"
}
]
},
{
"Id": "Shelf2",
"Items": [
{
"Id": "Item4",
"Name": "cookies"
},
{
"Id": "Item5",
"Name": "chocolate"
}
]
},
{
"Id": "Shelf3",
"Items": []
}
]
},
{
"Id": "Store3",
"Info": {
"Location": "Store2 Street",
"PhoneNumber": 222
},
"MaxItemsPerShelf": 2,
"Shelf": [
{
"Id": "Shelf4",
"Items": [
{
"Id": "Item6",
"Name": "champoo"
},
{
"Id": "Item7",
"Name": "toothpaste"
}
]
},
{
"Id": "Shelf5",
"Items": [
{
"Id": "Item8",
"Name": "chicken"
}
]
}
]
}
]
Given a specific Shelf.Id I want to get the following result ( Shelf.Id = "Shelf2"):
[{
"Info": {
"Location": "Store1 Street",
"PhoneNumber": 111
},
"ItemsNumber": 2,
"ItemsRemaining": 1
}]
Therefore:
ItemsNumberis the $size of Shelf
and
ItemsRemainingis equal to MaxItemsPerShelf $size of Shelf
also I want to copy the value of the Info to the aggregate output.
How can I accomplish this with aggregate? On my efforts I couldn't pass through an iterator that gets the $size of $Shelf.Items
You can use below aggregation
db.collection.aggregate([
{ "$match": { "Shelf.Id": "Shelf2" }},
{ "$replaceRoot": {
"newRoot": {
"$let": {
"vars": {
"shelf": {
"$filter": {
"input": {
"$map": {
"input": "$Shelf",
"in": {
"Id": "$$this.Id",
"count": { "$size": "$$this.Items" }
}
}
},
"as": "ss",
"cond": { "$eq": ["$$ss.Id", "Shelf2"] }
}
}
},
"in": {
"Info": "$Info",
"ItemsNumber": { "$arrayElemAt": ["$$shelf.count", 0] },
"ItemsRemaining": {
"$subtract": [
"$MaxItemsPerShelf",
{ "$ifNull": [
{ "$arrayElemAt": ["$$shelf.count", 0] },
0
]}
]
}
}
}
}
}}
])

Combining unique elements of arrays without $unwind

I would like to get the unique elements of all arrays in a collection. Consider the following collection
[
{
"collection": "collection",
"myArray": [
{
"name": "ABC",
"code": "AB"
},
{
"name": "DEF",
"code": "DE"
}
]
},
{
"collection": "collection",
"myArray": [
{
"name": "GHI",
"code": "GH"
},
{
"name": "DEF",
"code": "DE"
}
]
}
]
I can achieve this by using $unwind and $group like this:
db.collection.aggregate([
{
$unwind: "$myArray"
},
{
$group: {
_id: null,
data: {
$addToSet: "$myArray"
}
}
}
])
And get the output:
[
{
"_id": null,
"data": [
{
"code": "GH",
"name": "GHI"
},
{
"code": "DE",
"name": "DEF"
},
{
"code": "AB",
"name": "ABC"
}
]
}
]
However, the array "myArray" will have a lot of elements (about 6) and the number of documents passed into this stage of the pipeline will be about 600. So unwinding the array would give me a total of 3600 documents being processed. I would like to know if there's a way for me to achieve the same result without unwinding
You can use below aggregation
db.collection.aggregate([
{ "$group": {
"_id": null,
"data": { "$push": "$myArray" }
}},
{ "$project": {
"data": {
"$reduce": {
"input": "$data",
"initialValue": [],
"in": { "$setUnion": ["$$this", "$$value"] }
}
}
}}
])
Output
[
{
"_id": null,
"data": [
{
"code": "AB",
"name": "ABC"
},
{
"code": "DE",
"name": "DEF"
},
{
"code": "GH",
"name": "GHI"
}
]
}
]