MongoDB update array if condition matches - mongodb

I'm looking for a MongoDB aggregation pipeline which updates an array with a conditional statement.
My data looks like the following:
{
"_id": 1,
"locations": [
{
"controllerID": 1,
"timestamp": 1234
},
{
"controllerID": 2,
"timestamp": 2342
}
]
},...
Potential new entry:
{
"controllerID": 2,
"timestamp": //will be set automatically
}
At first I want to match the _id (not a problem) and then push the new entry to the locations array if the element with the newest/latest timestamp has a different controllerID.
When pushing a new location object the timestamp will be set automatically.
Example 1
Input:
{
"controllerID": 2,
}
Expected Result:
{
"_id": 1,
"locations": [
{
"controllerID": 1,
"timestamp": 1234
},
{
"controllerID": 2,
"timestamp": 2342
}//noting is added because the newset entry in the array has the same controllerID
]
},
Example 2
Input:
{
"controllerID": 1,
}
Expected Result:
{
"_id": 1,
"locations": [
{
"controllerID": 1,
"timestamp": 1234
},
{
"controllerID": 2,
"timestamp": 2342
},
{//added because the controllerID is different to te last element
"controllerID": 1,
"timestamp": 4356
}
]
},
Thanks in advance!

Here's a solution.
var candidate = 2;
rc=db.foo.update({}, // add matching criteria here; for now, match ALL
[
[
// We cannot say "if condition then set fld = X else do nothing".
// We must say "set fld to something based on condition."
// The common pattern becomes:
// "Set fld to (if condition then X else fld)"
// in other words, set the fld to *itself*
//
// Note the use of the dot operator on the $locations field.
// Also, not sure about what sort of new timestamp is desired so let's
// just throw in an ISODate() for now.
{$set: {'locations': {$cond: [
{$ne:[candidate, {$last:'$locations.controllerID'}]}, // IF not same as candidate...
{$concatArrays: ['$locations',
// $concatArrays wants arrays, not objects, so we must wrap our new
// object with [] to make an array of 1:
[ {controllerId:candidate,timestamp:new ISODate() } ]
]}, // THEN concat a new entry to end of existing locations
'$locations' // ELSE just set back to existing locations
]}
}}
],
{multi:true}
);
The engine is "smart enough" to realize that setting a field to itself will not trigger a modification so the approach is performant and will not rewrite the entire set of matched objects; this can be seen in the output of the update() call, e.g.:
printjson(rc);
{ "nMatched" : 1002, "nUpserted" : 0, "nModified" : 1 }

Related

MongoDB Update: push old field value to another array field if new value is different

I have a simple collection like:
{
"_id": "62e92e47e0f473e37a491574",
"id": 1,
"price": 999,
"price_changed": false,
"prices_history": [] # there cases when this field does not exist
}
I want to make an update statement (using pymongo) with price = 1000, so the final document looks like
{
"_id": "62e92e47e0f473e37a491574",
"id": 1,
"price": 1000,
"price_changed": true,
"prices_history": [999]
}
So far I can only detect field value change with:
ops = [pymongo.UpdateOne(
{'id': 1},
[
{
'$set':
{
'id': 1,
'price': 1000,
'price_changed': {'$ne': ['$price', 1000]},
}
}
]
)]
collection.bulk_write(ops)
But I cannot understand how to build the pipeline to add the last stage - push old value to the prices_history array
Any suggestions?
Cases:
Insert happens - should be inserted with prices_history = []
Update happens, values are equal - prices_history = prices_history
Update happens, values are different - prices_history = prices_history + price (old)
You can use $concatArrays operator to add new element in array, and $ifNull operator if the field prices_history does not exists,
[
{
'$set': {
'price': 1000,
'price_changed': { '$ne': ['$price', 1000] },
'prices_history': {
'$concatArrays': [
{ "$ifNull": ["$prices_history", []] },
["$price"]
]
}
}
}
]
Playground

How to insert subdocument into array field only if there's no document with the same "key": "value" pair (MongoDB)?

I have a collection of documents that look like this:
{
"AAA": 1,
"BBB": [
{
"CCC": 1,
"DDD": [1,2,3]
}
]
}
How to insert a new subdocument ({"CCC": 1, "DDD": []}) into "BBB" array only if there's no object with {"CCC": 1} key pair?
You can actually do this in a couple of ways, the easiest would be to make the query 'fail' to match if the document has CCC: 1, like so:
db.collection.updateOne(
{
_id: docId,
'BBB.CCC': {
$ne: 1,
},
},
{
'$push': {
BBB: {
'CCC': 1,
'DDD': [],
},
},
},
);
Now if the document has a BBB.CCC value of 1 then the update will not find a document to update and nothing will be updated as you expect.
Mongo Playground

Mongodb: push element to nested array if the condition is met

I have the following collection:
{
"_id": 11,
"outerArray": [
{ "_id" : 21,
"field": {
"innerArray" : [
1,
2,
3
]
}
},
{ "_id" : 22,
"field": {
"innerArray" : [
2,
3
]
}
},
{ "_id" : 23,
"field": {
"innerArray" : [
2
]
}
}
]
}
I need to go through all documents in collection and push to innerArray new element 4, if innerArray already contains element 1 or element 3
I tried to do it this way, and few others, similar to this one, but it didn't work as expected, it only pushes to innerArray of first element of outerArray
db.collection.updateMany(
{ "outerArray.field.innerArray": { $in: [ 1, 3 ] } },
{ $push: { "outerArray.$.field.innerArray": 4} }
)
How to make it push to all coresponding innerArrays?
Problem here is your missunderstanding a copule things.
When you do "outerArray.field.innerArray": { $in: [ 1, 3 ] } into your query, your are not getting only innerArray where has 1 or 3. You are gettings documents where exists these arrays.
So you are querying the entire document.
You have to use arrayFilter to update values when the filter is match.
So, If I've understood you correctly, the query you want is:
db.collection.update(
{}, //Empty object to find all documents
{
$push: { "outerArray.$[elem].field.innerArray": 4 }
},
{
"arrayFilters": [ { "elem.field.innerArray": { $in: [ 1, 3 ] } } ]
})
Example here
Note how the first object into update is empty. You have to put there the field to match the document (not the array, the document).
If you want to update only one document you have to fill first object (query object) with values you want, for example: {"_id": 11}.

How to find documents with child collection containing only certain value

I have a following JSON structure:
{
"id": "5cea8bde0c80ee2af9590e7b",
"name": "Sofitel",
"pricePerNight": 88,
"address": {
"city": "Rome",
"country": "Italy"
},
"reviews": [
{
"userName": "John",
"rating": 10,
"approved": true
},
{
"userName": "Marry",
"rating": 7,
"approved": true
}
]
}
I want to find a list of similar documents where ALL ratings values of a review meet a certain criteria eg. less than 8. The document above wouldn't qualify as on of the review has rating 10.
with Querydsl in the following form I still obtain that documnt
BooleanExpression filterByRating = qHotel.reviews.any().rating.lt(8);
You can use $filter and $match to filter out the transactions that you don't need. Following query should do it:
Note: The cond in the $filter is the opposite of your criteria. Since you need ratings less than 8, in this case you gonna need ratings greater than or equals 8
db.qHotel.aggregate([
{
$addFields: {
tempReviews: {
$filter: {
input: "$reviews",
as: "review",
cond: { $gte: [ "$$review.rating", 8 ] } // Opposite of < 8, which is >= 8
}
}
}
},
{
$match : {
tempReviews : [] // This will exclude the documents for which there is at least one review with review.rating >= 8
}
}
]);
The result in the end will contain empty field named tempReviews, you can just use $project to remove it.
EDIT:
Check the example here.

mongodb check an element has one nested attribute

I have a collection of documents like this
[
{ "name": "pika", "attrs": { "A": 1, "B": 2 ... } },
{ "name": "chu", "attrs": { "C": 3 } },
{ "name": "plop", "attrs": { "A": 1, "C": 3 } }
]
I would like to delete records that have a "C" and only a "C" attribute in their "attrs" (line named "chu") using mongodb 2.4. The number of possible attributes under the attrs key is possibly large (> 100).
I can use several queries.
How would you do that ?
Edit : I want to keep attr C in lines containing other attributes.
You have two choices. If your key space is small you can do
db.collection.remove( {C : {$exists:true}, A: {$exists:false}, B: {$exists: false} })
Otherwise you'll need to do
var col = db.collection.find( {C : {$exists:true}} );
for(doc in col) {
var found = false
for(key in obj) {
if( key !== 'C' ) {
found = true;
break;
}
}
if(found === false) {
db.collection.remove(doc);
}
}
There's no way to count the number of keys in a document directly within MongoDB and there's no way to query on wildcards in key names (e.g. you can't do "key not equal to C : {$exists: false}"). So you either need to test all keys explicitly or test each document in your application layer.
If the "attrs" is a array, in other words, you collections like this:
{ "name": "pika", "attrs": [{ "A": 1}, {"B": 2}] };
{ "name": "chu", "attrs": [{ "C": 3 }] };
{ "name": "plop", "attrs": [{ "A": 1}, {"C": 3 }] }
Then you can write a query like below to find the specific record you want:
db.entities.find({"attrs.C": {$exists: true}, "attrs": {$size: 1}});
You can check the mongodb website to find the $size operation, http://docs.mongodb.org/manual/reference/operator/size/