How to add a sub-document field with a condition in MongoDB? - mongodb

I am trying to add a new subField with a condition.
In the case the field already exists, I don't overwrite it.
In the case the condition is not fulfilled, I don't want to add the parent object.
Here is my collection :
{type: "A", object: {a: "", b: "foo"}},
{type: "A", object: {a: ""}},
{type: "A"},
{type: "B"}
Here is my aggregate :
{
$addFields: {
"object.b": {
$cond: {
if: {$eq: ["$type","A"]},
then: {$ifNull: ["$object.b", "bar"]},
else: "$DROP"
}
}
}
}
$DROP is not an aggregate command, but in the else case I don't want to add the new field.
It will not create the b field, but the parent object remains.
Here is my current result :
{type: "A", "object": {a: "", b: "foo"}},
{type: "A", "object": {a: "", b: "bar"}},
{type: "A", "object": {b: "bar"}},
{type: "B", "object": {}},
Here is what I want :
{type: "A", object: {a: "", b: "foo"}},
{type: "A", object: {a: "", b: "bar"}},
{type: "A", object: {b: "bar"}},
{type: "B"}
Your help will be highly appreciated.

This aggregate query will give you the desired result:
db.collection.aggregate([
{
$addFields: {
object: {
$cond: {
if: { $eq: [ "$type", "A" ] },
then: {
$mergeObjects: [
"$object",
{ b: { $ifNull: [ "$object.b", "bar" ] } }
]
},
else: "$$REMOVE"
}
}
}
}
])
Note the $$REMOVE is a aggregation system variable.

When a $set adds a path, all path is added, even if you end up to $$REMOVE this will affect only the last of the path, the rest would be already added see example
Query
$set with switch case starting from object
if object doesn't exist
if type A add {object :{"b" : "bar"}}
else $$REMOVE
$type = "A" AND (not-value "$object.b")
add {"b" : "bar"} (this case covers also the case object:null)
else
keep old value (another type, or b had value)
*Maybe it could be smaller but we check many things(see the example for all cases of data)
object exists/null/not exists
type A/not
b exists/null/value
Test code here
db.collection.aggregate([
{
"$set": {
"object": {
"$switch": {
"branches": [
{
"case": {
"$eq": [
{
"$type": "$object"
},
"missing"
]
},
"then": {
"$cond": [
{
"$eq": [
"$type",
"A"
]
},
{
"b": "bar"
},
"$$REMOVE"
]
}
},
{
"case": {
"$and": [
{
"$eq": [
"$type",
"A"
]
},
{
"$or": [
{
"$eq": [
"$object.b",
null
]
},
{
"$eq": [
{
"$type": "$object.b"
},
"missing"
]
}
]
}
]
},
"then": {
"$mergeObjects": [
"$object",
{
"b": "bar"
}
]
}
}
],
"default": "$object"
}
}
}
}
])

Related

Project object existence boolean in MongoDB

I have a document structure that looks like this (two example docs below).
{
"A": "value"
},
{
"A": "value",
"B": {
"a": "value",
"b": "value"
}
}
I want to aggregate such that the value of field A is projected while a true/false value is returned depending on whether the object B exists. The result of the query would be:
{
"A": "value",
"B": false
},
{
"A": "value",
"B": true
}
Even a shorter solution:
db.collection.aggregate({
$project: {
A: 1,
B: { $cond: ["$B", true, false] }
}
})
or
db.collection.aggregate({
$project: {
A: 1,
B: { $ifNull: [{ $toBool: "$B" }, false] }
}
})
However, following documents will yield different result than the other answers. Check your application if such documents apply.
{
'A': 'value5',
'B': false
},
{
'A': 'value5',
'B': []
}
You can use below aggregation
db.collection.aggregate([
{ "$addFields": {
"B": {
"$cond": [
{ "$eq": ["$B", undefined] },
false,
true
]
}
}}
])
You may use $type operator:
If the argument is a field that is missing in the input document, $type returns the string "missing".
db.collection.aggregate([
{
$project: {
A: 1,
B: {
$ne: [
{
$type: "$B"
},
"missing"
]
}
}
}
])
MongoPlayground

how to convert query to use regex instead of equal

