Mongoose 5.10.0 was released on August 14, 2020. Mongoose 5.10 is a semver minor version that introduces several important new features. In this article, I'll describe what is arguably the most important new feature: the Connection#transaction()
function, which improves Mongoose's support for MongoDB transactions.
Why Mongoose Needs a Transaction Wrapper
The primary goal of a transaction is to let you update multiple documents in MongoDB in isolation, and potentially undo
all the updates if some error occurs by aborting the transaction. For example, if you start a transaction and insert
two documents, calling abortTransaction()
makes it so neither of the documents end up in the database:
const session = await Customer.startSession();
session.startTransaction();
// Passing the `session` option is how you indicate that `create()` is part of a transaction
await Customer.create([{ name: 'Test' }], { session: session });
await Customer.create([{ name: 'Test2' }], { session: session });
await session.abortTransaction();
const count = await Customer.countDocuments();
count; // 0, no documents were inserted, because the transaction was aborted.
Sessions have a convenient withTransaction()
function that handles aborting the transaction if an error occurs.
For example, the below code shows that the withTransaction()
function successfully undoes any changes when the
transaction executor function throws an error.
const session = await db.startSession();
await User.create({ name: 'MongoDB 4.2' });
await session.withTransaction(async function executor() {
const user = await User.findOne({ name: 'MongoDB 4.2' }).session(session);
user.name = 'MongoDB 4.4';
// By default, `save()` uses the session associated with the `findOne()`
// query, so this `save()` is part of a transaction.
await user.save();
throw new Error('Oops!');
}).catch(() => {});
const doc = await Customer.findOne();
doc.name; // 'MongoDB 4.2', `withTransaction()` aborted the transaction.
The withTransaction()
function makes working with transactions more convenient because it automatically aborts
the transaction if the executor throws an error. Unfortunately, because withTransaction()
is part of the MongoDB driver, it doesn't know about Mongoose and so it doesn't reset Mongoose document state.
In other words, if you create a document outside a transaction, try to save()
the document within a transaction, and
then abort the transaction, Mongoose will still think the document is saved!
const session = await db.startSession();
const doc = new User({ name: 'MongoDB 4.4' });
await session.withTransaction(async function executor() {
await doc.save({ session });
throw new Error('Oops!');
});
doc.isNew; // `false`, Mongoose thinks the document was saved!
This limitation is also important when you load a document outside of a transaction, modify the document within a transaction, and then abort the transaction.
const session = await db.startSession();
const doc = await User.create({ name: 'MongoDB 4.2' });
await session.withTransaction(async function executor() {
doc.name = 'MongoDB 4.4';
await doc.save({ session });
throw new Error('Oops!');
});
doc.modifiedPaths(); // []
await doc.save();
doc.name; // 'MongoDB 4.4'
const fromDb = await User.findById(doc._id);
// 'MongoDB 4.2', Mongoose didn't persist the changes because it thinks `save()` succeeded!
fromDb.name;
Introducing Connection#transaction()
In Mongoose 5.10, connections have a transaction()
function that you should use as a drop-in replacement
for withTransaction()
. Mongoose's transaction()
function calls withTransaction()
under the hood, but it
also handles resetting document state in the event of a failed transaction.
const session = await db.startSession();
const doc = await User.create({ name: 'MongoDB 4.2' });
// Use Mongoose connection's `transaction()` instead of `withTransaction()`
await db.transaction(async function executor() {
doc.name = 'MongoDB 4.4';
await doc.save({ session });
throw new Error('Oops!');
});
doc.modifiedPaths(); // ['name']
await doc.save();
doc.name; // 'MongoDB 4.4'
const fromDb = await User.findById(doc._id);
// 'MongoDB 4.4', the `transaction()` function makes sure `doc` knows to revert
// its internal state when the transaction is aborted.
fromDb.name;
Specifically, the transaction()
function resets the document's change tracking and isNew
value. If you save a new document and then abort the transaction, the transaction()
function will handle resetting the value of Document#isNew
.
const session = await db.startSession();
const doc = new User({ name: 'MongoDB 4.4' });
await db.transaction(async function executor() {
await doc.save({ session });
throw new Error('Oops!');
});
doc.isNew; // `true`, the `transaction()` function reset `isNew`
The transaction()
function is a method on Mongoose connections. If you're working with the Mongoose global, you should use mongoose.connection.transaction()
:
const mongoose = require('mongoose');
mongoose.connection.transaction(async function executor() {
// ...
});
If you're working with multiple connections, you
can use conn.transaction()
:
const mongoose = require('mongoose');
const conn = mongoose.createConnection(process.env.MONGODB_URI);
conn.transaction(async function executor() {
// ...
});
Moving On
The transaction()
function is just one of 16 new features in Mongoose 5.10. Mongoose 5.10 also adds an officially
supported optimisticConcurrency
option, improved
support for Atlas Text Search, and the ability to disable _id
for subdocuments
globally. You can find the full list on the Mongoose changelog. Make sure you upgrade to take advantage of all the new features!
Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!