Auto calculating fields in mongodb - mongodb

Let's say there are documents in MongoDB, that look something like this:
{
"lastDate" : ISODate("2013-14-01T16:38:16.163Z"),
"items":[
{"date":ISODate("2013-10-01T16:38:16.163Z")},
{"date":ISODate("2013-11-01T16:38:16.163Z")},
{"date":ISODate("2013-12-01T16:38:16.163Z")},
{"date":ISODate("2013-13-01T16:38:16.163Z")},
{"date":ISODate("2013-14-01T16:38:16.163Z")}
]
}
Or even like this:
{
"allAre" : false,
"items":[
{"is":true},
{"is":true},
{"is":true},
{"is":false},
{"is":true}
]
}
The top level fields "lastDate" and "allAre" should be recalculated every time the data in array changes. "lastDate" should be the biggest "date" of all. "allAre" should be equal to true only if all the items have "is" as true.
How should I build my queries to achieve such a behavior with MongoDB?
Is it considered to be a good practice to precalculate some values on insert, instead of calculating them during the get request?

MongoDB cannot make what you are asking for with 1 query.
But you can make it in two-step query.
First of all, push the new value into the array:
db.Test3.findOneAndUpdate(
{_id: ObjectId("58047d0cd63cf401292fe0ad")},
{$push: {"items": {"date": ISODate("2013-01-27T16:38:16.163+0000")}}},
{returnNewDocument: true},
function (err, result) {
}
);
then update "lastDate" only if is less then the last Pushed.
db.Test3.findOneAndUpdate (
{_id: ObjectId("58047d0cd63cf401292fe0ad"), "lastDate":{$lt: ISODate("2013-01-25T16:38:16.163+0000")}},
{$set: {"lastDate": ISODate("2013-01-25T16:38:16.163+0000")}},
{returnNewDocument: true},
function (err, result) {
}
);
the second parameter "lastDate" is needed in order to avoid race condition. In this way you can be sure that inside "lastDate" there are for sure the "highest date pushed".
Related to the second problem you are asking for you can follow a similar strategy. Update {"allAre": false} only if {"_id":yourID, "items.is":false)}. Basically set "false" only if some child of has a value 'false'. If you don't found a document with this property then do not update nothing.
// add a new Child to false
db.Test4.findOneAndUpdate(
{_id: ObjectId("5804813ed63cf401292fe0b0")},
{$push: {"items": {"is": false}}},
{returnNewDocument: true},
function (err, result) {
}
);
// update allAre to false if some child is false
db.Test4.findOneAndUpdate (
{_id: ObjectId("5804813ed63cf401292fe0b0"), "items.is": false},
{$set: {"allAre": false}},
{returnNewDocument: true},
function (err, result) {
}
);

Related

Mongoose update field if another array field has a size bigger than 5

I'm new to Mongoose and I cannot find a way to achieve a certain update query.
I have the next schema:
{
usersJoined: [{
type: String
}],
status: {
type: Number,
default: 0
},
}
I want to update the status to 1 when the size of the usersJoined array is 5.
I am using the following query to query for the right document:
Model.findOneAndUpdate(
{
status: 0,
usersJoined: {$ne: user}
},
{
$push: {usersJoined: user}
}
);
I want to update the status to 1 when the size of the usersJoined
array is 5
Use array $size operator to filter records, findOneAndUpdate method will update the first matched record. Refer to document findOneAndUpdate. If this not suitable for your usecase check additional update methods
db.collection.findOneAndUpdate(
{"userJoined": {"$size": 5}},
{$set: {"status": 1}},
{returnNewDocument: true} //if true, returns the updated document.
);
Example

FindOne followed by updateOne, using a single query

