Efficient way to modify an object from array of objects via index and insert new objects in the same array in mongodb? - mongodb

I have an array available in the db with the key heros.
Heros: [
{
name: 'Ironman',
country: 'USA'
},
{
name: 'Shaktiman',
country: 'India'
},
{
name: 'Black Panther',
country: 'Wakanda'
}
]
and I have a new array which basically have additional heros:
additionalHeros = [{ name: 'Wonder woman', country: 'USA' }, { name: 'Kenshiro', country: 'Japan' }]
I want to update the name and country of a hero at a specific index say 1 and replace it with new values. After that I have to put all the additional heros in the same query.
After executing update query, expected result would be:
At index 2 -> update name: 'Krishh', country: 'India' -> put rest of the heros
Updated db value for heros is:
Heros: [
{
name: 'Ironman',
country: 'USA'
},
{
name: 'Shaktiman',
country: 'India'
},
{
name: 'Krishh',
country: 'India'
},
{ name: 'Wonder woman',
country: 'USA'
},
{ name: 'Kenshiro',
country: 'Japan'
}
]
I'm aware of the part to push a value but I can't find any efficient way to update and insert at once
$push: { heros: { $each: additionalHeros } }

The regular update query will not allow to do 2 operations in the same field, it will create a conflict error
You can use update with aggregation pipeline starting from MongoDB 4.2, but i think this approach is not efficient and you want an efficient approach,
$map to iterate loop of heros array
$indexOfArray to get the index of the current object in heros array
$cond to check if the above return index match with our input index then update the latest value otherwise return the same object
second $set stage to add new heros using $concatArrays operator
additionalHeros = [{ name: 'Wonder woman', country: 'USA' }, { name: 'Kenshiro', country: 'Japan' }]
updateIndex = {
name: "Krishh",
country: "India"
}
index = 1
db.collection.updateOne(
{}, // your query
[{
$set: {
heros: {
$map: {
input: "$heros",
in: {
$cond: [
{
$eq: [
{
$indexOfArray: [
"$heros",
{
name: "$$this.name",
country: "$$this.country"
}
]
},
index
]
},
updateIndex,
"$$this"
]
}
}
}
}
},
{
$set: {
heros: {
$concatArrays: ["$heros", additionalHeros]
}
}
}]
)
Playground
You can do two separate update queries or bulkWrite query
update specific index property
db.collection.updateOne(
{}, // your query
{
$set: {
"heros.1.name": "Krishh",
"heros.1.country": "India"
}
}
)
Playground
push array in heros
db.collection.updateOne(
{}, // your query
{
$push: {
heros: {
$each: [
{
name: "Wonder woman",
country: "USA"
},
{
name: "Kenshiro",
country: "Japan"
}
]
}
}
}
)
Playground

this will work
let document be
{
"_id" : "heroes",
"Heros" : [
{
"name" : "Ironman",
"country" : "USA"
},
{
"name" : "Shaktiman",
"country" : "India"
},
{
"name" : "Black Panther",
"country" : "Wakanda"
}
]
}
Code would be
let additionalHeros = [{ name: 'Wonder woman', country: 'USA' }, { name: 'Kenshiro', country: 'Japan' }]
let index = 2;
let data = { name: 'Krishh', country: 'India' }
let updateQuery = {};
updateQuery[`Heros.${index}`] = data;
let ops = [];
ops.push(
{
updateOne: {
filter: { _id: "heroes" },
update: {
$set: updateQuery,
},
upsert: true
}
},
);
ops.push(
{
updateOne: {
filter: { _id: "heroes" },
update: {
$push: { Heros: { $each: additionalHeros } }
},
upsert: true
}
},
);
let x = await db1.bulkWrite(ops, { ordered: true, upsert: true })
Then Output would be
{
"_id" : "heroes",
"Heros" : [
{
"name" : "Ironman",
"country" : "USA"
},
{
"name" : "Shaktiman",
"country" : "India"
},
{
"name" : "Krishh",
"country" : "India"
},
{
"name" : "Wonder woman",
"country" : "USA"
},
{
"name" : "Kenshiro",
"country" : "Japan"
}
]
}

