Transaction Multi Collection MongoDB by Mongoose - node.js

I want to make a transaction of MongoDB by Mongoose.
Mongoose version is ^5.8.9
Here is code to explain more details.
Below is my question.
how to match session on each collection. because I looking forward to use a session for each collection. however i fail to find a solution.
If one collection of User or Score, Transaction should be rollback and change work never to apply collection.
Is it possible ?
// here is just sample. it is not able to run by below the code
const session = await User.startSession();
await session.startTransaction();
try {
const user = await User.create(object);
const score = await Score.updateOne({userId : user._id }, {score : { $inc :5 }} );
// if one of User or Score will be failed, transaction should be rollback.
await session.commitTransaction();
} catch(e) {
console.log(e)
await session.abortTransaction();
} finally {
await session.endSession();
}

Related

Nodejs mongodb find and update multiple documents in transaction

I have a mongo 4.2 replica set. I have N processes running concurrently and trying to read a collection. This collection is like a queue. I'd like to read 100 elements and update them in a transaction so other processes won't try to read those.
My code goes:
const collection = client.db("test").collection(TEST_COLLECTION);
const session = client.startSession();
try {
let data = null;
await session.withTransaction(async () => {
console.log("starting transaction")
data = await collection.find({ runId: null }, { _id: 1, limit: 100 }).toArray();
const idList = data.map(item => item._id.toHexString());
await collection.updateMany(
{ runId: { $in: idList } },
{ $set: { runId: runId } },
{ session });
console.log("Successful transaction")
});
data.map(item => {
// process element one by one and update them (no need for transaction here)
})
} catch (e) {
console.error("The transaction was aborted due to an unexpected error: " + e);
} finally {
await session.endSession();
console.log("Closing transaction")
}
this is the code I've got right now. The thing is that find() won't accept options so I can't pass the session. This means it won't be part of the transaction.
the mongodb documentations states that: When using the drivers, each operation in the transaction must be associated with the session (i.e. pass in the session to each operation).
So I'm assuming that this is actually not transactional only the update part which not solves my problem. Is there any way to include both in my transaction? Any ideas on this? Other/better options?
Thanks
EDIT:
So I was staring at my question for 15 minutes when it hit me. If I update first using the transaction. Then querying with the runId even outside of the transaction I can achieve my goal. Am I right? Is it so easy?
EDIT2:
Edit1 was stupid now I can't limit to 100 items. Back to the start.
EDIT3:
I'am using native mongodb nodejs driver.
To use a find in a transaction, pass the session using the session method:
doc = await Customer.findOne({ name: 'Test' }).session(session);
See Transactions in Mongoose

Mongoose findOneAndUpdate return old AND new values

I have update query like this
Balances.findOneAndupdate({_id: }, {$inc: {balance: 10}}, { new: true })
In post middleware i've got
schema.post('findOneAndUpdate', function (result) {
result === updated document
})
How can i get old and new(both of them) values (for logging) without multiple queries ?
Thanks.
If someone need, i solve it with transactions.
const session = await dbInstance.startSession()
await session.startTransaction()
query ...
query ...
await session.commitTransaction()
session.endSession()

Delete documents from different collections in MongoDB from NodeJS to maintain database consistency

I have these collections: articles, favorites, and comments. I want to delete an article and with it, delete its comments and favorites.
My code:
//delete comments
await this.commentsService.deleteArticleComments(articleId);
//delete favorites
await this.favoritesService.deleteArticleFavorites(articleId);
//delete article
await this.articlesService.deleteArticle(articleId);
Each one calls to a method in service that does the deletion with Mongoose:
async deleteArticleComments(articleId: string){
return await this.commentModel.deleteMany({articleId}).exec();
}
The logic works but my concern is if the favorites or comments step fails, the article won't be deleted and the database would lose its consistency.
Is there any way to call these 3 delete actions currently and execute all of them at once? And if is there an error, all of them should be canceled.
I solved this way:
const session = await this.articleModel.collection.conn.startSession();
session.startTransaction();
try {
//delete comments
await this.commentModel.deleteMany({articleId});
//delete favorites
await this.favoriteModel.deleteMany({articleId});
//delete article
await this.articleModel.deleteOne({"_id": articleId});
await session.commitTransaction();
} catch (error) {
// If an error occurred, abort the whole transaction and
// undo any changes that might have happened
await session.abortTransaction();
throw new HttpException('Error deleting article', HttpStatus.BAD_REQUEST);
} finally {
session.endSession();
}

Mongodb Transactions - Unable to read from a snapshot

I am trying MongoDB transactions with mongoose but on every request to create a database and insert a user and a company, I am getting the following error
"Unable to read from a snapshot due to pending collection catalog changes; please retry the operation. Snapshot timestamp is Timestamp(1584451745, 1). Collection minimum is Timestamp(1584451753, 1)"
On retrying the request it works. But When I send a new request to create a new database and insert a user and a company, I get the same error. I always need to retry the request to successfully insert the data.
const session = await mongoose.startSession();
session.startTransaction();
try {
await db.createCollection("users");
await db.createCollection("companies");
const savedUser = await UserModel({
...userDetails,
}).save({ session });
const company = await new CompanyModel({
...companyDetails
}).save({ session });
await session.commitTransaction();
return { message: "User Added Successfully" };
} catch (err) {
error("Transaction Error", err);
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
Any idea what I am missing here?
Ok, I figured this out. As per mongoose documentation, you need to have collections created before using transactions:
Model.createCollection()
Parameters
[options] «Object» see MongoDB driver docs
[callback] «Function»
Create the collection for this model. By default, if no indexes are specified, mongoose will not create the collection for the model until any documents are created. Use this method to create the collection explicitly.
Note 1: You may need to call this before starting a transaction See https://docs.mongodb.com/manual/core/transactions/#transactions-and-operations
source: https://mongoosejs.com/docs/api.html#model_Model.createCollection

Data is mutated but not updated in the database

I have a sever connected to a mongodb database. When I add a first level data and then save that, it works.
For example :
// this works fine
router.post('/user/addsomedata', async (req,res)=>{
try {
const user = await User.findOne({email : req.body.email})
user.username = req.body.username
await user.save()
res.send()
} catch(e) {
res.status(404).send(e)
}
})
BUT if I try to save the object with deeper level data, it's not getting saved. I guess the update is not detected and hence the user didn't get replaced.
Example :
router.post('/user/addtask', auth ,async (req,res)=>{
const task = new Task({
name : req.body.name,
timing : new Date(),
state : false,
})
try {
const day = await req.user.days.find((day)=> day.day == req.body.day)
// day is found with no problem
req.user.days[req.user.days.indexOf(day)].tasks.push(task)
// console.log(req.user) returns exactly the expected results
await req.user.save(function(error,res){
console.log(res)
// console.log(res) returns exactly the expected results with the data filled
// and the tasks array is populated
// but on the database there is nothing
})
res.status(201).send(req.user)
} catch(e) {
res.status(400).send(e)
}
})
So I get the tasks array populated on the console even after the save callback but nothing on the db image showing empty tasks array
You're working on the user from the request, while you should first find the user from the DB like in your first example (User.findOne) and then update and save that model.
Use .lean() with your find queries whenever you are about to update the results returned by mongoose. Mongoose by default return instance objects which are immutable by nature. lean() method with find returns normal js objects which can be modified/updated.
eg. of using lean()
const user = await User.findOne({email : req.body.email}).lean();
You can read more about lean here
Hope this helps :)

Resources