By using the "findOne" method of mongoDb driver (no mongoose), can I use the retrieved document to update it later?
For example:
document.collection('myCollection').findOne({ _id: myId }, (err, foundDoc) => {
// **I need to do some multiple checks in between**, so I don't want to use "findOneAndUpdate()"
// foundDoc is the retrieved document. Can I use it directly for update? (something like "foundDoc.update()")
document.collection('myCollection').updateOne({ _id: myId }, { $set: { name: 'John' } });
});
As you can see I am basically doing a second query by using the "updateOne" method (first it searches for the document and then it updates it). Can I avoid that somehow, and use the foundDoc directly for update?
If you want to update the same document, you don't have to call .findOne() followed by .updateOne() method. By default, upsert option is set to false in .upadateOne(), so it will refuse to insert the document if not found else it will update.
document.collection('myCollection').updateOne({ _id: myId }, { $set: { name: 'John' } });
.updateOne should be sufficient in your case.
Also if you want to add some filter conditions, .updateOne() supports that as below :
db.collection.updateOne(
<filter>, // you can place filters here
<update>,
{
upsert: <boolean>,
writeConcern: <document>,
collation: <document>,
arrayFilters: [ <filterdocument1>, ... ]
}
)
Link : https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/

Updating the path 'x' would create a conflict at 'x'

This error happens when I tried to update upsert item:
Updating the path 'x' would create a conflict at 'x'
Field should appear either in $set, or in $setOnInsert. Not in both.
I had the same problem while performing an update query using PyMongo.
I was trying to do:
> db.people.update( {'name':'lmn'}, { $inc : { 'key1' : 2 }, $set: { 'key1' : 5 }})
Notice that here I'm trying to update the value of key1 from two MongoDB Update Operators.
This basically happens when you try to update the value of a same key with more than one MongoDB Update Operators within the same query.
You can find a list of Update Operators over here
If you pass the same key in $set and in $unset when updating an item, you will get that error.
For example:
const body = {
_id: '47b82d36f33ad21b90'
name: 'John',
lastName: 'Smith'
}
MyModel.findByIdAndUpdate(body._id, { $set: body, $unset: {name: 1}})
// Updating the path 'name' would create a conflict at 'name'
You cannot have the same path referenced more than once in an update. For example, even though the below would result in something logical, MongoDB will not allow it.
db.getCollection("user").updateOne(
{_id: ...},
{$set: {'address': {state: 'CA'}, 'address.city' : 'San Diego'}}
)
You would get the following error:
Updating the path 'address.city' would create a conflict at 'address'
db.products.update(
{ _id: 1 },
{
$set: { item: "apple" },
$setOnInsert: { defaultQty: 100 }
},
{ upsert: true }
)
Below is the key explanation to the issue:
MongoDB creates a new document with _id equal to 1 from the
condition, and then applies the $set AND $setOnInsert operations to
this document.
If you want a field value is set or updated regardless of insertion or update, use it in $set. If you want it to be set only on insertion, use it in $setOnInsert.
Here is the example: https://docs.mongodb.com/manual/reference/operator/update/setOnInsert/#example
Starting from MongoDB 4.2 you can use aggregate pipelines in update:
db.your_collection.update({
_id: 1
},
[{
$set:{
x_field: {
$cond: {
if: {$eq:[{$type:"$_id"} , "missing"]},
then: 'upsert value', // it's the upsert case
else: '$x_field' // it's the update case
}
}
}
}],
{
upsert: true
})
db.collection.bulkWrite() also supports it
With the Ruby library at least, it's possible to get this error if you have the same key twice, once as a symbol and once as a string:
db.getCollection("user").updateOne(
{_id: ...},
{$set: {'name': "Horse", name: "Horse"}}
)
I recently had the same issue while using the query below.
TextContainer.findOneAndUpdate({ blockId: req.params.blockId, 'content._id': req.params.noteId }, { $set: { 'content.note': req.body.note } }, { upsert: true, new: true })
When i have changed 'content.note' to 'content.$.note' it has been fixed. So my final query is :
TextContainer.findOneAndUpdate({ blockId: req.params.blockId, 'content._id': req.params.noteId }, { $set: { 'content.$.note': req.body.note } }, { upsert: true, new: true })