Related

MongoDB returns only the specified query

db.customerOrder.insert({
firstName: "Andrew",
lastName: "Lee",
DOB: ISODate("1974-10-28T00:00:00Z"),
phone: "+1 (959) 567-3312",
email: "mark#gmail.com",
address: {
street: "Cornish Street, Victoria",
houseNumber: "68",
postalCode: "3024",
country: "Australia",
},
language: ["English", "Mandarin"],
balance: 0,
orders: [
{
orderNumber: "ord003",
orderDate: ISODate("2020-01-10T00:00:00Z"),
staffNumber: "stf789"
}
]
});
Given the document above, and other documents which contain other orders and order number, how do i specify an aggregation so that it will only list all orderNumbers that's handled by a staffNumber x?
Example, orderNumber ord004 and ord005 is handled by staffNumber stf890
I tried doing
db.customerOrder.aggregate([ {"$match":{"orders.staffNumber":"stf890"}}, {"$project":{"orders.orderNumber":1, "_id":0}} ])
but the result was
{
"orders" : [
{
"orderNumber" : "ord003"
},
{
"orderNumber" : "ord003"
},
{
"orderNumber" : "ord005"
}
]
}
{
"orders" : [
{
"orderNumber" : "ord001"
},
{
"orderNumber" : "ord005"
}
]
}
{
"orders" : [
{
"orderNumber" : "ord003"
},
{
"orderNumber" : "ord004"
}
]
}
I expect the result to output only ord004 and ord005
How do i achieve this?
Thank you for your help
Try this! your query is almost correct but you're missing the case of matching orderNumber.
db.customerOrder.aggregate([
{
"$match":{
"orders.staffNumber":"stf890"
}
},
{
$unwind:{
"path":"$orders"
}
},
{
"$match":{
"orders.orderNumber":{$in:["ord004","ord005"]}
}
},
{
"$project":{
"orders.orderNumber":1,
"_id":0
}
}
])
If you don't care about the structure you can just $unwind and then match. otherwise you need to use something like $filter
Option 1:
db.customerOrder.aggregate([
{
"$match": {
"orders.staffNumber": "stf890"
}
},
{
"$unwind": "$orders"
},
{
"$match": {
"orders.staffNumber": "stf890"
}
},
{
"$project": {"orders.orderNumber": 1, "_id": 0}
}])
Option 2:
db.customerOrder.aggregate([
{
"$match": {
"orders.staffNumber": "stf890"
}
},
{
$project: {
orders: {
$filter: {
input: "$orders",
as: "order",
cond: {
$eq: ["$$order.staffNumber", "stf890"]
}
}
}
}
},
{
"$project": {
"orders.orderNumber": 1,
"_id": 0
}
}
])

Merge documents from 2 collections in MongoDB & overwrite properties on a field

