著者は、 Open Internet / Free Speech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

transaction は、トランザクション内のすべての操作が正しく実行された場合にのみ成功する一連のデータベース操作です。 トランザクションは、長年にわたってリレーショナルデータベースの重要な機能でしたが、最近までドキュメント指向データベースにはほとんど存在していませんでした。 ドキュメント指向データベースの性質(単一のドキュメントを、単純な値だけでなく埋め込みドキュメントと配列を含む、堅牢なネストされた構造にすることができます)は、単一のドキュメント内に関連データを格納することを合理化します。 そのため、単一の論理操作の一部として複数のドキュメントを変更する必要がない場合が多く、多くのアプリケーションでトランザクションの必要性が制限されます。

ただし、ドキュメント指向のデータベースでも、整合性が保証された1回の操作で複数のドキュメントにアクセスして変更する必要があるアプリケーションがあります。 MongoDBは、このようなユースケースのニーズを満たすために、データベースエンジンのバージョン4.0でマルチドキュメントACIDトランザクションを導入しました。 この記事では、トランザクションとは何か、トランザクションのACIDプロパティ、およびMongoDBでトランザクションを使用する方法について説明します。

前提条件

MongoDBでの実装方法により、トランザクションは、より大きなクラスターの一部として実行されているMongoDBインスタンスでのみ実行できます。 これは、シャーディングされたデータベースクラスターまたはレプリカセットのいずれかです。 テスト目的で使用できる既存のシャーディングされたMongoDBクラスターまたはレプリカセットを実行している場合は、次のセクションに進んでACIDトランザクションについて学習できます。

ただし、適切で機能的なレプリカセットまたはシャーディングされたMongoDBクラスターをセットアップするには、少なくとも3つのMongoDBインスタンスを実行する必要があり、理想的には別々のサーバーで実行する必要があります。 さらに、このガイドの例はすべて、レプリカセットのメンバーとして実行されている単一のノードでの作業を含みます。 このガイドでは、複数のサーバーをプロビジョニングして各サーバーでMongoDBを構成し、そのうちの1つだけを使用するのではなく、スタンドアロンのMongoDBインスタンスを使用可能な単一ノードのレプリカセットに変換できます。トランザクションの実行を練習します。

このガイドの最初のステップでは、これを行う方法の概要を説明しているため、このチュートリアルを完了するには、次のものだけが必要です。

  • sudo特権を持つ通常の非rootユーザーとUFWで構成されたファイアウォールを持つ1台のサーバー。 このチュートリアルは、Ubuntu 20.04を実行しているサーバーを使用して検証されており、Ubuntu20.04のこの初期サーバーセットアップチュートリアルに従ってサーバーを準備できます。
  • サーバーにインストールされているMongoDB。 これを設定するには、 Ubuntu20.04にMongoDBをインストールする方法に関するチュートリアルに従ってください。

ACIDトランザクションを理解する

transaction は、データベース操作(読み取りや書き込みなど)のセットであり、順番に、オールオアナッシングの方法で実行されます。 つまり、これらの操作の実行結果をデータベース内に保存し、トランザクションの外部で他のデータベースクライアントに表示するには、個々の操作がすべて成功する必要があります。 いずれかの操作が正しく実行されない場合、トランザクションは中止され、トランザクションの最初から行われたすべての変更が取り消されます。 その後、データベースは、操作が行われなかったかのように以前の状態に復元されます。

トランザクションがデータベースシステムにとって重要である理由を説明するために、銀行で働いていて、顧客Aから顧客Bに送金する必要があると想像してください。 つまり、ソースアカウントの残高を減らすと同時に、宛先アカウントの残高を増やす必要があります。

2つの操作のいずれかが個別に失敗し、もう一方が実行されると、銀行の記録に一貫性がなくなります。 顧客Bはどこからともなくお金を受け取るか(顧客Aの口座の残高が減らされなかった場合)、または顧客Aは理由もなくお金を失います(残高が減ったが顧客Bが入金されなかった場合)。 結果が常に一貫していることを確認するには、両方の操作が成功するか、両方が失敗する必要があります。 トランザクションは、このような状況で特に便利であり、オールオアナッシングの実行を保証します。

