Skip to content

Commit

Permalink
docs(transactions): introduce session.withTransaction() before `ses…
Browse files Browse the repository at this point in the history
…sion.startTransaction()` because `withTransaction()` is the recommended approach

Fix #10008
  • Loading branch information
vkarpov15 committed Mar 19, 2021
1 parent 61d313b commit f2651d7
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 93 deletions.
71 changes: 29 additions & 42 deletions docs/transactions.pug
Expand Up @@ -27,53 +27,17 @@ block content
in isolation and potentially undo all the operations if one of them fails.
This guide will get you started using transactions with Mongoose.

## Your First Transaction
## Getting Started with Transactions

[MongoDB currently only supports transactions on replica sets](https://docs.mongodb.com/manual/replication/#transactions),
not standalone servers. To run a
[local replica set for development](http://thecodebarbarian.com/introducing-run-rs-zero-config-mongodb-runner.html)
on macOS, Linux or Windows, use npm to install
[run-rs](https://www.npmjs.com/package/run-rs) globally and run
`run-rs --version 4.0.0`. Run-rs will download MongoDB 4.0.0 for you.

To use transactions with Mongoose, you should use Mongoose `>= 5.2.0`. To
check your current version of Mongoose, run `npm list | grep "mongoose"`
or check the [`mongoose.version` property](http://mongoosejs.com/docs/api.html#mongoose_Mongoose-version).

Transactions are built on [MongoDB sessions](https://docs.mongodb.com/manual/reference/server-sessions/).
To start a transaction, you first need to call [`startSession()`](/docs/api.html#startsession_startSession)
and then call the session's `startTransaction()` function. To execute an
operation in a transaction, you need to pass the `session` as an option.
To create a transaction, you first need to create a session using or [`Mongoose#startSession`](/docs/api/mongoose.html#mongoose_Mongoose-startSession)
or [`Connection#startSession()`](/docs/api/connection.html#connection_Connection-startSession).

```javascript
[require:transactions.*basic example]
const session = await mongoose.startSession();
```

In the above example, `session` is an instance of the
[MongoDB Node.js driver's `ClientSession` class](https://mongodb.github.io/node-mongodb-native/3.2/api/ClientSession.html).
Please refer to the [MongoDB driver docs](https://mongodb.github.io/node-mongodb-native/3.2/api/ClientSession.html)
for more information on what methods `session` has.

## Aborting a Transaction

The most important feature of transactions is the ability to roll back _all_
operations in the transaction using the [`abortTransaction()` function](https://docs.mongodb.com/manual/reference/method/Session.abortTransaction/).

Think about [modeling a bank account in Mongoose](https://thecodebarbarian.com/a-node-js-perspective-on-mongodb-4-transactions.html#transactions-with-mongoose).
To transfer money from account `A` to account `B`, you would decrement
`A`'s balance and increment `B`'s balance. However, if `A` only has a balance
of $5 and you try to transfer $10, you want to abort the transaction and undo
incrementing `B`'s balance.

```javascript
[require:transactions.*abort]
```

## The `withTransaction()` Helper

The previous examples explicitly create a transaction and commits it. In
practice, you'll want to use the [`session.withTransaction()` helper](https://mongodb.github.io/node-mongodb-native/3.2/api/ClientSession.html#withTransaction)
instead. The `session.withTransaction()` helper handles:
In practice, you should use either the [`session.withTransaction()` helper](https://mongodb.github.io/node-mongodb-native/3.2/api/ClientSession.html#withTransaction)
or Mongoose's `Connection#transaction()` function to run a transaction. The `session.withTransaction()` helper handles:

- Creating a transaction
- Committing the transaction if it succeeds
Expand All @@ -87,6 +51,14 @@ block content
For more information on the `ClientSession#withTransaction()` function, please see
[the MongoDB Node.js driver docs](https://mongodb.github.io/node-mongodb-native/3.2/api/ClientSession.html#withTransaction).

Mongoose's `Connection#transaction()` function is a wrapper around `withTransaction()` that
integrates Mongoose change tracking with transactions. For example, the `Connection#transaction()`
function handles resetting a document if you `save()` that document in a transaction that later fails.

```javascript
[require:transactions.*can save document after aborted transaction]
```

## With Mongoose Documents and `save()`

If you get a [Mongoose document](/docs/documents.html) from [`findOne()`](/docs/api.html#findone_findOne)
Expand All @@ -109,3 +81,18 @@ block content
```javascript
[require:transactions.*aggregate]
```

## Advanced Usage

Advanced users who want more fine-grained control over when they commit or abort transactions
can use `session.startTransaction()` to start a transaction:

```javascript
[require:transactions.*basic example]
```

You can also use `session.abortTransaction()` to abort a transaction:

```javascript
[require:transactions.*abort]
```
105 changes: 54 additions & 51 deletions test/es-next/transactions.test.es6.js
Expand Up @@ -127,25 +127,25 @@ describe('transactions', function() {
await User.createCollection();
// acquit:ignore:end
const session = await db.startSession();
session.startTransaction();

await User.create({ name: 'foo' });

const user = await User.findOne({ name: 'foo' }).session(session);
// Getter/setter for the session associated with this document.
assert.ok(user.$session());
user.name = 'bar';
// By default, `save()` uses the associated session
await user.save();

// Won't find the doc because `save()` is part of an uncommitted transaction
let doc = await User.findOne({ name: 'bar' });
assert.ok(!doc);
await session.withTransaction(async () => {
await User.create({ name: 'foo' });

const user = await User.findOne({ name: 'foo' }).session(session);
// Getter/setter for the session associated with this document.
assert.ok(user.$session());
user.name = 'bar';
// By default, `save()` uses the associated session
await user.save();

// Won't find the doc because `save()` is part of an uncommitted transaction
const doc = await User.findOne({ name: 'bar' });
assert.ok(!doc);
});

await session.commitTransaction();
session.endSession();

doc = await User.findOne({ name: 'bar' });
const doc = await User.findOne({ name: 'bar' });
assert.ok(doc);
});

Expand Down Expand Up @@ -174,35 +174,35 @@ describe('transactions', function() {
await Event.createCollection();
// acquit:ignore:end
const session = await db.startSession();
session.startTransaction();

await Event.insertMany([
{ createdAt: new Date('2018-06-01') },
{ createdAt: new Date('2018-06-02') },
{ createdAt: new Date('2017-06-01') },
{ createdAt: new Date('2017-05-31') }
], { session: session });

const res = await Event.aggregate([
{
$group: {
_id: {
month: { $month: '$createdAt' },
year: { $year: '$createdAt' }
},
count: { $sum: 1 }
}
},
{ $sort: { count: -1, '_id.year': -1, '_id.month': -1 } }
]).session(session);

assert.deepEqual(res, [
{ _id: { month: 6, year: 2018 }, count: 2 },
{ _id: { month: 6, year: 2017 }, count: 1 },
{ _id: { month: 5, year: 2017 }, count: 1 }
]);

await session.withTransaction(async () => {
await Event.insertMany([
{ createdAt: new Date('2018-06-01') },
{ createdAt: new Date('2018-06-02') },
{ createdAt: new Date('2017-06-01') },
{ createdAt: new Date('2017-05-31') }
], { session: session });

const res = await Event.aggregate([
{
$group: {
_id: {
month: { $month: '$createdAt' },
year: { $year: '$createdAt' }
},
count: { $sum: 1 }
}
},
{ $sort: { count: -1, '_id.year': -1, '_id.month': -1 } }
]).session(session);

assert.deepEqual(res, [
{ _id: { month: 6, year: 2018 }, count: 2 },
{ _id: { month: 6, year: 2017 }, count: 1 },
{ _id: { month: 5, year: 2017 }, count: 1 }
]);
});

await session.commitTransaction();
session.endSession();
});

Expand Down Expand Up @@ -372,12 +372,15 @@ describe('transactions', function() {

it('can save document after aborted transaction (gh-8380)', async function() {
const schema = Schema({ name: String, arr: [String], arr2: [String] });
// acquit:ignore:start
db.deleteModel(/Test/);
// acquit:ignore:end

const Test = db.model('gh8380', schema);
const Test = db.model('Test', schema);

await Test.createCollection();
await Test.create({ name: 'foo', arr: ['bar'], arr2: ['foo'] });
const doc = await Test.findOne();
let doc = await Test.create({ name: 'foo', arr: ['bar'], arr2: ['foo'] });
doc = await Test.findById(doc);
await db.
transaction(async (session) => {
doc.arr.pull('bar');
Expand All @@ -391,15 +394,15 @@ describe('transactions', function() {
assert.equal(err.message, 'Oops');
});

const changes = doc.$__delta()[1];
const changes = doc.getChanges();
assert.equal(changes.$set.name, 'baz');
assert.deepEqual(changes.$pullAll.arr, ['bar']);
assert.deepEqual(changes.$push.arr2, { $each: ['bar'] });
assert.ok(!changes.$set.arr2);

await doc.save({ session: null });

const newDoc = await Test.collection.findOne();
const newDoc = await Test.findById(doc);
assert.equal(newDoc.name, 'baz');
assert.deepEqual(newDoc.arr, []);
assert.deepEqual(newDoc.arr2, ['foo', 'bar']);
Expand All @@ -408,7 +411,7 @@ describe('transactions', function() {
it('can save a new document with an array', async function () {
const schema = Schema({ arr: [String] });

const Test = db.model('new_doc_array', schema);
const Test = db.model('new_doc_array1', schema);

await Test.createCollection();
const doc = new Test({ arr: ['foo'] });
Expand All @@ -423,14 +426,14 @@ describe('transactions', function() {
it('can save a new document with an array and read within transaction', async function () {
const schema = Schema({ arr: [String] });

const Test = db.model('new_doc_array', schema);
const Test = db.model('new_doc_array2', schema);

await Test.createCollection();
const doc = new Test({ arr: ['foo'] });
await db.transaction(
async (session) => {
await doc.save({ session });
const testDocs = await Test.collection.find({}).session(session);
const testDocs = await Test.find({}).session(session);
assert.deepStrictEqual(testDocs.length, 1);
},
{ readPreference: 'primary' }
Expand Down

0 comments on commit f2651d7

Please sign in to comment.