3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

MongoDBAdvent Calendar 2019

Day 15

Mongoid でトランザクションを試す

Last updated at Posted at 2019-12-18

MongoDB 4.x はトランザクションをサポートしている。

巷には様々なプログラミング言語から MongoDB のトランザクションを試す記事が上がっているが、 Mongoid も例にもれずトランザクションに対応しているので、これを試してみる。

MongoDB のトランザクション

詳しいことは 公式ドキュメント を参照してもらうのが良い。
MongoDB のトランザクションは以下のような事柄に対応しており、この記事ではそれらについて試す。

  1. 1件のドキュメントをコミット
  2. ロールバック
  3. 複数ドキュメントを一括で処理
  4. 異なるコレクションのドキュメントを一括で処理

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_transactioncommit_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 を使う。

app/models/item.rb
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つモデルを用意し、異なるコレクションをまとめて処理するケースを試す。

app/models/owner.rb
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 での現在時刻の確認よりも前であることもわかる。

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?