My $or selector in a database trigger match expression doesn't work at the second level of nesting when configuring a database trigger - mongodb

Update: I use "$match expression" to describe this but I don't actually use the $match operator. According to the docs, the selector should conform with $match's syntax, though the $match keyword is apparently not necessary in the actual expression.
Update 2: In the actual collection, outerField represents message, fieldA represents fansNo, and fieldB represents sharedNo. So outerField.fieldA represents message.fansNo and outerField.fieldB represents message.sharedNo. This is a stringified representation of the updateDescription field when the trigger fires (i.e. when I only specify updateDescription.updatedField in the match expression):
"updateDescription: {\"removedFields\":[],\"updatedFields\":{\"someOtherField\":310,\"message.fansNo\":1,\"updatedAt\":\"2020-06-22T13:29:08.829Z\"}}"
================================================================
Original post:
So I can't understand why it fails to trigger when I specify message.fansNo and message.sharedNo in the match expression.
I am setting up a database trigger on updates to a collection, but I'm not able to get my $match expression to work in filtering the change events that cause the trigger to fire. I want to fire the trigger only if one or both of 2 nested fields are present, say fieldA and fieldB. These 2 fields are nested inside an object, and the object is the value of a field in each document. Something like this:
// CollectionA schema
{
_id: ...,
outerField: {
fieldA: 1 // or any number
fieldB: 2 // or any number
},
...
}
I have tried using this $match expression below, but the trigger doesn't fire:
{
"$or": [
{
"updateDescription.updatedFields.outerField.fieldA": {"$exists":true}
},
{
"updateDescription.updatedFields.outerField.fieldB":{"$exists":true}
}
]
}
If I remove outerField.<field>, it works. That is:
{
"$or": [
{
"updateDescription.updatedFields": {"$exists":true}
},
{
"updateDescription.updatedFields":{"$exists":true}
}
]
}
But of course that's not useful to me because the trigger will fire on any update at all.
I would provide a demo but I'm not sure how to create a sample that has database triggers configured.
Any help will be appreciated, thanks!