I'm using MongoDB and i have the following records:
{
name: "a",
status: [
{ age: 15, closed: true},
{ age: 38, closed: true},
{ age: 42, closed: false},
]
},
{
name: "b",
status: [
{ age: 29, closed: true},
{ age: 5, closed: false},
]
}
I want to check if the before last object in status has for example age = 29.
so for that i have this working query:
db.col.find({
$expr: {
$eq: [
29,
{
$let: {
vars: { beforeLast: { $arrayElemAt: [ "$status", -2 ] } },
in: "$$beforeLast.age"
}
}
]
}
})
but i want now to check for example if age contain a value like "2". i need to use regex expression. is there anyway to convert that query to use regex inside/instead $eq operator ?
PS: i don't want to use aggregation because i'm working with an old version.
You can use $indexOfCP to find sub string inside an string character
db.collection.find({
"$expr": {
"$ne": [
{
"$indexOfCP": [
{ "$toLower": {
"$let": {
"vars": {
"beforeLast": {
"$arrayElemAt": [
"$status",
-2
]
}
},
"in": "$$beforeLast.age"
}
}},
"2"
]
},
-1
]
}
})
Try with aggregation
db.collection.aggregate([
{
$project:
{
last: { $arrayElemAt: [ "$status", -2 ] },
}
},
{$addFields:{
ageInString: { $substr: [ "$last.age", 0, 3 ] }
}},
{$match:{ageInString:{ $regex: '^2' }}}
])

$elemMatch against two Array elements if one fails