このような複雑な操作を安全かつ確実に実行し、エラーや中断があってもデータの有効性を保証するデータベーストランザクションの4つのプロパティは、 ACID: atomicity consistency[ X248X]、分離、および耐久性。 データベースシステムが、トランザクションにグループ化された一連の操作に対して4つすべてを保証できる場合は、実行中に予期しないエラーが発生した場合でも、データベースが有効な状態のままになることも保証できます。

  • Atomicity は、トランザクション内のすべてのアクションが単一の作業単位として扱われ、すべてまたはまったくのいずれかが、間に何もない状態で実行されることを意味します。 ある口座から引き落とされて別の口座に追加されるお金の前の例は、原子性の原則を強調しています。 MongoDBでは、トランザクションを使用しなくても、単一のドキュメント内の更新は(ドキュメント構造がどれほど複雑でネストされていても)常にアトミックであることに注意してください。 トランザクションがより強力なアトミック性の保証を提供するのは、複数のドキュメントを扱っている場合のみです。

  • 整合性は、データベースに加えられた変更がデータベースの既存の制約に準拠する必要があることを意味します。準拠しないと、トランザクション全体が失敗します。 たとえば、操作の1つが一意のインデックスまたはスキーマ検証ルールに違反した場合、MongoDBはトランザクションを中止します。

  • Isolation は、同時に実行される個別のトランザクションを相互に分離し、どちらも他方の結果に影響を与えないという考え方です。 2つのトランザクションが同時に実行された場合、分離ルールにより、最終結果が次々に実行された場合と同じになることが保証されます。

  • Durability は、トランザクションが成功するとすぐに、クライアントがデータが適切に永続化されていることを確認できることを保証します。 ハードウェア障害や停電などでも、トランザクションが無効になることはありません。

MongoDBのトランザクションは、これらのACID原則に準拠しており、一度に複数のドキュメントを変更する必要がある場合に確実に使用できます。

ステップ1—スタンドアロンのMongoDBインスタンスをレプリカセットに変換する

前述のように、MongoDBでの実装方法により、トランザクションは、より大きなクラスターの一部として実行されているデータベースでのみ実行できます。 このクラスターは、シャーディングされたデータベースクラスターまたはレプリカセットのいずれかです。

トランザクションの実行の練習に使用できるレプリカセットまたはシャードクラスターを既に構成している場合は、この手順をスキップして、手順2でそのクラスターを使用できます。 そうでない場合、このステップでは、スタンドアロンのMongoDBインスタンスを単一ノードのレプリカセットに変換する方法の概要を説明します。

警告:この手順で構成するような単一ノードのレプリカセットは、テスト目的には役立ちますが、実稼働環境での使用には適していません。 この理由は、レプリカセットが複数の分散ノードで実行されることを意図しているためです。これは、データベースの高可用性を維持するのに役立ちます。いずれかのノードに障害が発生した場合でも、クライアントが接続できる他のノードがセットに存在します。 このシングルノードセットにはこの冗長性はなく、はレプリカセットの使用が必要な状況でのテストにのみ使用する必要があります。

MongoDBでのレプリケーションとそれに伴うセキュリティへの影響について詳しく知りたい場合は、 Ubuntu20.04でMongoDBレプリカセットを構成する方法に関するチュートリアルを確認することを強くお勧めします。

スタンドアロンのMongoDBインスタンスをレプリカセットに変換するには、お好みのテキストエディターを使用してMongoDB構成ファイルを開くことから始めます。 この例では、nanoを使用しています。

  1. sudo nano /etc/mongod.conf

このファイルの下部にある#replication:というセクションを見つけます。

/etc/mongod.conf
. . .
#replication:
. . .