I have 2 collections in MongoDB :
Collection1 :
{
_id:1,
Field1: "Some info",
Field2: "Some other info",
Elements: [
{
id: 0,
Enabled: false
},
{
id: 1,
Enabled: false
},
{
id: 2,
Enabled: false
}
]
}
Collection2 :
{
Identifier: "identifier",
ElementsOverride: [
{
id: 0,
Enabled: true
},
{
id: 1,
Enabled: false
},
{
id: 2,
Enabled: true
}
]
}
What I would like to do is perform an operation which flattens "Element" collection and returns Collection1 with the flattened Element collection (basically the Enabled field from collection 2 overwrites the enabled field of Collection 1.
Is there a way to achieve this in Mongodb?
Adding more clarification for what the output should be like:
Essentially what I'm trying to do is merge the document identified by _id:1 in collection 1 (document1), with the document identified by Identifier: "identifier" in collection 2 (document 2) such that:
All the properties in document1 and document2 are available in the output.
The ElementsOverride from document2 with the same ID's as document1 (ex; id: 0) will overwrite the values in document1
Required Output :
{
_id:1,
Identifier: "identifier",
Field1: "Some info",
Field2: "Some other info",
Elements: [
{
id: 0,
Enabled: true
},
{
id: 1,
Enabled: false
},
{
id: 2,
Enabled: true
}
]
}
You can try below query :
db.Collection1.aggregate([
/** get only one required doc from Collection1 */
{ $match: { _id: 1 } },
/** Join relative doc from Collection2 */
{
$lookup:
{
from: "Collection2",
pipeline: [
{
$match:
{
$expr:
{ $eq: ["$Identifier", "identifier"] }
}
}
],
as: "data"
}
},
/** As lookup will default to an array of objects getting an object out of array */
{ $unwind: '$data' },
/** Replacing existing elements field of Collection1 & adding Identifier field to existing doc */
{
$addFields: {
Identifier: '$data.Identifier', Elements:
{
$reduce: {
input: { $reverseArray: { $setUnion: ["$Elements", "$data.ElementsOverride"] } },
initialValue: [],
in: { $concatArrays: ["$$value", { $cond: [{ $in: ['$$this.id', '$$value.id'] }, [], ['$$this']] }] }
}
}
}
},
/** removing unnecessary field created at lookup stage */
{ $project: { data: 0 } }
])
Test : MongoDB-Playground
I am not sure how you want the output.
flattens "Element" collection
generally means the array Element is unwound. Please correct my interpretation, in case I have misunderstood.
But, the following steps in Mongo Shell will get the result:
arr1 = db.c1.aggregate( [ { $unwind: "$Elements" }, { $sort: { "Elements.id": 1 } ] ).toArray()
arr2 = db.c2.aggregate( [ { $unwind: "$ElementsOverride" }, { $sort: { "ElementsOverride.id": 1 } ] ).toArray()
for (let i=0; i < arr1.length; i++) {
updated = Object.assign(arr1[i].Elements, arr2[i].ElementsOverride);
arr1[i].Elements = updated
}
The variable arr1 will have:
[
{
"_id" : 1,
"Field1" : "Some info",
"Field2" : "Some other info",
"Elements" : {
"id" : 0,
"Enabled" : true
}
},
{
"_id" : 1,
"Field1" : "Some info",
"Field2" : "Some other info",
"Elements" : {
"id" : 1,
"Enabled" : false
}
},
{
"_id" : 1,
"Field1" : "Some info",
"Field2" : "Some other info",
"Elements" : {
"id" : 2,
"Enabled" : true
}
}
]
[ EDIT ADD ]
Updated to reflect the required output:
arr2 = db.c2.aggregate( [
{ $unwind: "$ElementsOverride" },
{ $replaceRoot: { newRoot: "$ElementsOverride" } }
] ).toArray()
db.c1.aggregate( [
{ $unwind: "$Elements" },
{ $addFields: {
"Elements.Enabled": {
$filter: {
input: arr2,
cond: { $eq: [ "$$this.ElementsOverride.id", "$Elements.id" ] }
}
}
} },
{ $group: {
_id: "$_id",
doc: { $first: "$$ROOT"},
Identifier: { $first: "$Elements.Enabled.Identifier"},
Elements: { $push: { $arrayElemAt: [ "$Elements.Enabled", 0 ] } }
} },
{ $addFields: {
"doc.Elements": "$Elements.ElementsOverride",
"doc.Identifier": { $arrayElemAt: [ "$Identifier", 0 ] }
} },
{ $replaceRoot: { newRoot: "$doc" } }
] )
[ EDIT ADD 2 ]
Here is another way of merging the documents:
doc1 = db.c1.findOne()
arr2 = db.c2.aggregate( [ { $unwind: "$ElementsOverride" } ] ).toArray()
for (let e2 of arr2) {
for (i = 0; i < doc1.Elements.length; i++) {
if (doc1.Elements[i].id == e2.ElementsOverride.id) {
doc1.Elements[i].Enabled = e2.ElementsOverride.Enabled
doc1.Identifier = e2.Identifier
}
}
}
The output is the doc1 document.

How to filter a sub array but keep the root content?

Using mongo 3.6
I have a top level object that has attributes that contain array of content.
{
firstName: "first1",
lastName: "last1",
phones: [
{
name: "home",
number: "1800"
},
{
name: "work",
number: "1888"
}
]
}
and I want to only return 'work' phone numbers, but maintain the root content.
expected result would be :
{
firstName: "first1",
lastName: "last1",
phones: [
{
name: "work",
number: "1888"
}
]
}
The use case is return patients that are on specific medications, but only return those medications and not the complete medication lists.
so I tried this:
{
$project: {
phones: {
$filter: {
input: "$phones",
as: "phones",
cond: {
$eq: [
"$$phones.name",
"work"
]
}
}
}
}
}
I have to do this often to remove elements of sub arrays so any help would be appreciated.
The following query can get us the expected output:
db.collection.aggregate([
{
$addFields:{
"phones":{
$filter:{
"input":"$phones",
"as":"phone",
"cond":{
$eq:["$$phone.name","work"]
}
}
}
}
}
]).pretty()
Output:
{
"_id" : ObjectId("5d56d6c632ac518eee84d462"),
"firstName" : "first1",
"lastName" : "last1",
"phones" : [
{
"name" : "work",
"number" : "1888"
}
]
}

Mongodb $project: $filter sub-array

There is an items (mongoose) schema that looks like this (simplified to what it matters to the question):
{
brand: {
name: String,
},
title: String,
description: [{ lang: String, text: String }],
shortDescription: [{ lang: String, text: String }],
variants: {
cnt: Number,
attrs: [
{
displayType: String,
displayContent: String,
displayName: [{ lang: String, text: String }],
name: String,
},
],
}
}
I'm trying to filter the items by language, so I've constructed the following query:
db.items.aggregate([
{ $match: { 'description.lang': 'ca', 'shortDescription.lang': 'ca' } },
{ $project: {
'brand.name': 1,
title: 1,
description: {
'$filter': {
input: '$description',
as: 'description',
cond: { $eq: ['$$description.lang', 'ca'] }
}
},
shortDescription: {
'$filter': {
input: '$shortDescription',
as: 'shortDescription',
cond: { $eq: ['$$shortDescription.lang', 'ca'] }
}
},
'variants.cnt': 1,
'variants.attrs': 1
} }
])
And it works as expected: it filters description and shortDescription by language. Right now I'm wondering if it could be possible to filter every variants.attrs.$.displayName as well. Is there any way to do it?
I've been trying to $unwind variant.attrs but I get completly lost when trying to $group again and I'm not really sure if this is the best way...
You are nearly there. Try these steps:
use $unwind stage before $project stage to expand the outer array of documents, i.e. variants.attrs
Add filter for the sub array variants.attrs.displayName in the $project stage.
You will have to project all the sub fields of variants key.
Next add $group stage and group by all the elements except the sub-array. Use $push to rebuild the sub array in group by stage.
Lastly, add $project stage to rebuild the document to its original structure.
db.items.aggregate([
{ $match: { 'description.lang': 'ca', 'shortDescription.lang': 'ca' } },
{ $unwind : "$variants.attrs" },
{ $project: {
'_id' : 1,
'brand.name': 1,
title: 1,
description: {
'$filter': {
input: '$description',
as: 'description',
cond: { $eq: ['$$description.lang', 'ca'] }
}
},
shortDescription: {
'$filter': {
input: '$shortDescription',
as: 'shortDescription',
cond: { $eq: ['$$shortDescription.lang', 'ca'] }
}
},
'variants.attrs.displayName' : {
'$filter' : {
input: '$variants.attrs.displayName',
as: 'variants_attrs_displayName',
cond: { $eq : ['$$variants_attrs_displayName.lang','ca']}
}
},
'variants.cnt': 1,
'variants.attrs.displayType': 1,
'variants.attrs.displayContent' : 1,
'variants.attrs.name' : 1
}
} , { $group:
{
_id : {
_id: "$_id",
title: "$title",
brand:"$brand",
description:"$description",
shortDescription:"$shortDescription",
variants_cnt : "$variants.cnt"
},
variants_attrs : { $push :
{
displayType : "$variants.attrs.displayType",
displayContent : "$variants.attrs.displayContent",
displayName : "$variants.attrs.displayName",
name: "$variants.attrs.name"
}
}
}
},
{ $project :
{
"_id" : 0,
brand : "$_id.brand",
title : "$_id.title",
description : "$_id.description",
shortDescription : "$_id.shortDescription",
variants : {
cnt : "$_id.variants_cnt" ,
attrs : "$variants_attrs"
}
}
}
])
Depending on your use case, you should reconsider your data model design to avoid duplication of filter values. i.e.
'description.lang': 'ca', 'shortDescription.lang': 'ca', 'variants.attrs.displayName.lang': 'ca'

Mongo DB - Second Level Search - elemMatch

I am trying to fetch all records (and count of all records) for a structure like the following,
{
id: 1,
level1: {
level2:
[
{
field1:value1;
},
{
field1:value1;
},
]
}
},
{
id: 2,
level1: {
level2:
[
{
field1:null;
},
{
field1:value1;
},
]
}
}
My requirement is to fetch the number of records that have field1 populated (atleast one in level2). I need to say fetch all the ids or the number of such ids.
The query I am using is,
db.table.find({},
{
_id = id,
value: {
$elemMatch: {'level1.level2.field1':{$exists: true}}
}
}
})
Please suggest.
EDIT1:
This is the question I was trying to ask in the comment. I was unable to elucidate in the comment properly. Hence, editing the question.
{
id: 1,
level1: {
level2:
[
{
field1:value1;
},
{
field1:value1;
},
]
}
},
{
id: 2,
level1: {
level2:
[
{
field1:value2;
},
{
field1:value2;
},
{
field1:value2;
}
]
}
}
{
id: 3,
level1: {
level2:
[
{
field1:value1;
},
{
field1:value1;
},
]
}
}
The query we used results in
value1: 4
value2: 3
I want something like
value1: 2 // Once each for documents 1 & 3
value2: 1 // Once for document 2
You can do that with the following find query:
db.table.find({ "level1.level2" : { $elemMatch: { field1 : {$exists: true} } } }, {})
This will return all documents that have a field1 in the "level1.level2" structure.
For your question in the comment, you can use the following aggregation to "I had to return a grouping (and the corresponding count) for the values in field1":
db.table.aggregate(
[
{
$unwind: "$level1.level2"
},
{
$match: { "level1.level2.field1" : { $exists: true } }
},
{
$group: {
_id : "$level1.level2.field1",
count : {$sum : 1}
}
}
]
UPDATE: For your question "'value1 - 2` At level2, for a document, assume all values will be the same for field1.".
I hope i understand your question correctly, instead of grouping only on the value of field1, i added the document _id as an xtra grouping:
db.table.aggregate(
[
{
$unwind: "$level1.level2"
},
{
$match: {
"level1.level2.field1" : { $exists: true }
}
},
{
$group: {
_id : { id : "$_id", field1: "$level1.level2.field1" },
count : {$sum : 1}
}
}
]
);
UPDATE2:
I altered the aggregation and added a extra grouping, the aggregation below gives you the results you want.
db.table.aggregate(
[
{
$unwind: "$level1.level2"
},
{
$match: {
"level1.level2.field1" : { $exists: true }
}
},
{
$group: {
_id : { id : "$_id", field1: "$level1.level2.field1" }
}
},
{
$group: {
_id : { id : "$_id.field1"},
count : { $sum : 1}
}
}
]
);