'Using $if Condition with $or for updating multiple MongoDB fields in one go
I am trying to make the following update
if offer == {} OR offer.eta < now THEN offer = new_offer_obj ELSE offer = offer
AND another simultaneous update
if offer.eta < now THEN offer_list.push(offer) ELSE offer_list = offer_list
--- ALL of the above in a one go ---
Inside a MongoDB doc, offer is an object with fields:
offer: {info: {...}, eta: "<ISO-Time-String>"}
offer_list: is simply an array list of the above offer Object
offer_list: []
--- BELOW this is what I tried ---
Here is the code I tried after referencing other answers on SO. This does not work correctly. Not sure what is wrong here.
const now = new Date().getTime()
const res = await collection.update(
{ "_id": 12345 },
{
$set: {
offer: { $cond: { $if: { $or:[ {$lt:["$offer.eta", now]}, {$eq:["$offer", {}]}] }}, then: new_offer_obj, else:"$offer" },
offer_list: {
$cond: [{ $lt:["$offer.eta", now]},
{$push: { offer_list: "$offer" }},
"$offer_list" ]
}
}
},
{
"returnDocument" : "after",
"returnOriginal" : false
}
)
For offer I tried using one type of syntax and for offer_list I tried using another type to experiment which one works. It'd be great if someone could help.
Solution 1:[1]
There are a lot of conditions and assumptions in this question, among them trying to compare ISO8601 strings against real dates, whether offer_list is initialized to [] or if it is unset at the start, etc. So the solution below "works" but should not be considered bulletproof. It is mostly a matter of using the pipeline form of update and getting the date comparison working. Given input like this:
now = new ISODate('2022-02-26');
new_offer_obj = {info: {"NEW":"OFFER"}, eta: "2022-06-02T00:00:00Z"};
var r = [
{
'desc':"empty initial offer so this gets JUST new offer obj",
'offer': {}
// does not matter if offer_list is [] or not
}
,{
'desc':"offer is not empty but eta > now so NOTHING HAPPENS",
'offer': {info: {a:1}, eta: "2022-02-27T00:00:00Z"}
}
,{
'desc':"offer not empty AND eta < now so replace offer with new one and push existing onto offer list",
'offer': {info: {a:1}, eta: "2022-02-02T00:00:00Z"},
'offer_list': ['foo']
}
];
then this update:
rc = db.foo.update(
{},
[
// If no offer.eta, assume a very distant date; then convert in either case to real datetime:
{$addFields: {XD: {$toDate: {$ifNull:['$offer.eta','9999-01-01']}}} },
{$set: {
offer: { $cond: [
{$or:[{$lt:['$XD',now]},{$eq:["$offer",{}]}]},
new_offer_obj,
'$offer'
]
},
offer_list: { $cond: [
{ $lt:['$XD', now]},
// $offer is an object so wrap with [] to make an array of 1:
{$concatArrays: [ "$offer_list", ['$offer'] ]},
"$offer_list"
]
}
}},
{$unset: 'XD'}
],
{multi:true}
);
produces these mods:
{ "nMatched" : 3, "nUpserted" : 0, "nModified" : 2 }
{
"_id" : 0,
"desc" : "empty initial offer so this gets JUST new offer obj",
"offer" : {
"info" : {
"NEW" : "OFFER"
},
"eta" : "2022-06-02T00:00:00Z"
}
}
{
"_id" : 1,
"desc" : "offer is not empty but eta > now so NOTHING HAPPENS",
"offer" : {
"info" : {
"a" : 1
},
"eta" : "2022-02-27T00:00:00Z"
}
}
{
"_id" : 2,
"desc" : "offer not empty AND eta < now so replace offer with new one and push existing onto offer list",
"offer" : {
"info" : {
"NEW" : "OFFER"
},
"eta" : "2022-06-02T00:00:00Z"
},
"offer_list" : [
"foo",
{
"info" : {
"a" : 1
},
"eta" : "2022-02-02T00:00:00Z"
}
]
}
An interesting alternative is available if using v4.4 or higher. Starting in v4.4, the $merge stage can write back to the same collection that is being processed, which can turn an aggregate function into a "giant" update function:
c=db.foo.aggregate([
{$addFields: {XD: {$toDate: {$ifNull:['$offer.eta','9999-01-01']}}} },
{$project: {
offer: { $cond: [
{$or:[{$lt:['$XD',now]},{$eq:["$offer",{}]}]},
new_offer_obj,
'$offer'
]
},
offer_list: { $cond: [
{ $lt:['$XD', now]},
{$concatArrays: [ "$offer_list", ['$offer'] ]},
"$offer_list"
]
}
}},
// Write the docs back onto foo. Because of the $project above, only _id,
// offer, and offer_list are flowing through the pipeline now. We will
// match on _id (fast!) and just like $mergeObjects, we will merger
// the new offer and offer_list on top of the existing material.
// We set 'whenNotMatched' to 'fail' because there are no normal conditions where we
// cannot match on the same _id coming through the pipeline:
//
{$merge: {
into: "foo",
on: [ "_id" ],
whenMatched: "merge",
whenNotMatched: "fail"
}}
]);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
