Update more than one inner document in MongoDb - mongodb

In my example project, I have employees under manager. Db schema is like this;
{
"employees": [
{
"name": "Adam",
"_id": "5ea36b27d7ae560845afb88e",
"bananas": "allowed"
},
{
"name": "Smith",
"_id": "5ea36b27d7ae560845afb88f",
"bananas": "not-allowed"
},
{
"name": "John",
"_id": "5ea36b27d7ae560845afb88g",
"bananas": "not-allowed"
},
{
"name": "Patrick",
"_id": "5ea36b27d7ae560845afb88h",
"bananas": "allowed"
}
]
}
In this case Adam is allowed to eat bananas and Smith is not. If I have to give the permission of eating bananas from Adam to Smith I need to perform update operation twice like this:
db.managers.update(
{ 'employees.name': 'Adam' },
{ $set: { 'employees.$.bananas': 'not-allowed' } }
);
and
db.managers.update(
{ 'employees.name': 'Smith' },
{ $set: { 'employees.$.bananas': 'allowed' } }
);
Is it possible to handle this in a single query?

You can use $map and $cond to perform conditional update to the array entries depending on the name of the employee. A $switch is used for potential extension of cases.
db.collection.update({},
[
{
"$set": {
"employees": {
"$map": {
"input": "$employees",
"as": "e",
"in": {
"$switch": {
"branches": [
{
"case": {
$eq: [
"$$e.name",
"Adam"
]
},
"then": {
"$mergeObjects": [
"$$e",
{
"bananas": "not-allowed"
}
]
}
},
{
"case": {
$eq: [
"$$e.name",
"Smith"
]
},
"then": {
"$mergeObjects": [
"$$e",
{
"bananas": "allowed"
}
]
}
}
],
default: "$$e"
}
}
}
}
}
}
])
Mongo Playground

db.managers.update(
{
$or: [
{"employees.name": "Adam"},
{"employees.name": "Smith"}
]
},
{
$set: {
"employees.$[e].bananas": {
$cond: [{ $eq: ["$e.name", "Adam"] }, "not-allowed", "allowed"]
}
}
},
{
arrayFilters: [{ "e.name": { $in: ["Adam", "Smith"] } }]
}
)

Related

How to compare a field to a field of a array in mongodb

I have a documents like below. I want to retrieve all documents whose address.city == "newyork" and address.id == active.
[
{
"name": "star1",
"active": 1,
"address": [
{
"id": 1,
"city": "newyork"
},
{
"id": 2,
"city": "sydney"
}
]
},
{
"name": "star2",
"active": 2,
"address": [
{
"id": 1,
"city": "newyork"
},
{
"id": 2,
"city": "london"
}
]
}
]
I have written below query and it Partially works, But It is not returning complete document. I can't use unwind. Do we have any solution without using "unwind". Is it possible to solve a problem only with $match
db.collection.aggregate([
{
$unwind: "$address"
},
{
$match: {
$expr: {
$eq: [
"$active",
"$address.id"
]
},
"address.city": "newyork"
}
}
])
Maybe something like this:
db.collection.aggregate([
{
"$addFields": {
"address": {
"$filter": {
"input": "$address",
"as": "a",
"cond": {
$and: [
{
$eq: [
"$$a.id",
"$active"
]
},
{
$eq: [
"$$a.city",
"newyork"
]
}
]
}
}
}
}
},
{
$match: {
address: {
$ne: []
}
}
}
])
Explained:
Use addFields/filter to match only matching documents in the array.
Remove the documents with empty address from the array for the cases where no subdocuments is found.
Playground
In case you need to match the whole document containing at least one entry having {address.id==active and address.city==newyork } here is an option:
db.collection.aggregate([
{
$match: {
$expr: {
"$in": [
{
id: "$active",
city: "newyork"
},
"$address"
]
}
}
}
])
Explained:
Match only documents having at least one object in address array with id==$active and city=="newyork"
Playground
In case we expect different order inside the address objects , the more correct option is as follow:
db.collection.aggregate([
{
$match: {
$expr: {
$or: [
{
"$in": [
{
id: "$active",
city: "newyork"
},
"$address"
]
},
{
"$in": [
{
city: "newyork",
id: "$active"
},
"$address"
]
}
]
}
}
}
])
Explained:
Match only documents having at least one object in array with { id==$active and city=="newyork" } or { city=="newyork" and id==$active }
Playground 2

