【golang】Mongodbでよく使った操作のまとめ

# 1. はじめに
今回のプロジェクトでは、APIサーバーを構築する言語としてGolangを、データベースにはMongodbを使いました。
基本的な操作から複雑な処理まで幾つか実践したので、今回は忘備録も兼ねてそれをまとめようと思います。

# 2. 一つずつ操作する
例としてIDを持つ適当な構造体、Objectを使います。なお、mongodbのカラム名とomitemptyタグを付けています。
例えば以下のようになります。

```golang
type Object struct {
	ID          *primitive.ObjectID `bson:"_id,omitempty"`
    Date        *time.Time          `bson:"date,omitempty"`
	FiscalYear  *int                `bson:"fiscal_year,omitempty"`
	UpdatedAt   *time.Time          `bson:"updated_at,omitempty"`
	CreatedAt   *time.Time          `bson:"created_at,omitempty"`
}
```

もしオブジェクトのメンバーの値が初期値、この場合nilであった場合は、その値を無視して更新・新規作成が行われます。
もしomitemptyがない場合、変更処理の際Objectをインスタンス化する際に一つ一つメンバーの値が初期値ではないかを確認する作業が必要になりコードが冗長になるので、omitemptyタグを付けることをお勧めします。

## 読み取り

一つの値を読み取る際にはFindOneメソッドを実行し、続けてDecodeメソッドを実行します。このとき引数にはポインターを渡すことに気を付けてください。メソッドの中で構造体のインスタンスに値を書き込むからです。

```golang
func (d *db) Read(ctx context.Context, fiscalYear int) (*Object, error) {
    var obj Object
    filter := bson.D{{"fiscal_year", fiscalYear}}
	obj := new(Object)
	err := d.collection.FindOne(ctx, filter).Decode(&obj)
	if err != nil {
		return nil, err
	}

	return obj, nil
}
```

## 書き込み
```golang
func (d *db) Create(ctx context.Context, obj *Object) error {
    id := primitive.NewObjectID()
    obj.ID = id
	_, err := b.collection.InsertOne(ctx, obj)

	return err
}
```

## 修正

一つのフィールドを変更したい場合、UpdateOneとUpdateByIDの二つがあります。
UpdateByIDは変更したい値の他にidを、UpdateOneはフィルターを必要とします。UpdateOneのフィルターをidに設定することもでき、フィルターをidに特化したメソッドがUpdateByIDと言えるでしょう。このため、基本的にUpdateOneはid以外の主キー、ないし少なくともユニークキーで検索をかけたいときに使うことになると思います。

```golang
func (d *db) UpdateByID(ctx context.Context, obj *Object) error {
    objID, err := primitive.ObjectIDFromHex(input.ID)
	if err != nil {
		return nil, err
	}

    update := bson.M{
		"$set": obj,
	}
    
	result, err := d.collection.UpdateByID(ctx, id, update)
	if err != nil {
		return err
	}
	if result.ModifiedCount == 0 {
		return fmt.Errorf("no change happened")
	}

	return nil
}
```
updateで、なんら更新されたものがない時はエラーである可能性があります。このため、resultからModifiedCountの値を取得し、何も変わっていない（0である場合）はエラーを吐くようにしています。


# 3. 複数操作する
## 読み取り

まとめて読み取る場合は、Findに対してfilterを渡し、cursorのAllメソッドを実行してデータを読み込みます。
mongodbにおいて複数の値を読み取る場合は、カーソルを扱わないといけないことに注意してください。

```golang
func (d *db) Reads(ctx context.Context, fiscalYear) ([]Object, error) {
    startDate := time.Date(fiscalYear, 4, 1, 0, 0, 0, 0, time.Local)
	endDate := time.Date(fiscalYear+1, 4, 1, 0, 0, 0, 0, time.Local).Add(-time.Second)
	filter := bson.D{{"date", bson.D{{"$gte", startDate}, {"$lt", endDate}}}}

    cursor, err := d.collection.Find(ctx, filter)
	if err != nil {
		return nil, err
	}
	var objects []Object
	if err = cursor.All(ctx, &objects); err != nil {
		return nil, err
	}

	return objects, nil
}
```

一々一つずつ読み取る機会は少ないと思いますが、for文で回して逐次的にcursorから値を読み取っていくこともできます。

```golang
cursor, err := d.collection.Find(ctx, filter)
if err != nil { 
    return nil, err
}

defer cursor.Close(ctx)

var objects []Object

for cursor.Next(ctx) {
  var object Object
  err := cur.Decode(&object)
  if err != nil { 
    return nil, err 
  }

  objects = append(objects, object)
}
if err := cur.Err(); err != nil {
  return nil, err
}
```