ポンド記号(#)を削除して、この行のコメントを解除します。 次に、この行の下にreplSetNameディレクティブを追加し、その後にMongoDBがレプリカセットを識別するために使用する名前を追加します。

/etc/mongod.conf
. . .
replication:
  replSetName: "rs0"
. . .

この例では、replSetNameディレクティブの値は"rs0"です。 ここでは任意の名前を指定できますが、わかりやすい名前を使用すると便利な場合があります。

:レプリケーションが有効になっている場合、MongoDBでは、キーファイル認証やx.509証明書の設定など、パスワード認証以外の認証手段も構成する必要があります。 Ubuntu 20.04でMongoDBを保護する方法チュートリアルに従い、MongoDBインスタンスで認証を有効にした場合、パスワード認証のみが有効になります。

このチュートリアルでは、より高度なセキュリティ対策を設定するのではなく、mongod.confファイルのsecurityブロックを無効にすることをお勧めします。 これを行うには、securityブロックのすべての行をポンド記号でコメントアウトします。

/etc/mongod.conf
. . .

#security:
#  authorization: enabled

. . .

このデータベースをトランザクションの実行またはその他のテスト目的でのみ使用することを計画している限り、これによってセキュリティ上のリスクが生じることはありません。 ただし、将来このMongoDBインスタンスを使用して機密データを保存する予定の場合は、これらの行のコメントを解除して、認証を再度有効にしてください

このファイルに加える必要がある変更はこれらだけなので、保存して閉じることができます。 nanoを使用してファイルを編集した場合は、CTRL + XYENTERの順に押すと編集できます。

その後、mongodサービスを再起動して、新しい構成の変更を実装します。

  1. sudo systemctl restart mongod

サービスを再起動した後、MongoDBシェルを開いて、サーバーで実行されているMongoDBインスタンスに接続します。

  1. mongo

MongoDBプロンプトから、次のrs.initiate()メソッドを実行します。 これにより、スタンドアロンのMongoDBインスタンスが、テストに使用できる単一ノードのレプリカセットに変わります。

  1. rs.initiate()

このメソッドが出力に"ok" : 1を返す場合は、レプリカセットが正常に開始されたことを意味します。

Output
{ . . . "ok" : 1, . . .

この場合、MongoDBシェルプロンプトが変化して、シェルが接続されているインスタンスがrs0レプリカセットのメンバーになったことを示します。

このプロンプト例は、このMongoDBインスタンスがレプリカセットのセカンダリメンバーであることを反映していることに注意してください。 通常、レプリカセットが開始されてから、そのメンバーの1つがプライマリメンバーに昇格するまでの間にギャップがあるため、これは予想されることです。

コマンドを実行する場合、またはしばらく待ってからENTERを押すと、プロンプトが更新され、レプリカセットのプライマリメンバーに接続していることが反映されます。

これで、スタンドアロンのMongoDBインスタンスが、トランザクションのテストに使用できる単一ノードのレプリカセットとして実行されます。 次のステップでMongoDBシェルを使用してサンプルコレクションを作成し、それにサンプルデータを挿入するため、今のところプロンプトを開いたままにします。

ステップ2—サンプルデータの準備

MongoDBのトランザクションがどのように機能し、どのように使用するかを説明するために、このステップでは、MongoDBシェルを開いてレプリカセットのプライマリノードに接続する方法の概要を説明します。 また、サンプルコレクションを作成し、それにいくつかのサンプルドキュメントを挿入する方法についても説明します。 このガイドでは、このサンプルデータを使用して、トランザクションを開始および実行する方法を説明します。

既存のシャーディングされたMongoDBクラスターまたはレプリカセットがあるために前の手順をスキップした場合は、データを書き込むことができる任意のノードに接続します。

  1. mongo

注:新しい接続では、MongoDBシェルはデフォルトでtestデータベースに自動的に接続します。 このデータベースを安全に使用して、MongoDBとMongoDBシェルを試すことができます。

または、別のデータベースに切り替えて、このチュートリアルに記載されているすべてのコマンド例を実行することもできます。 別のデータベースに切り替えるには、useコマンドに続けて、データベースの名前を実行します。

  1. use database_name

トランザクションの動作を理解するには、操作する一連のドキュメントが必要です。 このガイドでは、世界で最も人口の多い都市のいくつかを表すコレクションドキュメントを使用します。 例として、次のサンプルドキュメントは東京を表しています。

東京文書
{
    "name": "Tokyo",
    "country": "Japan",
    "continent": "Asia",
    "population": 37.400
}

このドキュメントには、次の情報が含まれています。

  • name:都市の名前。
  • country:都市が存在する国。
  • continent:都市が位置する大陸。
  • population:都市の人口(百万単位)。

次のinsertMany()メソッドを実行します。これにより、citiesという名前のコレクションが同時に作成され、3つのドキュメントが挿入されます。

  1. db.cities.insertMany([
  2. {"name": "Tokyo", "country": "Japan", "continent": "Asia", "population": 37.400 },
  3. {"name": "Delhi", "country": "India", "continent": "Asia", "population": 28.514 },
  4. {"name": "Seoul", "country": "South Korea", "continent": "Asia", "population": 25.674 }
  5. ])

出力には、新しく挿入されたオブジェクトに割り当てられたオブジェクト識別子のリストが含まれます。

Output
{ "acknowledged" : true, "insertedIds" : [ ObjectId("61646915c66c110cc07ca59b"), ObjectId("61646915c66c110cc07ca59c"), ObjectId("61646915c66c110cc07ca59d") ] }

引数なしでfind()メソッドを実行すると、ドキュメントが正しく挿入されたことを確認できます。これにより、citiesコレクション内のすべてのドキュメントが取得されます。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

最後に、createIndex()メソッドを使用して、コレクション内のすべてのドキュメントが一意のnameフィールド値を持つようにするインデックスを作成します。 これは、このガイドの後半でトランザクションを実行するときに整合性要件をテストするのに役立ちます。

  1. db.cities.createIndex( { "name": 1 }, { "unique": true } )

MongoDBは、インデックスが正常に作成されたことを確認します。

Output
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "commitQuorum" : "votingMembers", "ok" : 1, . . . }

これで、トランザクションの使用をテストするためのテストデータとして機能する、最も人口の多い都市のサンプルドキュメントのリストが正常に作成されました。 次に、トランザクションを設定する方法を学習します。

ステップ3—最初の完全なトランザクションを作成する

このステップでは、前のステップのサンプルコレクションに新しいドキュメントを挿入する単一の操作で構成されるトランザクションを作成する方法の概要を説明します。

2つの別々のMongoDBシェルセッションを開くことから始めます。 1つはトランザクションでコマンドを実行するために使用され、もう1つは、さまざまな時点でトランザクション外のデータベースの他のユーザーが利用できるデータを確認できるようにします。

:わかりやすくするために、このガイドでは、異なる色のコードブロックを使用して、これら2つの環境を区別します。 トランザクションの実行に使用する最初のインスタンスは、次のように青い背景になります。

2番目のインスタンスはトランザクションの外部にあり、トランザクション内で行った変更がトランザクション外のクライアントにどのように表示されるかを確認できます。 この2番目の環境は、次のように背景が赤になります。

この時点で、citiesコレクションをクエリすると、両方のシェルセッションに前に挿入した3つの都市が一覧表示されます。 最初のセッションから始めて、両方のシェルセッションでfind()クエリを発行して、次のことを確認します。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

次に、2番目のシェルセッションで同じクエリを実行します。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

このクエリの出力が両方のセッションで一貫していることを確認した後、コレクションに新しいドキュメントを挿入してみてください。 ただし、insertOne()メソッドを使用する代わりに、このドキュメントをトランザクションの一部として挿入します。

このガイドで概説されているように、通常、トランザクションはMongoDBシェルから作成および実行されません。 多くの場合、トランザクションは代わりに外部アプリケーションによって使用されます。 実行するトランザクションがアトミックで一貫性があり、分離され、耐久性があることを保証するには、アプリケーションはセッションを開始する必要があります。

MongoDBでは、セッションは、適切なMongoDBドライバーを介してアプリケーションによって管理されるデータベースオブジェクトです。 これにより、ドライバーは一連のデータベースステートメントを相互に関連付けることができます。つまり、ドライバーは共有コンテキストを持ち、トランザクションの使用を有効にするなど、グループとして追加の構成を適用できます。 このステップで説明するように、単一のセッション内で発生したことは、外の世界にはすぐには見えない場合があります。

このチュートリアルでは、外部アプリケーションを設定するのではなく、単純化されたJavaScript構文を使用してMongoDBシェルで直接トランザクションを操作するために必要な概念と個々の手順の概要を説明します。

公式のMongoDBドキュメントで、さまざまなプログラミング言語を使用してトランザクションを使用する方法の詳細を学ぶことができます。 公式ドキュメントで説明されているコード例は、このガイドに含まれているものよりも複雑になりますが、どちらの方法も同じ原則に従います。

このガイドでは、アプリケーションではなくMongoDBシェルを介してトランザクションを使用する方法の概要を説明していますが、一連の操作をトランザクションとして実行するには、セッションを開始する必要があります。 次のコマンドでセッションを開始できます。

  1. var session = db.getMongo().startSession()

このコマンドは、セッションオブジェクトを格納するsession変数を作成します。 次の例でsessionオブジェクトを参照するたびに、開始したばかりのセッションを参照していることになります。

このセッションオブジェクトが使用可能になったら、次のようにstartTransactionメソッドを呼び出してトランザクションを開始できます。

  1. session.startTransaction({
  2. "readConcern": { "level": "snapshot" },
  3. "writeConcern": { "w": "majority" }
  4. })

前の手順のコマンドのように、メソッドがdbではなく、session変数で呼び出されることに注意してください。 startTransaction()メソッドは、readConcernwriteConcernの2つのオプションを受け入れます。 writeConcern設定はいくつかのオプションを受け入れることができますが、この例にはwオプションのみが含まれています。このオプションは、トランザクションの書き込み操作が、集まる。 この例では、単一の番号ではなく、ノードのmajorityが書き込み操作を確認した場合にのみトランザクションが正常に保存されたと見なされることを指定しています。

トランザクションを開始したとしましょう。ただし、開始した後、別のユーザーが、クラスター内の別のノードで使用しているコレクションにドキュメントを追加します。 トランザクションはこの新しいデータを読み取る必要がありますか、それともトランザクションが開始されたノードに書き込まれたデータのみを読み取る必要がありますか? readConcernレベルを設定すると、トランザクションをコミットするときにトランザクションが読み取るデータを指定できます。 snapshotに設定すると、トランザクションは、クラスター内の大多数のノードによってコミットされたデータのスナップショットを読み取ることを意味します。

トランザクションのreadConcernレベルを設定するには、writeConcernmajorityに設定する必要があることに注意してください。 読み取りと書き込みに関するこれらの値は、ほとんどの場合に従うべき安全なデフォルトです。 レプリカセット全体のパフォーマンスと書き込みの確認に非常に特別な要件がない限り、データの永続性を確実に保証します。 公式のMongoDBドキュメントで、MongoDBがトランザクションで使用するために提供するさまざまな書き込みと読み取りの懸念事項について詳しく知ることができます。

startTransactionメソッドは、正しく実行された場合、出力を返しません。 このメソッドが成功した場合は、実行中のトランザクション内にいて、トランザクションの一部となるステートメントの実行を開始できます。

警告:デフォルトでは、MongoDBは60秒を超えて実行されるすべてのトランザクションを自動的に中止します。 これは、トランザクションがMongoDBシェルでインタラクティブに構築されるように設計されているのではなく、実際のアプリケーションで使用されるように設計されているためです。

このため、60秒の制限時間内に各コマンドを実行しないと、このチュートリアルの実行中に予期しないエラーが発生する可能性があります。 次のようなエラーが発生した場合は、制限時間を超えたためにMongoDBがトランザクションを中止したことを意味します。

エラーメッセージ
Error: error: {
        "errorLabels" : [
                "TransientTransactionError"
        ],
        "operationTime" : Timestamp(1634032826, 1),
        "ok" : 0,
        "errmsg" : "Transaction 1 has been aborted.",
        "code" : 251,
        "codeName" : "NoSuchTransaction",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1634032826, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

これが発生した場合は、次のようにabortTransaction()メソッドを実行して、トランザクションを終了済みとしてマークする必要があります。

  1. session.abortTransaction()

次に、前に実行したのと同じstartTransaction()メソッドを使用してトランザクションを再初期化する必要があります。

  1. session.startTransaction({
  2. "readConcern": { "level": "snapshot" },
  3. "writeConcern": { "w": "majority" }
  4. })

その後、トランザクションの各ステートメントを最初から再度実行する必要があります。 このことを念頭に置いて、最初にこのステップの残りの部分を読み、関連する概念をよりよく理解したら、60秒の制限時間内にコマンドを実行すると役立つ場合があります。

実行中のトランザクション内で作業している間、トランザクションの一部として実行するステートメントは、前に作成したsession変数で表されるセッションの共有コンテキスト内にある必要があります。

同様の目的で、実行中のトランザクションで作業する場合は、セッションのコンテキストで操作するコレクションを表す別の変数を作成すると便利です。 次の操作では、testデータベースからcitiesコレクションを返すことにより、citiesという変数を作成します。 ただし、これをdbオブジェクトから直接プルする代わりに、sessionオブジェクトを参照して、この変数が実行中のセッションのコンテキストでcitiesコレクションを表すようにします。

  1. var cities = session.getDatabase('test').getCollection('cities')

これからトランザクションをコミットするまで、db.citiesを使用してcitiesコレクションを参照するのと同じように、cities変数を使用できます。 この新しく割り当てられた変数は、セッションで、同様に、開始されたトランザクションでステートメントを実行していることを保証します。

オブジェクトを使用してコレクションからドキュメントを検索できることを確認して、これをテストします。

  1. cities.find()

データがまだ変更されていないため、コマンドは以前と同じドキュメントのリストを返します。

Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

その後、実行中のトランザクションの一部として、ニューヨーク市を表す新しいドキュメントをコレクションに挿入します。 insertOneメソッドを使用しますが、cities変数で実行して、セッションで実行されることを確認します。

  1. cities.insertOne({"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 })

MongoDBは成功メッセージを返します:

Output
{ "acknowledged" : true, "insertedId" : ObjectId("6164849d53abeea9d9dd10cf") }

cities.find()を再度実行すると、新しく挿入されたドキュメントが同じセッションですぐに表示されることがわかります。

  1. cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164822453abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

ただし、2番目のMongoDBシェルインスタンスでdb.cities.find()を実行すると、ニューヨークを表すドキュメントは存在しません。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 }

これは、実行中のトランザクション内で挿入ステートメントが実行されたが、トランザクション自体がまだコミットされていないためです。 この時点で、トランザクションは引き続き成功してデータを永続化するか、失敗する可能性があります。これにより、すべての変更が取り消され、データベースがトランザクションを開始する前と同じ状態のままになります。

トランザクションをコミットし、挿入されたドキュメントをデータベースに永続的に保存するには、セッションオブジェクトでcommitTransactionメソッドを実行します。

  1. session.commitTransaction()

startTransactionと同様に、このコマンドは成功した場合に出力を提供しません。

次に、両方のMongoDBシェルのcitiesコレクションのドキュメントを一覧表示します。 実行中のセッションでcities変数のクエリを開始します。

  1. cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164822453abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

次に、トランザクションの外部で実行されている2番目のシェルでcitiesコレクションをクエリします。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

今回は、新しく挿入されたドキュメントがセッション内とセッション外の両方に表示されます。 トランザクションはコミットされて正常に終了し、データベースに加えられた変更が保持されます。 これで、New Yorkオブジェクトに、トランザクションの外部とそれ以降のすべてのトランザクションの内部の両方でアクセスできます。

トランザクションを開始して実行する方法がわかったので、次のステップに進むことができます。このステップでは、トランザクションを開始した後にトランザクションを中止して、実行前に行った変更をロールバックする方法の概要を説明します。 このチュートリアルの残りの部分では両方を引き続き使用するため、必ず両方のMongoDBシェル環境を開いたままにしてください。

ステップ4—トランザクションを中止する

この手順は、同じ方法でトランザクションを開始するという点で、前の手順と同様のパスに従います。 ただし、この手順では、変更をコミットする代わりにトランザクションを中止する方法の概要を説明します。 そうすると、トランザクションによって導入されたすべての変更がロールバックされ、トランザクションが発生しなかったかのようにデータベースが以前の状態に戻ります。

前の手順を実行すると、ニューヨークを表す新しく追加された都市を含め、コレクションに4つの都市が含まれるようになります。

最初のMongoDBシェルで、セッションを開始し、それをsession変数に再度割り当てます。

  1. var session = db.getMongo().startSession()

次に、トランザクションを開始します。

  1. session.startTransaction({
  2. "readConcern": { "level": "snapshot" },
  3. "writeConcern": { "w": "majority" }
  4. })

繰り返しますが、このメソッドは成功した場合、出力を返しません。 成功すると、実行中のトランザクション内に移動します。

セッションコンテキスト内のcitiesコレクションに再度アクセスする必要があります。 これを行うには、セッション内のcitiesコレクションを表すcities変数を再度作成します。

  1. var cities = session.getDatabase('test').getCollection('cities')

今後は、cities変数を使用して、セッションのコンテキストでcitiesコレクションを操作できます。

トランザクションが開始されたので、実行中のトランザクションの一部として、このコレクションに別の新しいドキュメントを挿入します。 この例のドキュメントは、ブエノスアイレスを表しています。 insertOneメソッドを使用しますが、cities変数で実行して、セッションで実行されるようにします。

  1. cities.insertOne({"name": "Buenos Aires", "country": "Argentina", "continent": "South America", "population": 14.967 })

MongoDBは成功メッセージを返します:

Output
{ "acknowledged" : true, "insertedId" : ObjectId("6164887e322518cf706858b5") }

次に、cities.find()クエリを実行します。

  1. cities.find()

新しく挿入されたドキュメントは、トランザクション内の同じセッションですぐに表示されることに注意してください。

Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 } { "_id" : ObjectId("6164887e322518cf706858b5"), "name" : "Buenos Aires", "country" : "Argentina", "continent" : "South America", "population" : 14.967 }

ただし、トランザクション内で動作していない2番目のMongoDBシェルインスタンスでcitiesコレクションをクエリする場合、トランザクションがコミットされていないため、返されるリストにはブエノスアイレスのドキュメントは含まれません。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

間違いを犯し、トランザクションをコミットしたくないとしましょう。 代わりに、このセッションの一部として実行したステートメントをキャンセルし、トランザクションを完全に中止する必要があります。 これを行うには、abortTransaction()メソッドを実行します。

  1. session.abortTransaction()

abortTransaction()メソッドは、トランザクションで導入されたすべての変更を破棄し、データベースを前の状態に戻すようにMongoDBに指示します。 startTransactionおよびcommitTransactionと同様に、このコマンドは成功した場合に出力を提供しません。

トランザクションを正常に中止した後、両方のMongoDBシェルのcitiesコレクションからドキュメントを一覧表示します。 まず、実行中のセッションで次の操作を実行します。

  1. cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

次に、セッションの外部で実行されている2番目のシェルインスタンスで次のクエリを実行します。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

ブエノスアイレスは両方のリストに含まれていません。 ドキュメントが挿入された後、トランザクションがコミットされる前にトランザクションを中止することは、それが決して起こらなかったようなものです。

このステップでは、トランザクションを終了し、トランザクションの存在中に導入された変更をロールバックする方法を学習しました。 ただし、このようにトランザクションが手動で中止されるとは限りません。 多くの場合、MongoDBがトランザクションを実行する前に終了する理由は、トランザクション内の操作の1つがエラーを引き起こしたためです。

ステップ5—エラーによるトランザクションの中止

この手順は前の手順と似ていますが、今回は、トランザクション内で実行されたステートメントのいずれかでエラーが発生した場合に何が起こるかを学習します。

この時点では、まだ2つの開いているシェルセッションがあるはずです。 コレクションには、ニューヨークを表す新しく追加されたドキュメントを含む4つの都市が含まれています。 ただし、前の手順でトランザクションを中止したときに破棄されたため、ブエノスアイレスを表すドキュメントは挿入されませんでした。

最初のMongoDBシェルで、セッションを開始し、それをsession変数に割り当てます。

  1. var session = db.getMongo().startSession()

次に、トランザクションを開始します。

  1. session.startTransaction({
  2. "readConcern": { "level": "snapshot" },
  3. "writeConcern": { "w": "majority" }
  4. })

cities変数を再度作成します。

  1. var cities = session.getDatabase('test').getCollection('cities')

その後、実行中のトランザクションの一部として、このコレクションに別の新しいドキュメントを挿入します。 この例では、日本の大阪を表すドキュメントを挿入します。

  1. cities.insertOne({"name": "Osaka", "country": "Japan", "continent": "Asia", "population": 19.281 })

MongoDBは成功メッセージを返します:

Output
{ "acknowledged" : true, "insertedId" : ObjectId("61648bb3322518cf706858b6") }

新しく挿入された都市は、トランザクション内からすぐに表示されます。

  1. cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 } { "_id" : ObjectId("61648bb3322518cf706858b6"), "name" : "Osaka", "country" : "Japan", "continent" : "Asia", "population" : 19.281 }

ただし、大阪のドキュメントはトランザクションの外部にあるため、2番目のシェルには表示されません。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

トランザクションはまだ実行中であり、データベースでさらに変更を実行するために使用できます。

次の操作を実行して、このトランザクションの一部としてコレクションにもう1つのドキュメントを挿入してみてください。 このコマンドは、ニューヨーク市を表す別のドキュメントを作成します。 ただし、このコレクションを設定するときにnameフィールドに適用した一意性の制約と、このコレクションにはnameフィールドの値がNew Yorkであるドキュメントが既に含まれているため、 insertOne操作はその制約と競合し、エラーを引き起こします:

  1. cities.insertOne({"name": "New York", "country": "United States", "continent": "North America", "population": 18.819 })

MongoDBは、この操作が一意の制約に違反していることを示すエラーメッセージを返します。

Output
WriteError({ "index" : 0, "code" : 11000, "errmsg" : "E11000 duplicate key error collection: test.cities index: name_1 dup key: { name: \"New York\" }", "op" : { "_id" : ObjectId("61648bdc322518cf706858b7"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 } }) . . .

この出力は、ニューヨークを表すこの新しいドキュメントがデータベースに挿入されていないことを示しています。 ただし、これは、以前にトランザクションの一部として追加した大阪を表すドキュメントに何が起こったのかを説明するものではありません。

その2番目のニューヨークのドキュメントを追加しようとしたのは間違いでしたが、大阪のドキュメントをコレクションに保持するつもりだったとしましょう。 トランザクションをコミットして、大阪のドキュメントを永続化してみてください。

  1. session.commitTransaction()

MongoDBはこれを許可せず、代わりにエラーをスローします。

Output
uncaught exception: Error: command failed: { "errorLabels" : [ "TransientTransactionError" ], "operationTime" : Timestamp(1633979403, 1), "ok" : 0, "errmsg" : "Transaction 0 has been aborted.", "code" : 251, "codeName" : "NoSuchTransaction", "$clusterTime" : { "clusterTime" : Timestamp(1633979403, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } } } . . .

トランザクション内でエラーが発生すると、MongoDBはトランザクションを自動的に中止します。 また、トランザクションはオールオアナッシングで実行されるため、このような場合、トランザクション内からの変更は保持されません。 ニューヨークを表す2番目のドキュメントを追加することによって発生したエラーにより、MongoDBはトランザクションを中止し、大阪を表すドキュメントを破棄しました。

これは、両方のシェルでfind()クエリを実行することで確認できます。 最初のシェルで、セッションのコンテキスト内でクエリを実行します。

  1. cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

次に、セッションの外部で実行されている2番目のシェルで、db.citiesに対してfind()を実行します。

  1. db.cities.find()
Output
{ "_id" : ObjectId("61646915c66c110cc07ca59b"), "name" : "Tokyo", "country" : "Japan", "continent" : "Asia", "population" : 37.4 } { "_id" : ObjectId("61646915c66c110cc07ca59c"), "name" : "Delhi", "country" : "India", "continent" : "Asia", "population" : 28.514 } { "_id" : ObjectId("61646915c66c110cc07ca59d"), "name" : "Seoul", "country" : "South Korea", "continent" : "Asia", "population" : 25.674 } { "_id" : ObjectId("6164849d53abeea9d9dd10cf"), "name" : "New York", "country" : "United States", "continent" : "North America", "population" : 18.819 }

大阪も重複したニューヨークのエントリも表示されません。 MongoDBがトランザクションを自動的に中止すると、すべての変更が元に戻されることも確認されました。 大阪はトランザクションコンテキスト内で一時的に表示されましたが、トランザクション外で他のデータベースユーザーが利用できることはありませんでした。

結論

この記事を読むことで、MongoDBでのACIDの原則とマルチドキュメントトランザクションについて理解できました。 トランザクションを開始し、そのトランザクションの一部としてドキュメントを挿入し、ドキュメントがトランザクションの内外でいつ表示されるかを学習しました。 トランザクションをコミットする方法、トランザクションを中止して変更をロールバックする方法、およびトランザクション内でエラーが発生したときに何が起こるかを学びました。

これらの新しいスキルがあれば、必要になる可能性のあるアプリケーションでマルチドキュメントトランザクションのACID保証を活用できます。 ただし、MongoDBはドキュメント指向のデータベースであることを忘れないでください。 多くのシナリオでは、ドキュメントモデル自体と、注意深いスキーマ設計により、マルチドキュメントトランザクションを操作する必要性を減らすことができます。

このチュートリアルでは、MongoDBでのトランザクションについて簡単に紹介しました。 トランザクションの仕組みについて詳しくは、公式の公式のMongoDBドキュメントをご覧になることをお勧めします。