MongoDB 4.x はトランザクションをサポートしている。
巷には様々なプログラミング言語から MongoDB のトランザクションを試す記事が上がっているが、 Mongoid も例にもれずトランザクションに対応しているので、これを試してみる。
MongoDB のトランザクション
詳しいことは 公式ドキュメント を参照してもらうのが良い。
MongoDB のトランザクションは以下のような事柄に対応しており、この記事ではそれらについて試す。
- 1件のドキュメントをコミット
- ロールバック
- 複数ドキュメントを一括で処理
- 異なるコレクションのドキュメントを一括で処理
1. 1件のドキュメントをコミット
まずは rails console でトランザクション内で1件のドキュメントを作成してコミットしてみる。
irb(main):001:0> Item.with_session do |s|
irb(main):002:1* s.start_transaction
irb(main):003:1> Item.create(name: 'item01')
irb(main):004:1> s.commit_transaction
irb(main):005:1> end
MONGODB | [9] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "startTransaction"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df8f7f921b6200c6571f5af'), "name"=>"item01"}], "txnNumber"=>#<BSON::Int64:0x00007fd8e229e768 @value=1>, "lsid"=>{"id"=><BSON::Bi...
MONGODB | [9] mongo:30002 | sandbox.insert | SUCCEEDED | 0.001s
MONGODB | [10] mongo:30002 #1 | admin.commitTransaction | STARTED | {"commitTransaction"=>1, "autocommit"=>false, "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8e2350620 @seconds=1576597490, @increment=2>, "signature"=>{"hash"=><BSON::Binary:0x70284742394300 type=generic data=0x0000000000000000...>, "keyI...
MONGODB | [10] mongo:30002 | admin.commitTransaction | SUCCEEDED | 0.001s
=> true
with_session
のブロックが完了した時点で処理が走る。
デフォルトではトランザクションは60秒でタイムアウトするので、 start_transaction
と commit_transaction
の間に sleep を入れると違う結果が得られる。
irb(main):006:0> Item.with_session do |s|
irb(main):007:1* s.start_transaction
irb(main):008:1> Item.create(name: 'item02')
irb(main):009:1> sleep(90)
irb(main):010:1> s.commit_transaction
irb(main):011:1> end
MONGODB | [11] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "startTransaction"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df8fb5221b6200c6571f5b3'), "name"=>"item02"}], "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8f2ac52d8 @seconds=157...
MONGODB | [11] mongo:30002 | sandbox.insert | SUCCEEDED | 0.002s
MONGODB | [12] mongo:30002 #1 | admin.commitTransaction | STARTED | {"commitTransaction"=>1, "autocommit"=>false, "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8f2b96220 @seconds=1576598346, @increment=1>, "signature"=>{"hash"=><BSON::Binary:0x70284880949340 type=generic data=0x0000000000000000...>, "keyI...
MONGODB | [12] mongo:30002 | admin.commitTransaction | FAILED | Transaction 2 has been aborted. (251) | 0.004872s
Traceback (most recent call last):
2: from (irb):23
1: from (irb):27:in `block in irb_binding'
Mongo::Error::OperationFailure (Transaction 5 has been aborted. (251) (on mongo:30002, attempt 1))
2. ロールバック
abort_transaction
すればロールバックできる。
irb(main):001:0> Item.with_session do |s|
irb(main):002:1* s.start_transaction
irb(main):003:1> Item.create(name: 'item03')
irb(main):004:1> s.abort_transaction
irb(main):005:1> end
MONGODB | [9] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "startTransaction"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df8fd3f21b6204af471f5af'), "name"=>"item03"}], "txnNumber"=>#<BSON::Int64:0x00007fd8f2757858 @value=1>, "lsid"=>{"id"=><BSON::Bi...
MONGODB | [9] mongo:30002 | sandbox.insert | SUCCEEDED | 0.001s
MONGODB | [10] mongo:30002 #1 | admin.abortTransaction | STARTED | {"abortTransaction"=>1, "autocommit"=>false, "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8c20af2d8 @seconds=1576598846, @increment=1>, "signature"=>{"hash"=><BSON::Binary:0x70284472580280 type=generic data=0x0000000000000000...>, "keyId...
MONGODB | [10] mongo:30002 | admin.abortTransaction | SUCCEEDED | 0.002s
=> true
irb(main):006:0> Item.where(name: 'item03').first
MONGODB | [11] mongo:30002 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item03"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8c20af2d8 @seconds=1576598846, @increment=1>, "signature"=>{"hash"=><BSON::Binary:0x7028447...
MONGODB | [11] mongo:30002 | sandbox.find | SUCCEEDED | 0.002s
=> nil
3. 複数ドキュメントを一括で処理
トランザクションが機能していることを確認するために Mongoid::Timestamps
を使う。
class Item
include Mongoid::Document
include Mongoid::Timestamps
field :name
end
2件のドキュメントを連続して作成してみる(そして間に sleep を挟む)。
irb(main):001:0> Item.with_session do |s|
irb(main):002:1* s.start_transaction
irb(main):003:1> Item.create(name: 'item01')
irb(main):004:1> sleep(10)
irb(main):005:1> Item.create(name: 'item02')
irb(main):006:1> s.commit_transaction
irb(main):007:1> end
MONGODB | [9] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "startTransaction"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df9fd3721b620cf5d71f5af'), "name"=>"item01", "updated_at"=>2019-12-18 10:19:35 UTC, "created_at"=>2019-12-18 10:19:35 UTC}], "tx...
MONGODB | [9] mongo:30002 | sandbox.insert | SUCCEEDED | 0.002s
MONGODB | [10] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df9fd4121b620cf5d71f5b0'), "name"=>"item02", "updated_at"=>2019-12-18 10:19:45 UTC, "created_at"=>2019-12-18 10:19:45 UTC}], "$clusterTime"=>{"clusterTime...
MONGODB | [10] mongo:30002 | sandbox.insert | SUCCEEDED | 0.002s
MONGODB | [11] mongo:30002 #1 | admin.commitTransaction | STARTED | {"commitTransaction"=>1, "autocommit"=>false, "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8d24b0520 @seconds=1576664379, @increment=1>, "signature"=>{"hash"=><BSON::Binary:0x70284608897460 type=generic data=0x0000000000000000...>, "keyI...
MONGODB | [11] mongo:30002 | admin.commitTransaction | SUCCEEDED | 0.002s
=> true
そして上記を実行してから mongo shell でドキュメントを確認する。
mongo-set:PRIMARY> new Date()
ISODate("2019-12-18T10:19:39.393Z")
mongo-set:PRIMARY> db.items.find()
mongo-set:PRIMARY> db.items.find()
{ "_id" : ObjectId("5df9fd3721b620cf5d71f5af"), "name" : "item01", "updated_at" : ISODate("2019-12-18T10:19:35.398Z"), "created_at" : ISODate("2019-12-18T10:19:35.398Z") }
{ "_id" : ObjectId("5df9fd4121b620cf5d71f5b0"), "name" : "item02", "updated_at" : ISODate("2019-12-18T10:19:45.411Z"), "created_at" : ISODate("2019-12-18T10:19:45.411Z") }
1件目の created_at
よりも後に1回目の db.items.find()
を実行しているが、この時点では1件も保存されていないことがわかる。
4. 異なるコレクションのドキュメントを一括で処理
もう1つモデルを用意し、異なるコレクションをまとめて処理するケースを試す。
class Owner
include Mongoid::Document
include Mongoid::Timestamps
field :name
end
Item モデルと Owner モデルの間にリレーションは存在しない。
先ほどと同じ手順で実行する。
irb(main):020:0> Item.with_session do |s|
irb(main):021:1* s.start_transaction
irb(main):022:1> Owner.create(name: 'test owner')
irb(main):023:1> sleep(20)
irb(main):024:1> Item.create(name: 'item03')
irb(main):025:1> s.commit_transaction
irb(main):026:1> end
MONGODB | [14] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"owners", "ordered"=>true, "startTransaction"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df9ff8021b620cf5d71f5b2'), "name"=>"test owner", "updated_at"=>2019-12-18 10:29:20 UTC, "created_at"=>2019-12-18 10:29:20 UTC}]...
MONGODB | [14] mongo:30002 | sandbox.insert | SUCCEEDED | 0.002s
MONGODB | [15] mongo:30002 #1 | sandbox.insert | STARTED | {"insert"=>"items", "ordered"=>true, "autocommit"=>false, "documents"=>[{"_id"=>BSON::ObjectId('5df9ff9421b620cf5d71f5b3'), "name"=>"item03", "updated_at"=>2019-12-18 10:29:40 UTC, "created_at"=>2019-12-18 10:29:40 UTC}], "$clusterTime"=>{"clusterTime...
MONGODB | [15] mongo:30002 | sandbox.insert | SUCCEEDED | 0.002s
MONGODB | [16] mongo:30002 #1 | admin.commitTransaction | STARTED | {"commitTransaction"=>1, "autocommit"=>false, "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00007fd8d23f1dc8 @seconds=1576664979, @increment=1>, "signature"=>{"hash"=><BSON::Binary:0x70284608507340 type=generic data=0x0000000000000000...>, "keyI...
MONGODB | [16] mongo:30002 | admin.commitTransaction | SUCCEEDED | 0.002s
=> true
上記を実行してから現在時刻および両コレクションの値を取得する。
mongo-set:PRIMARY> new Date()
ISODate("2019-12-18T10:29:25.613Z")
mongo-set:PRIMARY> db.items.find()
{ "_id" : ObjectId("5df9fd3721b620cf5d71f5af"), "name" : "item01", "updated_at" : ISODate("2019-12-18T10:19:35.398Z"), "created_at" : ISODate("2019-12-18T10:19:35.398Z") }
{ "_id" : ObjectId("5df9fd4121b620cf5d71f5b0"), "name" : "item02", "updated_at" : ISODate("2019-12-18T10:19:45.411Z"), "created_at" : ISODate("2019-12-18T10:19:45.411Z") }
mongo-set:PRIMARY> db.owners.find()
mongo-set:PRIMARY>
mongo-set:PRIMARY> db.items.find()
{ "_id" : ObjectId("5df9fd3721b620cf5d71f5af"), "name" : "item01", "updated_at" : ISODate("2019-12-18T10:19:35.398Z"), "created_at" : ISODate("2019-12-18T10:19:35.398Z") }
{ "_id" : ObjectId("5df9fd4121b620cf5d71f5b0"), "name" : "item02", "updated_at" : ISODate("2019-12-18T10:19:45.411Z"), "created_at" : ISODate("2019-12-18T10:19:45.411Z") }
{ "_id" : ObjectId("5df9ff9421b620cf5d71f5b3"), "name" : "item03", "updated_at" : ISODate("2019-12-18T10:29:40.162Z"), "created_at" : ISODate("2019-12-18T10:29:40.162Z") }
mongo-set:PRIMARY> db.owners.find()
{ "_id" : ObjectId("5df9ff8021b620cf5d71f5b2"), "name" : "test owner", "updated_at" : ISODate("2019-12-18T10:29:20.158Z"), "created_at" : ISODate("2019-12-18T10:29:20.158Z") }
トランザクションの一連の処理が完了するまでは2件とも書き込まれていないことがわかる。
また owners への create が mongo shell での現在時刻の確認よりも前であることもわかる。