はじめに
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を使用します。
- mongo-native-driver v3.1以上
- mongoose v5.2以上
が必要です。
$ npm install mongodb@3.1
$ npm install mongoose@5.2
書き込み(insert)、更新(update)のサンプルファイルを作成します。
トランザクション内のクエリのoptionにsessionを渡すのを忘れないようにします。
// 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
にはまだ対応していませんが、
課金周りなどの重要な処理に実装しておくと良い気がします。