MongoDB Add Fields Depends on Name? - mongodb

db.getCollection("user").aggregate([
{ $match : { fname : "test1" } },
{ $addFields: { email_address : "test1#gmail.com" } },
{ $match : { fname : "test2" } },
{ $addFields: { email_address : "test2#gmail.com" } }
])
I just want to add email address depends on fname value.
This code works only when there's no test2 so works once but don't repeat.
is there any ways I can repeat match and addfields?

You can use $switch for your case.
db.collection.aggregate([
{
"$addFields": {
"email_address": {
"$switch": {
"branches": [
{
"case": {
$eq: [
"$fname",
"test1"
]
},
"then": "test1#gmail.com"
},
{
"case": {
$eq: [
"$fname",
"test2"
]
},
"then": "test2#gmail.com"
}
]
}
}
}
}
])
Here is the Mongo playground for your reference.

In the generalized case (in case it is applicable):
db.getCollection("user").aggregate([
{ $addFields: { email_address : {$concat: ["$fname", "#gmail.com"]} }}
])

Related

How to remove field conditionally mongoodb

I have a collection and its documents look like:
{
_id: ObjectId('111111111122222222223333'),
my_array: [
{
id: ObjectId('777777777788888888889999')
name: 'foo'
},
{
id: ObjectId('77777777778888888888555')
name: 'foo2'
}
//...
]
//more attributes
}
However, some documents have my_array: [{}] (with one element which is an empty array).
How can I add conditionally a projection or remove it?
I have to add it to a mongo pipeline at the end of the query, and I want to get my_array only when it has at least one element which is not an empty object. If there's an empty object remove it.
I tried with $cond and $eq in a projection stage but it is not supported. Any suggestion to solve this?
Suppose you have documents like this with my_array field:
{ "my_array" : [ ] }
{ "my_array" : [ { "a" : 1 } ] } // #(1)
{ "my_array" : null }
{ "some_fld" : "some value" }
{ "my_array" : [ { } ] }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] } // #(2)
And, the following aggregation will filter and the result will have the two documents (1) and (2):
db.collection.aggregate([
{
$match: {
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
}
}
])
This also works with a find method:
db.collection.find({
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
})
To remove the my_array field, from a document when its empty, then you try this aggregation:
db.collection.aggregate([
{
$addFields: {
my_array: {
$cond: [
{$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]},
"$my_array",
"$$REMOVE"
]
}
}
}
])
The result:
{ }
{ "my_array" : [ { "a" : 1 } ] }
{ }
{ "a" : 1 }
{ }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] }
You can't do that in a query, however in an aggregations you can add $filter to you pipeline, like so:
db.collection.aggregate([
{
$project: {
my_array: {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
}
])
Mongo Playground
However unless this is "correct" behavior I suggest you clean up your database, it's much simpler to maintain "proper" structure than to update all your queries everywhere.
You can use this update to remove these objects:
db.collection.update({
"myarray": {}
},
[
{
"$set": {
"my_array": {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
},
],
{
"multi": false,
"upsert": false
})
Mongo Playground

Optimize multiple "and" statements in mongo aggregate

Is there a simpler way that would also improve the performance of this mongodb query. I know I am suppose to group the either one or the other but cant find any docs or example to help me out.
const facetQuery = { $facet: {
xCreated: [
{ $match : { $and : [{ queueStatus: 'Created' }, { queueType: 'x' } ]}},
{ $count: "Created" },
],
xApproved: [
{ $match : { $and : [{ queueStatus: 'Approved' }, { queueType: 'x' }]}},
{ $count: "Approved" }
],
xDisapproved: [
{ $match : { $and : [{ queueStatus: 'Disapproved' }, { queueType: 'x' }]}},
{ $count: "Disapproved" }
],
yCreated: [
{ $match : { $and : [{ queueStatus: 'Created' }, { queueType: 'y' }]}},
{ $count: "Created" },
],
yApproved: [
{ $match : { $and : [{ queueStatus: 'Approved' }, { queueType: 'y' }]}},
{ $count: "Approved" }
],
yDisapproved: [
{ $match : { $and : [{ queueStatus: 'Disapproved' }, { queueType: 'y' }]}},
{ $count: "Disapproved" }
],
zCreated: [
{ $match : { $and : [{ queueStatus: 'Created' }, { queueType: 'z' }]}},
{ $count: "Created" },
],
zApproved: [
{ $match : { $and : [{ queueStatus: 'Approved' }, { queueType: 'z' }]}},
{ $count: "Approved" }
],
zDisapproved: [
{ $match : { $and : [{ queueStatus: 'Disapproved' }, { queueType: 'z' }]}},
{ $count: "Disapproved" }
],
}};
Oh wow, instead of doing all these separate matches and count you can just dynamically $group on both status and type and then construct the object you need from that:
db.collection.aggregate([
{
$group: {
_id: {
type: "$queueType",
status: "$queueStatus"
},
ApprovedCount: {
$sum: {
$cond: [
{
$eq: [
"$queueStatus",
"Approved"
]
},
1,
0
]
}
},
CreatedCount: {
$sum: {
$cond: [
{
$eq: [
"$queueStatus",
"Created"
]
},
1,
0
]
}
},
DisapprovedCount: {
$sum: {
$cond: [
{
$eq: [
"$queueStatus",
"Disapproved"
]
},
1,
0
]
}
},
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: {
$arrayToObject: [
[
{
k: {
$concat: [
"$_id.type",
"$_id.status"
]
},
v: {
$switch: {
branches: [
{
case: {
$eq: [
"$_id.status",
"Approved"
]
},
then: "$ApprovedCount"
},
{
case: {
$eq: [
"$_id.status",
"Created"
]
},
then: "$CreatedCount"
},
{
case: {
$eq: [
"$_id.status",
"Disapproved"
]
},
then: "$DisapprovedCount"
},
]
}
}
}
]
]
}
}
}
}
])
Mongo Playground

Count Both Outer and Inner embedded array in a single query

{
_id: ObjectId("5dbdacc28cffef0b94580dbd"),
"comments" : [
{
"_id" : ObjectId("5dbdacc78cffef0b94580dbf"),
"replies" : [
{
"_id" : ObjectId("5dbdacd78cffef0b94580dc0")
},
]
},
]
}
How to count the number of element in comments and sum with number of relies
My approach is do 2 query like this:
1. total elements of replies
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{ $unwind: "$comments",},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
2. count total elements of comments
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
Then sum up both, do we have any better solution to write the query like return the sum of of total element comments + replies
You can use $reduce and $concatArrays to "merge" an inner "array of arrays" into a single list and measure the $size of that. Then simply $add the two results together:
db.posts.aggregate([
{ "$match": { _id:ObjectId("5dbdacc28cffef0b94580dbd") } },
{ "$addFields": {
"totalBoth": {
"$add": [
{ "$size": "$comments" },
{ "$size": {
"$reduce": {
"input": "$comments.replies",
"initialValue": [],
"in": {
"$concatArrays": [ "$$value", "$$this" ]
}
}
}}
]
}
}}
])
Noting that an "array of arrays" is the effect of an expression like $comments.replies, so hence the operation to make these into a single array where you can measure all elements.
Try using the $unwind to flatten the list you get from the $project before using $count.
This is another way of getting the result.
Input documents:
{ "_id" : 1, "array1" : [ { "array2" : [ { id: "This is a test!"}, { id: "test1" } ] }, { "array2" : [ { id: "This is 2222!"}, { id: "test 222" }, { id: "222222" } ] } ] }
{ "_id" : 2, "array1" : [ { "array2" : [ { id: "aaaa" }, { id: "bbbb" } ] } ] }
The query:
db.arrsizes2.aggregate( [
{ $facet: {
array1Sizes: [
{ $project: { array1Size: { $size: "$array1" } } }
],
array2Sizes: [
{ $unwind: "$array1" },
{ $project: { array2Size: { $size: "$array1.array2" } } },
],
} },
{ $project: { result: { $concatArrays: [ "$array1Sizes", "$array2Sizes" ] } } },
{ $unwind: "$result" },
{ $group: { _id: "$result._id", total1: { $sum: "$result.array1Size" }, total2: { $sum: "$result.array2Size" } } },
{ $addFields: { total: { $add: [ "$total1", "$total2" ] } } },
] )
The output:
{ "_id" : 2, "total1" : 1, "total2" : 2, "total" : 3 }
{ "_id" : 1, "total1" : 2, "total2" : 5, "total" : 7 }

MongoDB - Performing an upsert on an array if arrayFilters are not satisfied

I have the following document stored in mongo:
{
"_id" : ObjectId("5d1a08d2329a3c1374f176df"),
"associateID" : "1234567",
"associatePreferences" : [
{
"type" : "NOTIFICATION",
"serviceCode" : "service-code",
"eventCode" : "test-template",
"preferences" : [
"TEXT",
"EMAIL"
]
},
{
"type" : "URGENT_NOTIFICATION",
"serviceCode" : "service-code",
"eventCode" : "test-template",
"preferences" : [
"TEXT"
]
}
]
}
I am basically trying to query one of the elements of the associatePreferences array based off of its type, serviceCode, and eventCode and add a new value to the preferences array. However, if that combination of type, serviceCode, and eventCode is not present, I would like to add a new element to the associatePreferences array with those values. This is my current query:
db.user_communication_preferences.update(
{'associateID':'testassociate'},
{$addToSet:{'associatePreferences.$[element].preferences':"NEW_VALUE"}},
{arrayFilters:[{'element.serviceCode':'service-code-not-present', 'element.eventCode':'event-code-not-present','element.type':'URGENT_NOTIFICATION'}]}
)
This query works if all of the arrayFilters are present in the an element of associatePreferences, but it does not add a new element if it is not present. What am I missing?
You can use aggregation pipeline to check the existence of the element, then append the element to associatePreferences array conditionally. Finally, using the aggregation result to update back your document.
db.user_communication_preferences.aggregate([
{
"$match": {
"associateID": "testassociate"
}
},
{
"$addFields": {
"filteredArray": {
"$filter": {
"input": "$associatePreferences",
"as": "pref",
"cond": {
$and: [
{
$eq: [
"$$pref.type",
"URGENT_NOTIFICATION"
]
},
{
$eq: [
"$$pref.eventCode",
"event-code-not-exists"
]
},
{
$eq: [
"$$pref.serviceCode",
"service-code-not-exists"
]
}
]
}
}
}
}
},
{
$addFields: {
"needAddElement": {
$eq: [
{
"$size": "$filteredArray"
},
0
]
}
}
},
{
"$addFields": {
"associatePreferences": {
"$concatArrays": [
"$associatePreferences",
{
"$cond": {
"if": {
$eq: [
"$needAddElement",
true
]
},
"then": [
{
"type": "URGENT_NOTIFICATION",
"serviceCode": "service-code-not-exists",
"eventCode": "event-code-not-exists",
"preferences": [
"TEXT"
]
}
],
"else": []
}
}
]
}
}
}
]).forEach(result){
db.user_communication_preferences.update({
_id : result._id
}, {
$set: {
"associatePreferences" : result.associatePreferences
}
})
}

Mongodb aggregation match ends with using field value

note: I'm using Mongodb 4 and I must use aggregation, because this is a step of a bigger aggregation
Problem
How to find in a collection documents that contains fields that ends with value from another field in same document ?
Let's start with this collection:
db.regextest.insert([
{"first":"Pizza", "second" : "Pizza"},
{"first":"Pizza", "second" : "not pizza"},
{"first":"Pizza", "second" : "not pizza"}
])
and an example query for exact match:
db.regextest.aggregate([
{
$match : { $expr: { $eq: [ "$first" ,"$second" ] } } }
])
I will get a single document
{
"_id" : ObjectId("5c49d44329ea754dc48b5ace"),
"first" : "Pizza", "second" : "Pizza"
}
And this is good.
But how to do the same, but with endsWith?
I've openend another question for start with here that uses indexOfBytes . But indexOf return only first match, and not last one
Edit: I've found an acceptable answer (with a lot of custom logic, my hope is Mongodb team will solve this), here the solution:
db.regextest.aggregate([
{
$addFields : {
"tmpContains" : { $indexOfBytes: [ "$first", { $ifNull : [ "$second" , 0] } ] }
}
},
{
$match: { "tmpContains" : { $gt : -1 } }
},
{
$addFields : {
"firstLen" : { $strLenBytes: "$first" }
}
},
{
$addFields : {
"secondLen" : { $strLenBytes: "$second" }
}
},
{
$addFields : {
"diffLen" : { $abs: { $subtract : [ "$firstLen", "$secondLen"] } }
}
},
{
$addFields : {
"res" : { $substr: [ "$first", "$diffLen", "$firstLen"] }
}
},
{
$match : { $expr : { $eq: [ "$res" , "$second" ] }}
}
])
As you know the length of both fields ($strLenBytes) you can use $substr to get last n characters of second field and the compare it to first field, try:
db.regextest.aggregate([
{
$match: {
$expr: {
$eq: [
"$first",
{
$let: {
vars: { firstLen: { $strLenBytes: "$first" }, secondLen: { $strLenBytes: "$second" } },
in: { $substr: [ "$second", { $subtract: [ "$$secondLen", "$$firstLen" ] }, "$$firstLen" ] }
}
}
]
}
}
}
])
Above aggregation will give you the same result as string comparison is case-sensitive in MongoDB. To fix that you can apply $toLower operator both on $first and on calculated substring of $second, try:
db.regextest.aggregate([
{
$match: {
$expr: {
$eq: [
{ $toLower: "$first" },
{
$let: {
vars: { firstLen: { $strLenBytes: "$first" }, secondLen: { $strLenBytes: "$second" } },
in: { $toLower: { $substr: [ "$second", { $subtract: [ "$$secondLen", "$$firstLen" ] }, "$$firstLen" ] } }
}
}
]
}
}
}
])