A bit odd but this is what I am looking for.
I have an array as follow:
Document 1:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "1234"
}
Document 2:
Items: [
{
"ZipCode": "11111",
"ZipCode4" "0000"
}
I would like to use a single query, and send a filter on ZipCode = 1111 && ZipCode4 = 4321, if this fails, the query should look for ZipCode = 1111 && ZipCode4: 0000
Is there a way to do this in a single query ? or do I need to make 2 calls to my database ?
For matching both data set (11111/4321) and (11111/0000), you can use $or and $and with $elemMatch like the following :
db.test.find({
$or: [{
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "4321" }
}
}]
}, {
$and: [{
"Items": {
$elemMatch: { "ZipCode": "11111" }
}
}, {
"Items": {
$elemMatch: { "ZipCode4": "0000" }
}
}]
}]
})
As you want conditional staging, this is not possible but we can get closer to it like this :
db.test.aggregate([{
$match: {
$or: [{
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "4321" }]
}, {
$and: [{ "Items.ZipCode": "11111" }, { "Items.ZipCode4": "0000" }]
}]
}
}, {
$project: {
Items: 1,
match: {
"$map": {
"input": "$Items",
"as": "val",
"in": {
"$cond": [
{ $and: [{ "$eq": ["$$val.ZipCode", "11111"] }, { "$eq": ["$$val.ZipCode4", "4321"] }] },
true,
false
]
}
}
}
}
}, {
$unwind: "$match"
}, {
$group: {
_id: "$match",
data: {
$push: {
_id: "$_id",
Items: "$Items"
}
}
}
}])
The first $match is for selecting only the items we need
The $project will build a new field that check if this items is from the 1st set of data (11111/4321) or the 2nd set of data (11111/0000).
The $unwind is used to remove the array generated by $map.
The $group group by set of data
So in the end you will have an output like the following :
{ "_id" : true, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394972"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] }, { "_id" : ObjectId("58af69ac594b51730a394974"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "4321" } ] } ] }
{ "_id" : false, "data" : [ { "_id" : ObjectId("58af69ac594b51730a394971"), "Items" : [ { "ZipCode" : "11111", "ZipCode4" : "0000" } ] } ] }
Your application logic can check if there is _id:true in this output array, just take the corresponding data field for _id:true. If there is _id:false in this object take the corresponding data field for _id:false.
In the last $group, you can also use $addToSet to builds 2 field data1 & data2 for both type of data set but this will be painful to use as it will add null object to the array for each one of the opposite type :
"$addToSet": {
"$cond": [
{ "$eq": ["$_id", true] },
"$data",
null
]
}
Here is a gist

mongodb aggregate pipeline if else to execute one of the query

My objects looks like the following
{
id: 1,
a: "val7f",
b: "online",
c: "reg",
d: "retd"
},
{
id: 2,
a: "val6f",
b: "online",
c: "reg",
d: "new"
},
{
id: 3,
a: "val6f",
b: "offline",
c: "non-reg",
d: "new"
}
I want to build a query in the following manner.
if (a=="val7f" && b=="offline" && c="reg") has objs > 1 return result Objs
else if(a=="val7f" && b=="offline" && c="reg") has objs > 1 return result objs
etc.
I built the following query but values displayed are from else query even if there are results for the if case:
db.Collection.aggregate([
{ $redact: {
$cond: {
if: {
$and: [
{ $eq: [ "a", "val7f" ] },
{ $eq: [ "b", "offline" ] },
{ $eq: [ "c", "reg" ] }
]
},
then: "$$DESCEND",
else: {
$cond: {
if: {
$and: [
{ $eq: [ "$a", "val6f"] },
{ $eq: [ "b", "offline"] },
{ $eq: [ "c", "reg"] }
]
},
else: "$$PRUNE"
}
}
}
} }
]);
Summarised as follows
I've put condition as follows
if: {
$and: [
{ $eq: [ "a", "val7f" ] },
{ $eq: [ "b", "offline" ] },
{ $eq: [ "c", "reg" ] }
]
}
if this has result say
{
id: 6,
a: "val7f",
b: "online",
c: "reg",
d: "retd"
}
it should return the above document.
in case there are no document matching criteria
else case should execute
if: {
$and: [
{ $eq: [ "$a", "val6f"] },
{ $eq: [ "b", "offline"] },
{ $eq: [ "c", "reg"] }
]
}
and if there is match for this say
{
id: 11,
a: "val6f",
b: "offline",
c: "reg",
d: "new"
}
In my case, for the query built above I'm getting both the documents in the output shell.

Mongodb array $push and $pull

I was looking to pull some value from array and simultaneously trying to update it.
userSchema.statics.experience = function (id,xper,delet,callback) {
var update = {
$pull:{
'profile.experience' : delet
},
$push: {
'profile.experience': xper
}
};
this.findByIdAndUpdate(id,update,{ 'new': true},function(err,doc) {
if (err) {
callback(err);
} else if(doc){
callback(null,doc);
}
});
};
i was getting error like:
MongoError: exception: Cannot update 'profile.experience' and 'profile.experience' at the same time
I found this explanation:
The issue is that MongoDB doesn’t allow multiple operations on the
same property in the same update call. This means that the two
operations must happen in two individually atomic operations.
And you can read that posts:
Pull and addtoset at the same time with mongo
multiple mongo update operator in a single statement?
In case you need replace one array value to another, you can use arrayFilters for update.
(at least, present in mongo 4.2.1).
db.your_collection.update(
{ "_id": ObjectId("your_24_byte_length_id") },
{ "$set": { "profile.experience.$[elem]": "new_value" } },
{ "arrayFilters": [ { "elem": { "$eq": "old_value" } } ], "multi": true }
)
This will replace all "old_value" array elements with "new_value".
Starting from MongoDB 4.2
You can try to update the array using an aggregation pipeline.
this.updateOne(
{ _id: id },
[
{
$set: {
"profile.experience": {
$concatArrays: [
{
$filter: {
input: "$profile.experience",
cond: { $ne: ["$$this", delet] },
},
},
[xper],
],
},
},
},
]
);
Following, a mongoplayground doing the work:
https://mongoplayground.net/p/m1C1LnHc0Ge
OBS: With mongo regular update query it is not possible.
Since Mongo 4.2 findAndModify supports aggregation pipeline which will allow atomically moving elements between arrays within the same document. findAndModify also allows you to return the modified document (necessary to see which array elements were actually moved around).
The following includes examples of:
moving all elements from one array onto the end of a different array
"pop" one element of an array and "push" it to another array
To run the examples, you will need the following data:
db.test.insertMany( [
{
"_id": ObjectId("6d792d6a756963792d696441"),
"A": [ "8", "9" ],
"B": [ "7" ]
},
{
"_id": ObjectId("6d792d6a756963792d696442"),
"A": [ "1", "2", "3", "4" ],
"B": [ ]
}
]);
Example 1 - Empty array A by moving it into array B:
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696441") },
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, "$A" ] } } },
{ $set: { "A": [] } }
],
new: true
});
Resulting in:
{
"_id": {
"$oid": "6d792d6a756963792d696441"
},
"A": [],
"B": [
"7",
"8",
"9"
]
}
Example 2.a - Pop element from array A and push it onto array B
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"A": {$exists: true, $type: "array", $ne: [] }},
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, [ { $first: "$A" } ] ] } } },
{ $set: { "A": { $slice: ["$A", 1, {$max: [{$subtract: [{ $size: "$A"}, 1]}, 1]}] } }}
],
new: true
});
Resulting in:
{
"_id": {
"$oid": "6d792d6a756963792d696442"
},
"A": [
"2",
"3",
"4"
],
"B": [
"1"
]
}
Example 2.b - Pop element from array A and push it onto array B but in two steps with a temporary placeholder:
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"temp": { $exists: false } },
update: [
{ $set: { "temp": { $first: "$A" } } },
{ $set: { "A": { $slice: ["$A", 1, {$max: [{$subtract: [{ $size: "$A"}, 1]}, 1]}] } }}
],
new: true
});
// do what you need to do with "temp"
db.test.findAndModify({
query: { _id: ObjectId("6d792d6a756963792d696442"),
"temp": { $exists: true } },
update: [
{ $set: { "B": { $concatArrays: [ { $ifNull: [ "$B", [] ] }, [ "$temp" ] ] } } },
{ $unset: "temp" }
],
new: true
});