MongoDB Increment or Upsert

Given the following document structure:
{
'_id': ObjectId("559943fcda2485a66285576e"),
'user': '70gw3',
'data': [
{
'date': ISODate("2015-06-29T00:00:00.0Z"),
'clicks': 1,
},
{
'date': ISODate("2015-06-30T00:00:00.0Z"),
'clicks': 5,
},
]
}
How can I increment the clicks value of 2015-06-30 and increase if it doesn't exists [the whole document OR the specific date], make it equals to 1?
Unfortunately it is not possible to achieve your goal within a single query. But instead you can do the following
var exists = db.yourCollection.find(
{'_id': ObjectId("559943fcda2485a66285576e"), 'data.date': <your date>}
).count()
This query will return the number of documents which have the specified _id and an object with the specified date in the data array. As the _id field is unique, you will get at most one result. If such element does not exist in the data array, you will not get any result. Thus, you have the following cases:
exists == 1: There IS an element with the specified date
exists == 0: There IS NO element with the specified date
Then this can become your condition:
if (exists) {
db.yourCollection.update({_id: <your id>, 'data.date': <your date>}, {$inc: {'data.$.clicks': 1}})
} else {
if (db.runCommand({findAndModify: 'yourCollection', query: {_id: <your id>, 'data.date': {$ne: <your date>}}, update: {$push: {data: {date: <your date>, clicks: 1}}}}).value) {
db.yourCollection.update({_id: <your id>, 'data.date': <your date>}, {$inc: {'data.$.clicks': 1}})
}
}
The correct answer is that while you need to perform two update operations in order to fulfil the logic there is no need to find and check data in a way.
You have two basic conditions
If the element is there then update it
If the element is not there then add it
The process is basically then two updates. One will succeed and the other will not. No need to check.
Probably the best way to implement this is using the "Bulk" operations API. This allows both update statements to be sent to the server at the same time, and the response it a single response so there are less trips. But the logic is the same even if you don't use this.
var bulk = db.collection.initializeOrderedBulkOp();
// Update the date where it matched
bulk.find({ "user": "70gw3", "data.date": new Date("2015-06-30") })
.updateOne({ "$inc": { "data.$.clicks": 1 } });
// Push a new element where date does not match
bulk.find({ "user": "70gw3", "data.date": { "$ne": new Date("2015-06-30") } })
.updateOne({ "$push": { "data": { "date": new Date("2015-06-30"), "clicks": 1 } }});
bulk.execute();
In short, out of two update requests sent for any document then only one of the operations will actually modify anything. This is because the conditions for testing "date" are the "inverse" of one another, in that you "update" where present, and "create" where the matching element is not present.
Both states cannot exist at the same time, as long as the "create" follows the "update" the logic is sound. So first try to update, and then try to "create" where no match is found.

Increment a {key: value} within an array of objects with MongoDB Mongoose driver

I have a document that looks similar to this:
{
'user_id':'{1231mjnD-32JIjn-3213}',
'name':'John',
'campaigns':
[
{
'campaign_id':3221,
'start_date':'12-01-2012',
'worker_id': '00000'
},
{
'campaign_id':3222,
'start_date':'13-01-2012',
'worker_id': '00000'
},
...
etc
...
]
}
Say I want to increment the campaign_id of element 7 in the campaigns array.
Using the dot with the mongoose driver, I thought I could do this:
camps.findOneAndUpdate({name: "John"}, {$inc: {"campaigns.7.campaign_id": 1}}, upsert: true, function(err, camp) {...etc...});
This doesn't increment the campaign_id.
Any suggestions on incrementing this?
You've got the right idea, but your upsert option isn't formatted correctly.
Try this:
camps.findOneAndUpdate(
{name: "John"},
{$inc: {"campaigns.7.campaign_id": 1}},
{upsert: true},
function(err, camp) {...etc...});