Help us understand the problem. What is going on with this article?

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした