LoginSignup
16
14

More than 5 years have passed since last update.

MongoDB 4.0の待望の新機能Multiple Documents Transactionを試す

Last updated at Posted at 2018-07-07

はじめに

MongoDBはNoSQLなため、列の定義の拡張が難しいRDBMSに対して柔軟に列定義の拡張が可能です。また一般的にNoSQLはRDBMSに対してパフォーマンス面で優位です。
その反面、複数のドキュメント操作に失敗したときに元の状態に戻す(いわゆる、RDBMSのトランザクション制御)ということがDBレベルでサポートされていませんでした。(注:単一のドキュメント操作レベルでのACIDはいままでも保証されていました)
MongoDB 3.0から3年越しであるMongoDB 4.0では待望のMultiple Documents Transactionがサポートされました。
これはMySQLなどのRDBMSでお馴染みのTransactionと同等の機能です。
(つまりNoSQLの欠点だったRDBMSのトランザクション機能も備わって最強というわけですね)
あまり世間的に騒がれていませんが(?)、これを機にやっぱりMongoDBはイケてるぞというのを再普及させたいと思ったのでMongoDBのMultiple Documents TransactionをNodeJSで試してみます。

動作サンプル

次のMongoose作者のブログを参考にサンプルを改良しました。
A Node.js Perspective on MongoDB 4.0: Transactions

現状、ReplicaSet構成でないと動きません。
ローカル上で簡易にReplicaSet構成を構築するにはrun-rsを導入します。
次のコマンドでMongoDB4.0のReplicaSetが動きます。

$ npm install run-rs -g
$ run-rs --version 4.0.0

また、NodeJSでMongoDBを使う際によく使われるORMライブラリであるMongooseを使用します。

が必要です。

$ npm install mongodb@3.1
$ npm install mongoose@5.2

書き込み(insert)、更新(update)のサンプルファイルを作成します。
トランザクション内のクエリのoptionにsessionを渡すのを忘れないようにします。

example.js
// mongo
const mongoose = require('mongoose')
const Schema = mongoose.Schema
mongoose.Promise = global.Promise
// mongoose.set('debug', true) // enable logging collection methods + arguments to the console
mongoose.connect('mongodb://localhost:27017,localhost:27018,localhost:27019/txn', {replicaSet: 'rs'})


const schema1 = new Schema({
  name: {type: String},
}, {
  timestamps: true,
  toObject: {
    virtuals: true,
  },
  toJSON: {
    virtuals: true,
    transform: (doc, m) => {
      delete m.__v
      return m
    },
  },
})

const User = mongoose.model('User', schema1)

const schema2 = new Schema({
  msg: {type: String, required: true},
}, {
  timestamps: true,
  toObject: {
    virtuals: true,
  },
  toJSON: {
    virtuals: true,
    transform: (doc, m) => {
      delete m.__v
      return m
    },
  },
})

const Chat = mongoose.model('Chat', schema2)


const insert = async function(isFail = false) {

  // session取得(Mongooseの場合はModel.startSessionで取得)
  const session = await User.startSession()


  try {
    await session.startTransaction()

    // テーブルがないと失敗するので注意
    await User.insertMany([{name: '次郎'}],  { session })
    await Chat.insertMany([{msg: 'こんにちわ'}],  { session })
    if (isFail) {
      throw new Error('保存失敗')
    }

    await session.commitTransaction() // コミット
    console.log('--------commit-----------')
  } catch (e) {
    await session.abortTransaction() // ロールバック
    console.log('--------abort-----------')
    console.error(e)
  } finally {
    await session.endSession()
  }

  // 状態確認
  const users = await User.find()
  console.log(users)
  const chats = await Chat.find()
  console.log(chats)
}


const update = async function(isFail = false) {

  // session取得(Mongooseの場合はModel.startSessionで取得)
  const session = await User.startSession()
  try {
    await session.startTransaction({readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }})

    await User.findOneAndUpdate({name: '次郎'}, {$set: {name: '花子'}}, {session, runValidators: true, new: true})
    await Chat.findOneAndUpdate({msg: 'こんにちわ'}, {$set: {msg: 'こんばんわ'}}, {session, runValidators: true, new: true})
    if (isFail) {
      throw new Error('保存失敗')
    }

    await session.commitTransaction() // コミット
    console.log('--------commit-----------')
  } catch (e) {
    await session.abortTransaction() // ロールバック
    console.log('--------abort-----------')
    console.error(e)
  } finally {
    await session.endSession()
  }

  // 状態確認
  const users = await User.find()
  console.log(users)
  const chats = await Chat.find()
  console.log(chats)
}



async function main() {

  await mongoose.connection.dropDatabase()

  // テーブル作成
  await User.create({name: '太郎'})
  await Chat.create({msg: 'おはよう'})
  // 状態確認
  const users = await User.find()
  console.log(users)
  const chats = await Chat.find()
  console.log(chats)

  await insert(true)
  await insert()
  await update(true)
  await update()

  await mongoose.disconnect()
}

