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

MongoDB のトランザクション中にできないこと

MongoDB のトランザクションには様々な制約がある。
ここではそれらについて整理する。

コレクションの作成はできない

MongoDB では存在しないコレクションに対して insert を行うことで自動的にコレクションが生成されるが、トランザクションの途中ではこれができない。

mongo-set:PRIMARY> session = db.getMongo().startSession()
session { "id" : UUID("5538d59a-3e07-4760-8dba-f4565cd49971") }
mongo-set:PRIMARY> books = session.getDatabase('sandbox').books;
sandbox.books
mongo-set:PRIMARY> session.startTransaction()
mongo-set:PRIMARY> books.insert({name: 'dummy data'})
WriteResult({
        "nInserted" : 0,
        "writeError" : {
                "code" : 263,
                "errmsg" : "Cannot create namespace sandbox.books in multi-document transaction."
        }
})
mongo-set:PRIMARY> session.abortTransaction()
mongo-set:PRIMARY> books.insert({name: 'first book'})
WriteResult({ "nInserted" : 1 })

上記を見ると、 session.startTransaction() の直後の books コレクションへの insert で失敗している。
その後、 session.abortTransaction() でトランザクションを終了してから同様の処理をすると、こちらは成功している。

つまり、トランザクションを使うのであれば、その中で利用する可能性のあるコレクションについては、開始時点で1件もドキュメントがないとしてもコレクションの枠だけは先に作っておく必要がある。

同様にコレクションの削除やインデックスの追加、削除等もできない(が、トランザクションの途中でコレクションを削除しようとはしないと思う)。

count() を実行できない

トランザクションの途中での非 CRUD は基本的にできない。
たとえば、わかりやすいところだと count() でエラーが起きる。

mongo-set:PRIMARY> items = session.getDatabase('sandbox').items;
sandbox.items
mongo-set:PRIMARY> db.items.count()
3
mongo-set:PRIMARY> items.count()
3
mongo-set:PRIMARY> session.startTransaction()
mongo-set:PRIMARY> db.items.count()
3
mongo-set:PRIMARY> items.count()
2019-12-19T00:42:14.301+0900 E  QUERY    [js] uncaught exception: Error: count failed: {
        "operationTime" : Timestamp(1576683733, 1),
        "ok" : 0,
        "errmsg" : "Cannot run 'count' in a multi-document transaction. Please see http://dochub.mongodb.org/core/transaction-count for a recommended alternative.",
        "code" : 263,
        "codeName" : "OperationNotSupportedInTransaction",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1576683733, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
} :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
DBQuery.prototype.count@src/mongo/shell/query.js:376:11
DBCollection.prototype.count@src/mongo/shell/collection.js:1368:12
@(shell):1:1

少し冗長だが、上記を見るとトランザクション開始前は items.count() で件数を取得できるのに、トランザクション開始後に同じコマンドを実行するとエラーになっている。
一方でセッションから独立している db.items.count() はトランザクションの開始前後を問わず件数を返している。

件数の取得

count() が使えないので件数が取得できないかというと、そうではない。
MongoDB 4.0.3 以降に用意されている countDocuments() を使えばトランザクションの途中で件数を取得できる。

mongo-set:PRIMARY> session.startTransaction()
mongo-set:PRIMARY> items.countDocuments({})
3

トランザクション中の count() の挙動

先ほど、 items.count() はダメだが db.items.count() はできると書いたが、トランザクション内で件数の増減は反映される。

mongo-set:PRIMARY> db.items.count()
3
mongo-set:PRIMARY> db.items.distinct('name')
[ "item01", "item02", "item03" ]
mongo-set:PRIMARY> items.count()
3
mongo-set:PRIMARY> items.distinct('name')
[ "item01", "item02", "item03" ]
mongo-set:PRIMARY> session.startTransaction()
mongo-set:PRIMARY> items.insert({name: 'new item'})
WriteResult({ "nInserted" : 1 })
mongo-set:PRIMARY> db.items.count()
4
mongo-set:PRIMARY> db.items.distinct('name')
[ "item01", "item02", "item03" ]
mongo-set:PRIMARY> items.count()
// エラー出力(省略)
mongo-set:PRIMARY> items.distinct('name')
[ "item01", "item02", "item03", "new item" ]
mongo-set:PRIMARY> session.abortTransaction()
mongo-set:PRIMARY> db.items.count()
3
mongo-set:PRIMARY> db.items.distinct('name')
[ "item01", "item02", "item03" ]
mongo-set:PRIMARY> items.count()
3
mongo-set:PRIMARY> items.distinct('name')
[ "item01", "item02", "item03" ]

db.items.count() の結果はトランザクションの途中でもリアルタイムに反映されているが db.items.distinct() の方には反映されていない。

この結果を考えると、トランザクションの途中では 最終的にコミットされるかロールバックされるかわからない件数の増減が反映されてしまう ため、注意が必要である。
これはトランザクションを実行している mongo shell とは別に 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
ユーザーは見つかりませんでした