So I was able to get around this problem by changing the query to watch for a field that gets updated at the same time but isn't nested. I think the problem with checking for a nested field is that the ChangeEvent's updateDescription property doesn't contain the actual nested object that has changed; instead it contains the dot-notation representation of the change. So if you look at Update 2 in my post you'll see that updatedFields has this value: {\"someOtherField\":310,\"message.fansNo\":1... instead of {\"someOtherField\":310,\"message\":{\"fansNo\":1.... By using message.fansNo in the $match query, Mongo will look for this object shape: {\"message\":{\"fansNo\":1..., which doesn't match in this case. A "real" solution here could be to escape the . in message.fansNo in my match expression, but I couldn't get that to work (see this thread).
So the "solution" that worked for me is really just a workaround that works for my specific use-case: it so happens that someOtherField is always updated along with message.fansNo, and someOtherField isn't nested. So I can match someOtherField without worrying about nesting. Basically this match expression gives me the results I want:
{
"$or": [
{
"updateDescription.updatedFields.someOtherField": {"$exists":true}
},
{
"updateDescription.updatedFields.someOtherField":{"$exists":true}
}
]
}
Hope this helps someone else!

Related

MongoDB: Updating element in array always results in first element updated

TL;DR : Updating element in array always results in first element updated and results differ based on property name of key used in find params
Playground : https://mongoplayground.net/p/-4kGZnxa-WA
I want to update an object in a array and I am using 2 of the object properties to find the object then using $set operator with array.$.updateProperty to update the object
Here is the working playground link of what I want to do:
https://mongoplayground.net/p/dswt8vuzJMc
But I cant reproduce the same when I change a single property name (both in database as well as find parameter) , from the above example I changed property foo to trackID but then only the first element in array is always updated
Playground : https://mongoplayground.net/p/-4kGZnxa-WA
It seems weird as I assumed the property name shouldn't matter as long as it used the same in find params too and its not a keyword like _id
Your update is very close. You need to use "$elemMatch" to identify the specific array position where both conditions match.
N.B.: $ will only update the first matching array element. If you want to update all array elements, use $[], and if you want to update all matching array elements, using "arrayFilters" with $[<indentifier>] is convenient.
db.collection.update({
"_id": ObjectId("62f11e22d99c79532de6ff7f"),
"jobs": {
"$elemMatch": {
"trackID": 0,
"name": "kaisen_track-0_h264_1080p"
}
}
},
{
"$set": {
"jobs.$.status": "Done"
}
})
Try it on mongoplayground.net.

how can I make the "updated" of mongodb stop when updating a field of a nested array?

I have a database like this:
{
"universe":"comics",
"saga":[
{
"name":"x-men",
"characters":[
{
"character":"wolverine",
"picture":"618035022351.png"
},
{
"character":"wolverine",
"picture":"618035022352.png"
}
]
}
]
},
{
"universe":"dc",
"saga":[
{
"name":"spiderman",
"characters":[
{
"character":"venom",
"picture":"618035022353.png"
}
]
}
]
}
And with this code, I update the field where name: wolverine:
db.getCollection('collection').findOneAndUpdate(
{
"universe": "comics"
},
{
$set: {
"saga.$[outer].characters.$[inner].character": "lobezno",
"saga.$[outer].characters.$[inner].picture": "618035022354.png"
}
},
/*{
"saga.characters": 1
},*/
{
"arrayFilters": [
{
"outer.name": "x-men"
},
{
"inner.character": "wolverine"
}
],
"multi":false
}
)
I want to just update the first object where there is a match, and stop it.
For example, if I have an array of 100,000 elements and the object where the match is, is in the tenth position, he will update that record, but he will continue going through the entire array and this seems ineffective to me even though he already did the update.
Note: if I did the update using an _id inside of universe.saga.characters instead of doing the update using the name, it would still loop through the rest of the elements.
How can I do it?
Update using arrayFilters conditions
I don't think it will find and update through loop, and It does not matter if collection have 100,000 sub documents, because here is nice explanation in $[<identifier>] and has mentioned:
The $[<identifier>] to define an identifier to update only those array elements that match the corresponding filter document in the arrayFilters
In the update document, use the $[<identifier>] filtered positional operator to define an identifier, which you then reference in the array filter documents. But make sure you cannot have an array filter document for an identifier if the identifier is not included in the update document.
Update using _id
Your point,
Note: if I did the update using an _id inside of universe.saga.characters instead of doing the update using the name, it would still loop through the rest of the elements.
MongoDB will certainly use the _id index. Here is the nice answer on question MongoDB Update Query Performance, from this you will get an better idea on above point
Update using indexed fields
You can create index according to your query section of update command, Here MongoDB Indexes and Indexing Strategies has explained why index is important,
In your example, lets see with examples:
Example 1: If document have 2 sub documents and when you update and check with explain("executionStats"), assume it will take 1 second to update,
quick use Mongo Playground (this platform will not support update query)
Example 2: If document have 1000 sub documents and when you update and check with explain("executionStats"), might be it will take more then 1 second,
If provide index on fields (universe, saga.characters.character and saga.characters.picture) then definitely it will take less time then usual without index, main benefit of index it will direct point to indexed fields.
quick use Mongo Playground (this platform will not support update query)
Create Index for your fields
db.maxData.createIndex({
"universe": 1,
"saga.characters.character": 1,
"saga.characters.picture": 1
})
For more experiment use above 2 examples data with index and without index and check executionStats you will get more clarity.

How to match a trigger on a specific field in mongodb stitch?

The $match expression on mongodb's stitch application does not work properly.
I am trying to set up a simple update trigger that will only work on one field in a collection.
The trigger setup provides a $match aggregation which seems simple enough to set up.
For example if I want the trigger to only fire when the field "online" in a specified collection gets set to "true" I would do:
{"updateDescription.updatedFields":{"online":"true"}}
which for a stitch trigger is the same as:
{$match:{{updateDescription.updatedFields:{online:"true"}}}
The problem is when i try to match an update on a field that is an object.(for example hours:{online:40,offline:120}
For some reason $exists or $in does not work
So doing:
{"updateDescription.updatedFields":{"hours":{"$exists":true}}
does not work,neither does something like:
{"updateDescription.updatedFields":{"hours.online":{"$exists":true}}
The $match for the trigger is supposed to work exactly like a normal mongo $match.
They just provide one example :
{
"updateDescription.updatedFields": {
"status": "blocked"
}
}
The example is from here:
https://docs.mongodb.com/stitch/triggers/database-triggers/
I tried 100's of variations but i can't seem to get it
The trigger is working fine if the match is a specific value like:
{"updateDescription.updatedFields":{"hours.online":{"$numberInt\":"20"}}
and then i set the hours.online to 20 in the database.
I was able to have it match items by using an explicit $expr operator or declare it as a single field not an embedded object. ie. "updateDescription.updatedFields.statue": "blocked"
I struggled with this myself, trying to get a trigger to fire when a certain nested field was updated to any value (rather than just one specific one).
The issue appears to have to do with how change streams report updated fields.
With credit and thanks to MongoDB support, I can finally offer this as a potential solution, at least for simpler cases:
{
"$expr": {
"$not": {
"$cmp": [{
"$let": {
"vars": { "updated": { "$objectToArray": "$updateDescription.updatedFields" } },
"in": { "$arrayElemAt": [ "$$updated.k", 0 ] }
}},
"example.subdocument.nested_field"
]
}
}
}
Of course replace example.subdocument.nested_field with your own dot-notation field path.

MongoDB - how do I update a value in nested array/object?

I have a document in my Mongo collection which has a field with the following structure:
"_id" : "F7WNvjwnFZZ7HoKSF",
"process" : [
{
"process_id" : "wTGqVk5By32mpXadZ",
"stages" : [
{
"stage_id" : "D6Huk89DGFsd29ds7",
"completed" : "N"
},
{
"stage_id" : "Msd390vekn09nvL23",
"completed" : "N"
}
]
}
]
I need to update the value of completed where the stage_id is equal to 'D6Huk89DGFsd29ds7' - the update query will not know which object in the stages array this value of stage_id will be in.
How do I do this?
Since you have nested arrays in your object, this is bit tricky and I'm not sure if this problem can be solved with help of just one update query.
However, if you happen to know index of your matching object in first array, in your case process[0] you can write your update query like.
db.collection.update(
{"process.stages.stage_id":"D6Huk89DGFsd29ds7"},
{$set:{"process.0.stages.$.completed":"Y"}}
);
Query above will work perfect with your test case. Again, there is still possibility of having multiple objects at root level and there is no guarantee that matching object will always be at 0 index.
Solution I proposed above will fail if you have multiple children of process and if matching index of object is not zero.
However, you can achieve your goal with help of client side programming. That is find matching document, modify on client side and replace whole document with new content.
Since this approach is very in efficient, I'll suggest that you should consider altering your document structure to avoid nesting. Create another collection and move content of process array there.
In the end, I removed the outer process block, so that the process_id and stages were in the root of the document - made the process of updating easier using:
MyColl.update(
{
_id: 'F7WNvjwnFZZ7HoKSF',
"stages.stage_id": 'D6Huk89DGFsd29ds7'
},
{
$set: {"stages.$.completed": 'Y'}
}
);

How to force MongoDB pullAll to disregard document order

I have a mongoDB document that has the following structure:
{
user:user_name,
streams:[
{user:user_a, name:name_a},
{user:user_b, name:name_b},
{user:user_c, name:name_c}
]
}
I want to use $pullAll to remove from the streams array, passing it an array of streams (the size of the array varies from 1 to N):
var streamsA = [{user:"user_a", name:"name_a"},{user:"user_b", name:"name_b"}]
var streamsB = [{name:"name_a", user:"user_a"},{name:"name_b", user:"user_b"}]
I use the following mongoDB command to perform the update operation:
db.streams.update({name:"user_name", {"$pullAll:{streams:streamsA}})
db.streams.update({name:"user_name", {"$pullAll:{streams:streamsB}})
Removing streamsA succeeds, whereas removing streamsB fails. After digging through the mongoDB manuals, I saw that the order of fields in streamsA and streamsB records has to match the order of fields in the database. For streamsB the order does not match, that's why it was not removed.
I can reorder the streams to the database document order prior to performing an update operation, but is there an easier and cleaner way to do this? Is there some flag that can be set to update and/or pullAll to ignore the order?
Thank You,
Gary
The $pullAll operator is really a "special case" that was mostly intended for single "scalar" array elements and not for sub-documents in the way you are using it.
Instead use $pull which will inspect each element and use an $or condition for the document lists:
db.streams.update(
{ "user": "user_name" },
{ "$pull": { "streams": { "$or": streamsB } }}
)
That way it does not matter which order the fields are in or indeed look for an "exact match" as the current $pullAll operation is actually doing.