insert multiple objects into nested arrays with condition - mongodb

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"
]
}
])

Related

merge two fields in mongodb

I used unwind aggregation but I am with an error
I need output like. the remaining data is also in the same format
[
{
"_id": "767",
"customId": [
"growth",
"fine"
],
"length": [
1526,
95
]
},
{
"_id": "66",
"customId": [
"height",
"good"
],
"length": [
86,
68
]
}
]
I used unwind aggregation but I am with an error
I need output like. the remaining data is also in the same format
[
{
"_id": "767",
"merged": [
{
"customId": "growth",
"length": 1526
},
{
"customId": "fine",
"length": 95
}
]
},
{
"_id": "66",
"merged": [
{
"customId": "height",
"length": 86
},
{
"customId": "good",
"length": 68
}
]
}
]
I am using postman to know whether it is working or not
You can use $zip to construct pairs from the 2 arrays and use $arrayElemAt to wrangle to your expected output.
db.collection.aggregate([
{
"$project": {
merged: {
"$zip": {
"inputs": [
"$customId",
"$length"
]
}
}
}
},
{
"$project": {
merged: {
"$map": {
"input": "$merged",
"as": "m",
"in": {
"customId": {
"$arrayElemAt": [
"$$m",
0
]
},
"length": {
"$arrayElemAt": [
"$$m",
1
]
}
}
}
}
}
}
])
Here is the Mongo Playground for your reference.

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",
},
},
]);

Get all matching subdocuments and add property from parent document

Let's say i have following documents:
[
{
"key": 1,
"sub": [
{
"id": 4,
"value": 23
},
{
"id": 1,
"value": 24
}
]
},
{
"key": 2,
"sub": [
{
"id": 1,
"value": 92
},
{
"id": 2,
"value": 93
}
]
},
{
"key": 4,
"sub": [
{
"id": 3,
"value": 22
},
{
"id": 2,
"value": 43
}
]
}
]
I now want to find subdocuments by their id and also see the corresponding parent property key. I have tried following query:
db.collection.aggregate([
{
"$match": {
"sub.id": 1
}
},
{
"$addFields": {
"value": {
"$filter": {
"input": "$sub",
"cond": {
$eq: [
"$$this.id",
1
]
}
}
}
}
},
{
"$project": {
sub: 0
}
}
])
This essentially returns the right information:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"key": 1,
"value": [
{
"id": 1,
"value": 24
}
]
},
{
"_id": ObjectId("5a934e000102030405000001"),
"key": 2,
"value": [
{
"id": 1,
"value": 92
}
]
}
]
But it is not formatted how I need it and also adding more properties from the subdocuments is annoying because of the $addFields. I would rather have it formatted like this:
[
{
"id": 1,
"value": 24,
"key": 1
},
{
"id": 1,
"value": 92,
"key": 2
}
]
So I can have just an array of the matching subdocuments with additional parent properties added.
How would I do that?
Mongo Playground
After you have filtered out the unwanted subdocuments, $reduce over the response to build the subdocuments you want to see:
{"$project": {
_id: 0,
"sub": {
$reduce: {
input: {
"$filter": {
"input": "$sub",
"cond": {$eq: ["$$this.id", 1]}
}
},
initialValue: [],
in: {
$concatArrays: [
[{
key: "$key",
id: "$$this.id",
value: "$$this.value"
}],
"$$value"
]
}
}
}
}
}
Playground

MongoDB count values of multible nested documents

My data looks something like that:
[
{
"_id": 1,
"members": [
{
"id": 1,
"name": "name_1",
"assigned_tasks": [
1,
2,
3
]
},
{
"id": 1,
"name": "name_2",
"assigned_tasks": [
1
]
}
],
"tasks": [
{
"id": 1,
"name": "task_1",
},
{
"id": 2,
"name": "task_2",
},
{
"id": 3,
"name": "task_3",
}
]
}
]
I have a collection that represents a "class" which contains a list of members and a list of projects.
Each member can be assigned to multiple projects.
I wanna be able to count the number of members assigned to each of the tasks in the results and add it as a new field like:
[
{
"_id": 1,
"members": [
{
"id": 1,
"name": "name_1",
"assigned_tasks": [
1,
2,
3
]
},
{
"id": 1,
"name": "name_2",
"assigned_tasks": [
1
]
}
],
"tasks": [
{
"id": 1,
"name": "task_1",
"number_of_assigned_members":2
},
{
"id": 2,
"name": "task_2",
"number_of_assigned_members":1
},
{
"id": 3,
"name": "task_3",
"number_of_assigned_members":2
}
]
}
]
How can I create that query?
You can use $map and than $reduce,
$map tasks through object by object check in $reduce on members, if assigned_tasks is available or not, if available then add 1 otherwise 0,
db.collection.aggregate([
{
$addFields: {
tasks: {
$map: {
input: "$tasks",
as: "t",
in: {
$mergeObjects: [
"$$t",
{
number_of_assigned_members: {
$reduce: {
input: "$members",
initialValue: 0,
in: {
$cond: [
{ $in: ["$$t.id", "$$this.assigned_tasks"] },
{ $add: ["$$value", 1] },
"$$value"
]
}
}
}
}
]
}
}
}
}
}
])
Playground

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
]}
]
}
}
}
}
}}
])