Go言語でMongoDBのトランザクションを使う

MongoDBのトランザクションをGo言語(1.12.1)から利用してみました。

Go言語からMongoDBのを使うには、公式のドライバとmgoという人気のあるライブラリを使う方法などがありますが、mgoの方は現時点ではトランザクションに対応していないっぽいので、公式ドライバを使った方法を記載します。

# トランザクションに対応したMongoDBを用意する

前に書いた記事で一瞬で作成できます
https://qiita.com/jp_ibis/items/c00b9509fac87abb32c6

# コレクションを作成しておく

MongoDBにはデータをinsertしたら自動でDBとCollectionを作成してくれる機能があるのですが、トランザクション中には使えないようです。事前にDBとCollectionを用意しておきましょう。

MongoDBのシェルを起動してコマンドでCollectionを作成しておきます。上のリンクの記事のようにDockerでMongoDBを立てていた場合は

```bash
docker exec -it コンテナ名 mongo
```

のようにしてシェルを起動してください。MongoDBのシェルで、

```mongo
use 作成するDB名
```

としてDB名を決めて、

```mongo
db.createCollection("作成するコレクション名")
```

で、空っぽのコレクションを作っておきます。必要なのはこれだけなので、quitでmongoのシェルを抜けておきます。

# ライブラリを用意する

go mod を使うのがナウいのですが、VSCodeで補完させようとすると結局 go get も必要なんですよね(´・ω・｀)

```bash
go get -u go.mongodb.org/mongo-driver/mongo
go get -u go.mongodb.org/mongo-driver/bson
```

# コードを書く

## ライブラリのimport部

mongoで接続するときにcontextを使用するので一緒にimportしておきます。

```go
import (
	"context"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)
```

## MongoDBへの接続

ApplyURIの引数で接続先を指定します。DockerコンテナのMongoDBを使う場合は、コンテナのIPを指定してください。

```go
	// MongoDBへの接続
	ctx := context.Background()

	client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://MongoDBのIPアドレス:ポート番号(省略可)"))
	if err != nil {
		panic(err)
	}
```

## セッションを使用する

公式ドライバにはやり方が何通りもあってどれを選べばいいのかわかりにくいですが、UseSessionを使うのが一番スッキリするかな、と。UseSessionの実装の中に ```defer defaultSess.EndSession(ctx)``` というコードがあったので、自分でEndSessionを書く必要は無さそうです。

```go
	// セッション開始
	err = client.UseSession(ctx, func(sc mongo.SessionContext) error {
		// ここにトランザクションの処理を書く
	})
	if err != nil {
		panic(err)
	}
```

## トランザクションを使用する

UseSessionのコールバック関数の中でトランザクションの処理を記述します。コールバックの引数でもらえるSessionContext型のコンテキストを使用することでトランザクションが実現できます。

```go
		// ここからトランザクション開始
		err = sc.StartTransaction()
		if err != nil {
			return err
		}

		// DBとCollection名はここで指定
		db := client.Database("DB名")
		collection := db.Collection("コレクション名")

		// 適当にInsert文などを。context型を求める引数にSessionContextを使うのが重要
		_, err = collection.InsertOne(sc, bson.M{"キー": 値})
		if err != nil {
			// EndSession内でAbortTransactionが呼ばれるので、ここで無理に呼ぶ必要はない。
			sc.AbortTransaction(sc)
			return err
		}

		// トランザクション中の処理を確定
		return sc.CommitTransaction(sc)
```

## まとめたサンプル

```go
package main

import (
	"context"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

func main() {
	// MongoDBへの接続
	ctx := context.Background()

	client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://172.17.0.2:27017"))
	if err != nil {
		panic(err)
	}

	// セッション開始
	err = client.UseSession(ctx, func(sc mongo.SessionContext) error {
		// ここからトランザクション開始
		err = sc.StartTransaction()
		if err != nil {
			return err
		}

		// DBとCollection名はここで指定
		db := client.Database("my_test_db")
		collection := db.Collection("my_test_col")

		// 適当にInsert文などをば。引数にSessionContextを入れているのが重要
		_, err = collection.InsertOne(sc, bson.M{"number": "abc"})
		if err != nil {
			// EndSession内でAbortTransactionが呼ばれるので、ここで無理に呼ぶ必要はない。
			sc.AbortTransaction(sc)
			return err
		}

		// トランザクション中の処理を確定
		return sc.CommitTransaction(sc)
	})
	if err != nil {
		panic(err)
	}
}
```

# おしまい

セッションのところがコールバックでちょっとモヤっとしたけど、JavaScriptみたいに非同期ではないのでまだ使いやすいです。あ、非同期にしたいときはUseSessionのあたりをgoルーチンにするだけです。