MongoDB query inside an array

I want to use this mongoDB collection:
[
{
"_id": {
"$oid": "627c4eb87e7c2b8ba510ac4c"
},
"Contact": [
{
"name": "ABC",
"phone": 5501234,
"mail": "abc#mail.com"
},
{
"name": "DEF",
"phone": 6001234,
"mail": "def#mail.com"
}
],
"nomatter": "trash"
}
]
search for {"name":"ABC"} and return only {"mail":"abc#mail.com"}.
It's possible to use find or it's necessary to use aggregate?
Try this one:
db.collection.aggregate([
{ $match: { "Contact.name": "ABC" } },
{
$project: {
Contact: {
$filter: {
input: "$Contact",
cond: { $eq: [ "$$this.name", "ABC" ] }
}
}
}
},
{ "$replaceWith": { mail: { $first: "$Contact.mail" } } }
])
Mongo Playground

Match and add new field to each array elements in Mongodb aggregation

I have two collections below and I'm creating a view table by using the aggregation and merge.
account table:
{
"_id": {
"$oid": "5f61b573db16185cebfeaaf9"
},
"status": "ACTIVE",
"personalInfo": {
"entityName": "John Doe",
...
},
"createdDateTime": {
"$date": "2020-09-16T06:49:23.103Z"
},
"modifiedDateTime": {
"$date": "2020-09-16T06:49:23.103Z"
}
}
account_transactions:
{
"_id": {
"$oid": "5f61b5b3db16185cebfeab0c"
},
"accountId": "5f61b573db16185cebfeaaf9",
"transactionType": "Payment",
"transactionStatus": "NEW",
"createdDate": {
"$date": "2020-09-16T06:50:27.305Z"
},
"createdBy": "sujal.shakya#joinsage.com",
"lastModifiedDate": {
"$date": "2020-09-16T10:23:06.617Z"
}
}
{
"_id": {
"$oid": "5f61b5b3db16185cebfeab0f"
},
"accountId": "5f61b573db16185cebfeaaf9",
"transactionType": "Credit",
"createdDate": {
"$date": "2020-09-16T06:50:27.305Z"
},
"createdBy": "sujal.shakya#joinsage.com",
"lastModifiedDate": {
"$date": "2020-09-16T10:23:06.617Z"
}
}
Below mongodb query creates a view table with each account having multiple transactions if exist and if no transactions exists then adds dummy transaction to with some dummy fields.
db.account.aggregate(
[
{
$match: {
'status': 'ACTIVE'
}
},
{$addFields: {"accountId": {"$toString": "$_id"}}},
{
$lookup: {
from: "account_transactions",
localField: "accountId",
foreignField: "accountId",
as: "transactions"
}
},
{
"$project": {
"accountType": 1,
"transactions": {
"$cond": [
{ "$eq": [ "$transactions", [] ] },
{$concatArrays: [[{"lastModifiedDate": {$arrayElemAt: ["$account.modifiedDateTime",0]}, "transactionType" : "BLANK"}]] },//adds dummy fields
{"$filter": {
"input": "$transactions",
"as": "transactions",
"cond": {
"$or": [
{
"$eq": [
"$$transactions.transactionType",
"Payment"
]
},
{
"$eq": [
"$$transactions.transactionType",
"Sale"
]
},
{
"$eq": [
"$$transactions.transactionType",
"Debit"
]
}
]
}
}
}
},
"createdBy": 1
}
},
{$merge: {into: "account_view_table", on: "_id", whenMatched: "replace", whenNotMatched: "insert"}}
])
What I need to do now is to add a new field in the view table is created above for each transactions by matching transaction type. For example: if an account has multiple transactions each with different transaction type, then check the transactionType of each transactions and add new fields "transactionTypeOrder" on each like:
I've been trying different things but nothing is working
{$addFields: {"transactions.transactionTypeOrder" :
{
$cond: [{$in:["$transactions.transactionType",["Payment"]]},"1",
{$cond: [ {$in:["$transactions.transactionType",["Sale"]]},"2",
{$cond: [{$in:["$transactions.transactionType",["Debit"]]},"3", "4" ]}
]}
]
}
}},
So if transactionType == "Payment" add transactionTypeOrder: 1, if transactionType == "Sales" add transactionTypeOrder: 2 etc. The output I'm looking for is something like below:
Case for 2nd Case checking another field also :
In the same transactions, I also have to do another check with different field to check if "transactionStatus" field exists and if not exists add "transactionStatusOrderAsc" field as "####" and "transactionStatusOrderDsc" as "~~~" and if exist same value as "transactionStatus" So the output will be as below:
So basically, I have to check transactionType and add new Filed and also at the same time check transactionStatus and two need fields.
//result view table
{
"_id": {
"$oid": "5f61b573db16185cebfeaafd"
},
//...other fields.
"transactions": [{
"transactionType": "Payment",
"transactionTypeOrder": 1,
"transactionStatus": "New",
"transactionStatusOrderAsc": "New",
"transactionStatusOrderDsc": "New"
},
{
"transactionType": "Sales",
"transactionTypeOrder": 2,
"transactionStatusOrderAsc": "####", //since transactionStatus field doesn't exist here
"transactionStatusOrderDsc": "~~~~"
},
{
"transactionType": "Debit",
"transactionTypeOrder": 3,
"transactionStatus": "Old",
"transactionStatusOrderAsc": "Old",
"transactionStatusOrderDsc": "Old"
}
]
}
You can use $map to apply to each element of your array, and $switch to check value and add your field.
db.account.aggregate([
{
$match: {
"status": "ACTIVE"
}
},
{
$addFields: {
"accountId": {
"$toString": "$_id"
}
}
},
{
$lookup: {
from: "account_transactions",
localField: "accountId",
foreignField: "accountId",
as: "transactions"
}
},
{
"$project": {
"accountType": 1,
"createdBy": 1,
transactions: /**adds dummy fields*/
{
"$map": {
"input": "$transactions",
"as": "transactions",
"in": {
$switch: {
branches: [
{
case: {
$eq: [
"$$transactions.transactionType",
"Payment"
]
},
then: {
transactionType: "$$transactions.transactionType",
transactionTypeOrder: 1
}
},
{
case: {
$eq: [
"$$transactions.transactionType",
"Sales"
]
},
then: {
transactionType: "$$transactions.transactionType",
transactionTypeOrder: 2
}
},
{
case: {
$eq: [
"$$transactions.transactionType",
"Debit"
]
},
then: {
transactionType: "$$transactions.transactionType",
transactionTypeOrder: 3
}
},
],
default: {
transactionType: "$$transactions.transactionType",
transactionTypeOrder: -1
}
}
}
}
}
}
},
])
If you need other field of each transaction, you have to add them in each case condition.
You can test it here
EDIT FROM FIRST COMMENT
If you have to include all your transactions fields, you can use $mergeObjects instead of manually creating your object.
db.account.aggregate([
{
$match: {
"status": "ACTIVE"
}
},
{
$addFields: {
"accountId": {
"$toString": "$_id"
}
}
},
{
$lookup: {
from: "account_transactions",
localField: "accountId",
foreignField: "accountId",
as: "transactions"
}
},
{
"$project": {
"accountType": 1,
"createdBy": 1,
transactions: /**adds dummy fields*/
{
"$map": {
"input": "$transactions",
"as": "transactions",
"in": {
$switch: {
branches: [
{
case: {
$eq: [
"$$transactions.transactionType",
"Payment"
]
},
then: {
$mergeObjects: [
"$$transactions",
{
transactionTypeOrder: 1
}
]
}
},
{
case: {
$eq: [
"$$transactions.transactionType",
"Sales"
]
},
then: {
$mergeObjects: [
"$$transactions",
{
transactionTypeOrder: 2
}
]
}
},
{
case: {
$eq: [
"$$transactions.transactionType",
"Debit"
]
},
then: {
$mergeObjects: [
"$$transactions",
{
transactionTypeOrder: 3
}
]
}
},
],
default: {
$mergeObjects: [
"$$transactions",
{
transactionTypeOrder: -1
}
]
}
}
}
}
}
}
},
])
Test it here

Mongo Query to fetch distinct nested documents

I need to fetch distinct nested documents.
Please find the sample document:
{
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z"),
"HList":[
{
"productId": 123,
"name": "Dubai",
"tsh": true
}
],
"PList":[
{
"productId": 123,
"name": "Dubai",
"tsh": false
},
{
"productId": 234,
"name": "India",
"tsh": true
}
],
"CList":[
{
"productId": 234,
"name": "India",
"tsh": false
}
]
}
Expected result is:
{
"produts":[
{
"productId": 123,
"name": "Dubai"
},
{
"productId": 234,
"name": "India"
}
]
}
I tried with this query:
db.property.aggregate([
{
$match: {
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z")
}
},
{
"$project": {
"_id": 0,
"unique": {
"$filter": {
"input": {
"$setDifference": [
{
"$concatArrays": [
"$HList.productId",
"$PList.productId",
"$CList.productId"
]
},
[]
]
},
"cond": {
"$ne": [ "$$this", "" ]
}
}
}
}
}
]);
Is $setDifference aggregation is correct choice here?
My query returns only unique product ids but i need a productId with name.
Could someone help me to solve this?
Thanks in advance
You can use $projectfirst to get rid of tsh field and then run $setUnion which ignores duplicated entries:
db.collection.aggregate([
{
$project: {
"HList.tsh": 0,
"PList.tsh": 0,
"CList.tsh": 0,
}
},
{
$project: {
products: {
$setUnion: [ "$HList", "$PList", "$CList" ]
}
}
}
])
Mongo Playground
The following two aggregations return the expected and same result (you can use any of the two):
db.collection.aggregate( [
{
$project: {
_id: 0,
products: {
$reduce: {
input: { $setUnion: [ "$HList", "$PList", "$CList" ] },
initialValue: [],
in: {
$setUnion: [ "$$value", [ { productId: "$$this.productId", name: "$$this.name" } ] ]
}
}
}
}
}
] )
This one is little verbose:
db.collection.aggregate( [
{
$project: { list: { $setUnion: [ "$HList", "$PList", "$CList" ] } }
},
{
$unwind: "$list"
},
{
$group: {
_id: null,
products: { $addToSet: { "productId": "$list.productId", "name": "$list.name" } }
}
},
{
$project: { _id: 0 }
}
] )
db.collection.aggregate([
{
$match: {
"propertyId": 1001820437,
"date": ISODate("2020-07-17T00:00:00.000Z")
}
},
{
$project: {
products: {
$filter: {
input: { "$setUnion" : ["$CList", "$HList", "$PList"] },
as: 'product',
cond: {}
}
}
}
},
{
$project: {
"_id":0,
"products.tsh": 1,
"products.name": 1,
}
},
])

Zip two array and create new array of object

hello all i'm working with a MongoDB database where each data row is like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
"userhasRate" : [
"51",
"52",
],
"ratings" : [
4,
3
],
}
and i need to change it to be like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
rate : [
{userhasrate: "51", value: 4},
{userhasrate: "52", value: 3},
]
}
I already try using this method,
db.getCollection('contens').aggregate([
{ '$group':{
'rates': {$push:{ value: '$ratings', user: '$userhasRate'}}
}
}
]);
and my result become like this
{
"rates" : [
{
"value" : [
5,
5,
5
],
"user" : [
"51",
"52",
"53"
]
}
]
}
Can someone help me to solve my problem,
Thank you
You can use $arrayToObject and $objectToArray inside $map to achieve the required output.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$objectToArray": {
"$arrayToObject": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
}
}
},
"as": "el",
"in": {
"userhasRate": "$$el.k",
"value": "$$el.v"
}
}
}
}
}
])
Alternative Method
If userhasRate contains repeated values then the first solution will not work. You can use arrayElemAt and $map along with $zip if it contains repeated values.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
},
"as": "el",
"in": {
"userhasRate": {
"$arrayElemAt": [
"$$el",
0
]
},
"value": {
"$arrayElemAt": [
"$$el",
1
]
}
}
}
}
}
}
])
Try below aggregate, first of all you used group without _id that grouped all the JSONs in the collection instead set it to "$_id" also you need to create 2 arrays using old data then in next project pipeline concat the arrays to get desired output:
db.getCollection('contens').aggregate([
{
$group: {
_id: "$_id",
rate1: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
0
]
},
value: {
$arrayElemAt: [
"$ratings",
0
]
}
}
},
rate2: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
1
]
},
value: {
$arrayElemAt: [
"$ratings",
1
]
}
}
}
}
},
{
$project: {
_id: 1,
rate: {
$concatArrays: [
"$rate1",
"$rate2"
]
}
}
}
])