もし構造体のインスタンスに代入せずにカーソルの値をそのまま扱う時は、`cursor.Current`を変数に代入できます。

## 挿入・更新・削除

BulkWriteはまとめて値を編集したい場合に使えます。
今回はWriteModelにUpdateOneModelをわたすことで一括更新処理を実装しました。SetUpsertの引数をtrueにすれば、該当する値がなかった場合挿入処理も行うことができます。
多数のデータの一括更新にはupdateManyメソッドもありましたが、これはある条件に当てはまるドキュメント全てをある値で一律に変更することになります。BulkWriteのUpdateOneModelはその名の通り一つずつ定められた値で別々に変更できるため、非常に便利です。

```golang
func (d *db) UpdateByIDs(ctx context.Context, objs []Object) error {
    updateObjectModels := make([]mongo.WriteModel, len(objs))

    for i, obj := range objs {
        objSetter := bson.M{"$set": obj}

        updateObjectModel := mongo.NewUpdateOneModel().
            SetFilter(bson.M{"_id": obj.ID}).
            SetUpdate(objSetter).
            SetUpsert(false)

        updateObjectModels[i] = updateObject
    
    }

    result, err := d.collection.BulkWrite(ctx, updateObjectModels)

    if err != nil {
        return err
    }

    if result.ModifiedCount == 0 {
		return fmt.Errorf("no change happened")
	}

	return nil

}
```

WriteModelに渡すモデルの違いによってどのようにデータを編集するかが変わります。Updateの他には、挿入処理の場合に`mongo.NewInsertOneModel()`、置換処理の際には`mongo.NewReplaceOneModel()`、削除処理の場合には`mongo.NewDeleteOneModel()`があります。細かいところについては公式ドキュメントをご参照ください。

https://www.mongodb.com/docs/drivers/go/current/fundamentals/crud/write-operations/bulk/

# 4. 特別な操作
日付のデータから年のみを取り出す処理を行う際、Aggregateメソッドを用いました。
Aggregateとは何かを説明するのは難しいので、公式ドキュメントから説明を引用します。

> Aggregation operations operate similarly to a car factory. Car factories have an assembly line. The assembly lines have assembly stations with specialized tools to perform a specific task. To build a car, you send raw parts to the factory. Then, the assembly line transforms and assembles the parts into a car.
> The assembly line resembles the aggregation pipeline, the assembly stations in the assembly line resemble the aggregation stages, the specialized tools represent the expression operators, and the finished product resembles the aggregated result.

即ち、pipelineが部品の乗る生産ラインで、特定の命令を実行するaggregationステージに順次引き渡して最終的な結果が出力されるようなイメージです。
以下のコードでは、dateメンバーから年だけを出力し、ソートするように命令しています。

```golang

func (d *db) GetDistinctYears(ctx context.Context) ([]int, error) {
	pipeline := mongo.Pipeline{
		{{"$group", bson.D{{"_id", bson.D{{"$year", "$date"}}}}}},
		{{"$sort", bson.D{{"_id", 1}}}},
	}

	cursor, err := d.collection.Aggregate(ctx, pipeline)
	if err != nil {
		return nil, err
	}

	var results []bson.M
	if err := cursor.All(ctx, &results); err != nil {
		return nil, err
	}

	yearSet := map[int32]struct{}{}
	years := make([]int32, 0)
	for _, result := range results {
		year := result["_id"]
		if y, ok := year.(int32); ok {
			if _, exist := yearSet[y]; !exist {
				yearSet[y] = struct{}{}
				years = append(years, y)
			}
		} else {
			return nil, fmt.Errorf("the type of year is not int")
		}
	}

	return years, nil
}

```
`{{"$group", bson.D{{"_id", bson.D{{"$year", "$date"}}}}}}`がステージにあたり、`$group`などがオペレーターにあたります。
実際のコードを通してみると、生産ライン（パイプライン）にテーブルのデータを乗せ、特定の命令（オペレーター）を実行する組み立て場（ステージ）で順次処理されることで最終的な結果が出力される、というイメージがわきやすくなったかなと思います。

Aggregateメソッドの実行やそのオペレーターなどは以下のサイトを参考にしました

https://stackoverflow.com/questions/33109897/get-distinct-iso-dates-by-days-months-year

https://www.mongodb.com/docs/manual/reference/operator/aggregation/year/?tck=mongodb_ai_chatbot

https://qiita.com/one-kelvin/items/fba03b293944265a7926