main()

次のコマンドで実行します

$node example.js

実行結果は、次のようになります。
transaction完了(session.commitTransaction)前にエラーになった場合に
session.abortTransactionでロールバックします。

$ node example.js 
[ { _id: 5b410aa1bab3f57320b8b011,
    name: '太郎',
    createdAt: 2018-07-07T18:46:57.436Z,
    updatedAt: 2018-07-07T18:46:57.436Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b011' } ]
[ { _id: 5b410aa1bab3f57320b8b012,
    msg: 'おはよう',
    createdAt: 2018-07-07T18:46:57.491Z,
    updatedAt: 2018-07-07T18:46:57.491Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b012' } ]
--------abort-----------
Error: 保存失敗
    at insert (/Users/daikiterai/Desktop/webapp/example.js:59:13)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
[ { _id: 5b410aa1bab3f57320b8b011,
    name: '太郎',
    createdAt: 2018-07-07T18:46:57.436Z,
    updatedAt: 2018-07-07T18:46:57.436Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b011' } ]
[ { _id: 5b410aa1bab3f57320b8b012,
    msg: 'おはよう',
    createdAt: 2018-07-07T18:46:57.491Z,
    updatedAt: 2018-07-07T18:46:57.491Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b012' } ]
--------commit-----------
[ { _id: 5b410aa1bab3f57320b8b011,
    name: '太郎',
    createdAt: 2018-07-07T18:46:57.436Z,
    updatedAt: 2018-07-07T18:46:57.436Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b011' },
  { _id: 5b410aa1bab3f57320b8b015,
    name: '次郎',
    __v: 0,
    createdAt: 2018-07-07T18:46:57.610Z,
    updatedAt: 2018-07-07T18:46:57.610Z,
    id: '5b410aa1bab3f57320b8b015' } ]
[ { _id: 5b410aa1bab3f57320b8b012,
    msg: 'おはよう',
    createdAt: 2018-07-07T18:46:57.491Z,
    updatedAt: 2018-07-07T18:46:57.491Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b012' },
  { _id: 5b410aa1bab3f57320b8b016,
    msg: 'こんにちわ',
    __v: 0,
    createdAt: 2018-07-07T18:46:57.611Z,
    updatedAt: 2018-07-07T18:46:57.611Z,
    id: '5b410aa1bab3f57320b8b016' } ]
--------abort-----------
Error: 保存失敗
    at update (/Users/daikiterai/Desktop/webapp/example.js:91:13)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
[ { _id: 5b410aa1bab3f57320b8b011,
    name: '太郎',
    createdAt: 2018-07-07T18:46:57.436Z,
    updatedAt: 2018-07-07T18:46:57.436Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b011' },
  { _id: 5b410aa1bab3f57320b8b015,
    name: '次郎',
    __v: 0,
    createdAt: 2018-07-07T18:46:57.610Z,
    updatedAt: 2018-07-07T18:46:57.610Z,
    id: '5b410aa1bab3f57320b8b015' } ]
[ { _id: 5b410aa1bab3f57320b8b012,
    msg: 'おはよう',
    createdAt: 2018-07-07T18:46:57.491Z,
    updatedAt: 2018-07-07T18:46:57.491Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b012' },
  { _id: 5b410aa1bab3f57320b8b016,
    msg: 'こんにちわ',
    __v: 0,
    createdAt: 2018-07-07T18:46:57.611Z,
    updatedAt: 2018-07-07T18:46:57.611Z,
    id: '5b410aa1bab3f57320b8b016' } ]
--------commit-----------
[ { _id: 5b410aa1bab3f57320b8b011,
    name: '太郎',
    createdAt: 2018-07-07T18:46:57.436Z,
    updatedAt: 2018-07-07T18:46:57.436Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b011' },
  { _id: 5b410aa1bab3f57320b8b015,
    name: '花子',
    __v: 0,
    createdAt: 2018-07-07T18:46:57.610Z,
    updatedAt: 2018-07-07T18:46:57.667Z,
    id: '5b410aa1bab3f57320b8b015' } ]
[ { _id: 5b410aa1bab3f57320b8b012,
    msg: 'おはよう',
    createdAt: 2018-07-07T18:46:57.491Z,
    updatedAt: 2018-07-07T18:46:57.491Z,
    __v: 0,
    id: '5b410aa1bab3f57320b8b012' },
  { _id: 5b410aa1bab3f57320b8b016,
    msg: 'こんばんわ',
    __v: 0,
    createdAt: 2018-07-07T18:46:57.611Z,
    updatedAt: 2018-07-07T18:46:57.669Z,
    id: '5b410aa1bab3f57320b8b016' } ]

Multiple Documents Transactionのロールバックが動作することで
複数のモデル操作が必要な処理にゴミが残らずロールバックしてくれます。
ReplicaSet構成前提でMongooseの方はModel.prototype.save、Model.createにはまだ対応していませんが、
課金周りなどの重要な処理に実装しておくと良い気がします。

16
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
14