Using annotations to simplify the use of mongoose transactions

Time:2020-3-2

Transaction provided by Mongoose

Mongodb 4.0 started to provide transaction support, and mongoose also provided the corresponding implementation. However, the current writing method is relatively cumbersome.
Let’s take a look at the demo given by Mongoose

const Customer = db.model('Customer', new Schema({ name: String }));

let session = null;
return Customer.createCollection().
  then(() => db.startSession()).
  then(_session => {
    session = _session;
    // Start a transaction
    session.startTransaction();
    // This `create()` is part of the transaction because of the `session`
    // option.
    return Customer.create([{ name: 'Test' }], { session: session });
  }).
  // Transactions execute in isolation, so unless you pass a `session`
  // to `findOne()` you won't see the document until the transaction
  // is committed.
  then(() => Customer.findOne({ name: 'Test' })).
  then(doc => assert.ok(!doc)).
  // This `findOne()` will return the doc, because passing the `session`
  // means this `findOne()` will run as part of the transaction.
  then(() => Customer.findOne({ name: 'Test' }).session(session)).
  then(doc => assert.ok(doc)).
  // Once the transaction is committed, the write operation becomes
  // visible outside of the transaction.
  then(() => session.commitTransaction()).
  then(() => Customer.findOne({ name: 'Test' })).
  then(doc => assert.ok(doc));

This demo exposes two problems:

  1. You need to commit and rollback for each transaction
  2. Transactions are distinguished by sessions. You need to pass sessions all the time

Use annotations

So aka JS provides a transaction annotation to simplify the process.

import * as mongoose from 'mongoose'
import {Schema} from 'mongoose'
import * as assert from 'assert'
import {Transactional, getSession} from './decorators/Transactional'

mongoose.connect('mongodb://localhost:27017,localhost:27018,localhost:27019/test?replicaSet=rs', {useNewUrlParser: true})
mongoose.set('debug', true)
let db = mongoose.connection
const Customer = db.model('Customer', new Schema({name: String}))

class ClassA {
  @Transactional()
  async main (key) {
    await new Customer({name: 'ClassA'}).save({session: getSession()})
    const doc1 = await Customer.findOne({name: 'ClassA'})
    assert.ok(!doc1)
    await new ClassB().step2()
    return key
  }
}

class ClassB {
  async step2 () {
    const doc2 = await Customer.findOne({name: 'ClassA'}).session(getSession())
    assert.ok(doc2)
    await Customer.remove({}).session(getSession())
  }
}

new ClassA().main('aaa').then((res) => {
  console.log('res', res)
  mongoose.disconnect(console.log)
}).catch(console.error)

Analysis:

  • @The transactional () annotation automatically commits or rolls back the transaction (when an exception occurs). See transactional.ts for the specific implementation, the core part. Try catch is used to catch exceptions and realize automatic rollback.
try {
  const value = await originalMethod.apply(this, [...args])
  // Since the mutations ran without an error, commit the transaction.
  await session.commitTransaction()
  // Return any value returned by `mutations` to make this function as transparent as possible.
  return value
} catch (error) {
  // Abort the transaction as an error has occurred in the mutations above.
  await session.abortTransaction()
  // Rethrow the error to be caught by the caller.
  throw error
} finally {
  // End the previous session.
  session.endSession()
}
  • In order to avoid the embarrassment of passing session all the time when nesting calls, akajs provides a global getsession() method, which relies on async hooks and is an experimental feature of node,

If you mind, please don’t use it in the production environment.

Be carefulMongodb transactions must be used on the replica set. To start the mongodb replica set in the development environment, run RS is recommended

Further more

Of course, it’s a little cumbersome to call the getsession() method at every place where session is needed. We can use each method of wrap mongoose to automatically inject session.

For example, replace the findone method of mongoose with

let originFindOne = mongoose.findOne
mongoose.findOne = () => {
originFindOne().session(getSession())
}

But there’s a lot of work. I don’t have time for it.

Recommended Today

Installation and configuration of node.js and NPM

Installation of node.js CentOS installation: $ dnf module list nodejs $ dnf module install nodejs:14 Windows installation: https://nodejs.org/en/download/ To view the installed version: $ node -v NPM installation Node.js has a built-in NPM package management tool, which does not need to be installed separately. To view the installed version: $ npm -v Upgrade NPM to […]