How to update mongodb document and retrieve previous state atomically? - mongodb

In Mongodb, is it possible to update a document and retrieve it's previous state in an atomic operation?

You can use findAndModify. It has a new parameter which returns the original document if set to false (it's the default).
Optional. When true, returns the modified document rather than the original. The findAndModify() method ignores the new option for remove operations. The default is false.
Example:
db.people.findAndModify({
query: { name: "Andy" },
sort: { rating: 1 },
update: { $inc: { score: 1 } },
new: false
})

Related

MongoDB upsert $in

I have a bunch of records I want to upsert for specific product ids. Depending on previous calculations I want to record what type that product was in the current week/year.
Problem is that I can't figure out a way to do this except for one at a time. Right now I'm doing:
a_group.forEach(p => {
db.abc.update({
product_id: p._id,
year: 2021
}, {
$set: {
'abc.34': 'a'
}
}, {
upsert: true
});
});
Where a_group is just an array of products.
This is really heavy in case of a large products array. It just does a_group.length upsert operations.
Ideally I would like to do something like:
db.abc.update({
product_id: { $in: a_group.map(p => p._id) },
year: 2021
}, {
$set: {
'abc.34': 'a'
}
}, {
upsert: true,
multi: true
});
Which would see that a_group is an array and try to match and upsert for every single item in the array. Except that doesn't work.
Any help would be very much appreciated.
The problem here is you want a separate upsert for each discreet value of the _id.
From the docs:
An upsert:
Updates documents that match your query filter
Inserts a document if there are no matches to your query filter
In the case of an upsert such as:
updateMany(
{a:{$in[1,2,3]}},
{$set:{b:true}},
{upsert: true}
)
If there exists a document containing a: 3, then it will match the query filter, and therefore that one document will be updated, and no inserts will occur.
In the event that no document matches any of the values passed to $in, a single new document will be upserted. Since the query has no way to determine which value of a you wanted, it will create a document containing {b:true}, but will leave a undefined.
What you probably want is a bulk operation that can perform many upsert operations with a single call to the database.
Using the mongosh shell, that might look like:
let ops = [];
a_group.forEach(p => {
ops.push(
{ updateOne:
{
filter: {
product_id: p._id,
year: 2021
},
update: {$set: {'abc.34': 'a'}},
upsert: true
}
);
});
db.abc.bulkWrite(ops)
Check the docs for the driver you are using to see how to do a bulk operation.
according to your code i think a_group is array of object because you use p._id in forEach ,
you should exports _id from a_group and push in new array for example productsId
and replace
product_id: { $in: a_group }
with
product_id: { $in: productsId },

How to find document with a max field value and update it in one go (atomically)?

We can use a line like this to find doc with a max value
db.collection.find().sort({ _id: -1 }).limit(1)
but it is impossible to chain update operation and find -> update means two server call which I'd like to avoid.
Is there another way to find and update item in on operation?
Try this once.
db.collection.update({},
{
"$set": {
key: 3
}
},
{
sort: {
_id: -1
},
upsert: true
})

How to only update a field if it's different in mongodb so that watch() isn't called

I'm using MongoDB's stream changes watch() function to check for changes in the database but it's returning an update change even if the value hasn't changed
I have the following document:
{
test: "abc"
}
if I use:
Price.findOneAndUpdate(
{
test: "abc"
},
{
test: "abc"
}
);
Then watch() is called even though test hasn't actually changed. Is there a way to only change the value in MongoDB if it is different?
For reference, here is my watch() code:
Price.watch([
{
$match: {
$or: [
{"updateDescription.updatedFields.test": {$exists: true}}
],
operationType: {$in: ["replace", "insert", "update"]}
}
}
],
{fullDocument: "updateLookup"}
).on("change", change => {
const {fullDocument} = change;
if (io) {
io.to(fullDocument.userID.toString()).emit("price", fullDocument);
}
});
I'm wondering if it's because I have timestamps enabled and the updateAt field is changed? This is how I define the schema:
const Price = new Schema(
{
userID: {type: Schema.Types.ObjectId, ref: "User", required: true},
test: String
},
{timestamps: true}
);
The watch method will only return entries that have been written to the oplog, and committed to a majority of the replica set.
If an update does not make any changes to any field in a matched document, nothing is written, so there is no oplog entry, nothing replicated to the secondary nodes, and nothing for watch to return.
It is indeed the timestamps being update that causes the document to be different, and require an oplog entry which is returned by watch. As you noted, the test field was not present in the updatedFields of the watch response, because it was not actually updated.

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

MongoDB: Filter on _id == obj._id without setting obj._id to null on insert

I'd like to configure an upsert. If _id already exists on my object, it recognizes that and updates the match. If _id doesn't exist, it should insert the new document and generate an _id. I'd expect { _id: obj._id } to work, but that overrides the auto-generation of _id. The document appears with _id: null. Is there a filter that would work for this? Do I need to route to insert/update in-code?
Edit: add query.
collection.updateOne(
{ _id: entity._id },
{ $set: entity },
{ upsert: true }
)
Edit: try delete.
Even when I delete the property, no luck.
const upsertTest = (collection) => {
const entity = { date: new Date() };
console.log(entity);
// { date: 2019-11-19T22:16:00.914Z }
const _id = entity._id;
delete entity._id;
console.log(entity);
// { date: 2019-11-19T22:16:00.914Z }
collection.updateOne(
{ _id: _id },
{ $set: entity },
{ upsert: true }
);
console.log(entity);
// { date: 2019-11-19T22:16:00.914Z }
}
But the new document is this:
screenshot of new document
You have to use $setOnInsert to Insert _id
let ObjectID = require('mongodb').ObjectID;
collection.updateOne(
{ _id: entity._id },
{ $set: entity,$setOnInsert:{_id:new ObjectID()}},
{ upsert: true }
)
As the name suggests it will set the _id on insert
If you are using mongodb then new ObjectID() should work,
If its mongoose then you can use mongoose.Types.ObjectId() to generate new ObjectID
Well I Found your Issue
Changed in version 3.0: When you execute an update() with upsert: true and the query matches no existing document, MongoDB will refuse to insert a new document if the query specifies conditions on the _id field.
So in a nutshell you cannot insert a new doc using upsert:true if your query is using _id, Reference
First of all, the _id property WILL always exist, whether you set it yourself or let it auto-generate. So there's no need to check if it exists.
The syntax for upsert is as follows:
db.collection.update({
_id: 'the id you want to check for' // can put any valid 'find' query here
}, {
$set: {
foo: 'bar',
biz: 'baz'
}
}, {
upsert: true
});
If a document with the given _id exists, it will be updated with the $set object. If it does not exist, a new one will be created with the properties of the $set object. If _id is not specified in the $set object, a new _id value will be auto-generated, or will keep using the existing _id value.
The key here is using $set, rather than putting the object right in there. Using set will merge and replace the properties. Without $set it will replace the entire document, removing any unset properties.
Edit: Now seeing the actual query you made, I would suggest you delete the _id property from the entity before setting it. This will make sure it is left alone.
const _id = entity._id;
delete entity._id;
// now do your query using the new `_id` value for the ID.
From our comments you mentioned you were using the local database. The local database allows _id to be null. Using any other DB should fix your issue.
If the entity._id is null then it will create a doc with _id null, We can solve this issue by adding Types.ObjectId(). If the _id doesn't exist then it will create a new document with the proper _id. otherwise, it will update the existing document.
const { Types } = require("mongoose")
collection.updateOne({ _id: Types.ObjectId(entity._id) },{ $set: entity },{ upsert: true})