golangを使ってFirestoreにjsonファーストでデータを投入した話


# はじめに
私は[とある魔法少女のソシャゲ](https://magireco.com/)をやっているのですが、魔法少女の数も増えてきて検索が大変になってきました。
ゲーム内の検索機能ではすこし物足りない部分もあるため、勉強も兼ねてFirebase＋Nuxt.jsで魔法少女の一覧を色々な検索できるよう実装をはじめました。

Firestoreにマスタデータを用意するとき、管理画面からデータを入力を考えましたが、さすがに大変ですしメンテナンスも難しそうです。
制作の過程でモック用のjsonを作成していたので、jsonをそのままFirestoreに保存しようと考えました。

普段趣味でgolangを触っているということもあったので、golangを使用してjsonを読み込みし、firestoreにInsert,Updateするアプリケーションをjsonファーストで作成してみました。
（実装をどうするかって？ならGo言語で書け！！（Mr.パーカJr風に））

なお、本記事での投入とはFirestoreへデータをInsert、Updateすることとしています。

# 用意するもの
以下の状態までできている前提です

- Firebaseでプロジェクトを作成し、Firestore Databaseまでは作成している
- golangが導入済みである（筆者環境は12.6で、動作を確認しています)

# jsonの準備
## 元となるデータ
魔法少女たちは以下の情報を持ちます。（説明用に部分的に省略しています）

- Key（データを探索するときに使用、Firestoreではドキュメント名にも割当します。）
- 名前
- 属性（火、水、木、光、闇、無）
- タイプ（アタック、ディフェンス、バランス、ヒールなど）
- HP、攻撃力、防御力といったステータス

## jsonデータ
魔法少女のデータを元にjsonを作成します
サンプルは１人ですが、実際には複数人登録するため配列にしています。

```json
[
  {
    "key" : "kaname madoka",
    "name" : "鹿目 まどか",
    "attribute" : "光",
    "type" : "ヒール",
    "status" : {
      "hp" : 24336,
      "attack": 6832,
      "defense" : 9276
    }
  }
] 
```

# Firestoreの準備
いろんな人がFirestoreに関する記事をあげているので、ここでは割愛します。
最終的には空のFirestoreが作成できていればOKです。
![スクリーンショット 2020-06-10 1.22.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/b16e6e2c-bf6a-b002-01dd-3a4d95065bc6.png)

## Firestoreの構造について
このあとFirestoreにデータを投入しますが、どの位置にドキュメントを配置するか、ドキュメント名をどうするかはFirestoreを扱うときの１つのポイントとなります。
今回は以下の構造で作成していきます。

- jsonの中身は'private/v1/magicalGirls'に配置する
- ドキュメント名はKey名と同一とする

配置先は本運用になったときにセキュリティルールを設定しやすいようにするためと、バージョンアップをした時に対応できるようにしています。
（自分もまだ手探りなので、指摘あればコメントください）

ドキュメント名をKey名と同一としているのは、マスタデータとして探索、更新しやすいようにするためです。
トランザクションであればドキュメント名を自動で割振でも良いのですが、マスタデータは変更が発生しやすく、変更のたびにドキュメント名を探索するのは効率が悪いです。
また、golang側もドキュメント名を元にInsert、Updateを行うので、ドキュメント名をKey名と同一にしています。

# golangでFirestoreにデータを投入する。
## jsonデータをgolangのtypeに変換する
まずは投入したいデータをgolang側で読取できるよう準備します。

以下サイトでjsonファイルを元に他言語に変換が可能です。
[Instantly parse JSON in any language \| quicktype](https://app.quicktype.io/)

この画面は左、中央、右の３ペインに分かれています。
左側のペインで、Nameにgolangのタイプ名（配列なので、複数名で入力）、左側ペイン中央に作成したjsonを入力します。
右側ペインで変換先の言語を選択できるので、Goを選択します。
選択後に中央ペインにgolangのソースが生成されます。

![スクリーンショット 2020-06-10 1.12.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/114a090a-ea9e-3df9-0f20-bc2f444079ef.png)


## Firestoreの接続キーを取得する
golangでFirestoreにデータを投入するためにはサービスアカウントに紐づく秘密鍵を含めたjsonデータが必要です。
取得は難しくありませんが、秘密鍵なので絶対に公開してはいけません。gitとかで管理してもNGです。

Firebase Projectの画面でプロジェクトの概要の隣にある歯車マークを選択し、「プロジェクトを設定」を選択します。
![スクリーンショット 2020-06-10 1.32.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/7282e9d5-dcd2-8460-a6d1-3fb55a421110.png)

サービスアカウントを選択します。
![スクリーンショット 2020-06-10 1.34.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/051e3298-1263-01a2-0739-17c16a351af5.png)

様々な言語での接続方法があるので、Goを選択します。
![スクリーンショット 2020-06-10 1.35.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/ca15f40d-6540-8997-da00-3d5af4a1e57f.png)

新しい秘密鍵の生成をクリックすると、秘密鍵を含んだjsonファイルをダウンロードします。
このファイルは秘密鍵なので大切に、他人に渡らないよう厳重に保管してください。
![スクリーンショット 2020-06-10 1.38.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/af5f6304-9dd7-9892-52e1-9669a3c6d507.png)

## golangでFirestoreに接続する
秘密鍵の生成時に気が付いたと思いますが、golangのソースサンプルが記載されています。
このサンプルソースはFirestoreの認証部分になります。
サンプルを元に認証部分を作成します。

まずはFirebaseのライブラリをインストールします。

```shell
go get -u firebase.google.com/go
```

### Firebaseへの認証
Firebaseの認証をまずは行います。
main.goファイルを作成し、サンプルソースをmain関数に記載してきます。
エラー時はメッセージを表示して終了します。

```golang
package main

import (
	"context"
	"fmt"

	"google.golang.org/api/option"

	firebase "firebase.google.com/go"
)

func main() {
	// Use a service account
	ctx := context.Background()
	sa := option.WithCredentialsFile("path/to/serviceAccountKey.json")
	app, err := firebase.NewApp(ctx, nil, sa)
	if err != nil {
		fmt.Printf("error initializing app: %v", err)
		return 
	}
}
```

### Firestoreに接続する
Firestoreに接続するClientをmain関数内に追加します。

```golang
	client, err := app.Firestore(ctx)
	if err != nil {
		fmt.Printf("error create Firestore client: %v", err)
		return
	}
	defer client.Close()
```

### Firestoreに投入するデータの準備
jsonデータをgolangのtypeに変換するで生成したソースをmain.go内に貼り付けます。

```golang
package main

import (
	"context"
	"encoding/json"
	"fmt"

	"google.golang.org/api/option"

	firebase "firebase.google.com/go"
)

type MagicalGirls []MagicalGirl

func UnmarshalMagicalGirls(data []byte) (MagicalGirls, error) {
	var r MagicalGirls
	err := json.Unmarshal(data, &r)
	return r, err
}

func (r *MagicalGirls) Marshal() ([]byte, error) {
	return json.Marshal(r)
}

type MagicalGirl struct {
	Key       string `json:"key"`
	Name      string `json:"name"`
	Attribute string `json:"attribute"`
	Type      string `json:"type"`
	Status    Status `json:"status"`
}

type Status struct {
	HP      int64 `json:"hp"`
	Attack  int64 `json:"attack"`
	Defense int64 `json:"defense"`
}

func main() {
.....

```

### Firestoreにデータを投入する
jsonファイルをロードし、作成していたFirestoreのClientを使用してデータを投入する処理を実装します。
配置先はFirestoreの準備でも記載した通り、'private/v1/magicalGirls'にドキュメント名をKey名で投入します。

```golang
import (
...
	"io/ioutil"
...
)

...

	// load json file
	bytes, err := ioutil.ReadFile("path/to/magicalGirls.json")
	if err != nil {
		fmt.Printf("error load magicalGirls.json : %v", err)
	}
	magicalGirls, err := UnmarshalMagicalGirls(bytes)
	if err != nil {
		fmt.Printf("error unmarshal magicalGirls.json : %v", err)
	}

	for i := range magicalGirls {
		_, err = client.Collection("private/v1/magicalGirls").Doc(magicalGirls[i].Key).Set(ctx, magicalGirls[i])
		if err != nil {
			fmt.Printf("error adding magicalGirl: %v", err)
			return
		}
	}

```

### 投入したデータの確認
データを投入した後に、コレクション内にあるすべてのドキュメントを取得して、登録できているかを確認します。
Firebaseの画面から確認もできますが、せっかくなのでgolang側から取得します。
イテレータを使用することでFirestoreのデータを取得、確認できます。

```golang
import(
...
	"google.golang.org/api/iterator"
...
)

...
	iter := client.Collection("private/v1/magicalGirls").Documents(ctx)
	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			fmt.Printf("error get magicalGirl documents : %v", err)
		}
		fmt.Println(doc.Data())
	}

```

### 作成したmain.go
作成したmain.goは以下になります。
Firebaseに接続するための秘密鍵は外部から受け取るようにしています。

```golang
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"os"

	"google.golang.org/api/iterator"
	"google.golang.org/api/option"

	firebase "firebase.google.com/go"
)

type MagicalGirls []MagicalGirl

func UnmarshalMagicalGirls(data []byte) (MagicalGirls, error) {
	var r MagicalGirls
	err := json.Unmarshal(data, &r)
	return r, err
}

func (r *MagicalGirls) Marshal() ([]byte, error) {
	return json.Marshal(r)
}

type MagicalGirl struct {
	Key       string `json:"key"`
	Name      string `json:"name"`
	Attribute string `json:"attribute"`
	Type      string `json:"type"`
	Status    Status `json:"status"`
}

type Status struct {
	HP      int64 `json:"hp"`
	Attack  int64 `json:"attack"`
	Defense int64 `json:"defense"`
}

func main() {
	flag.Parse()
	if flag.NArg() != 1 {
		fmt.Println("firebase key file not set for Args")
		return
	}
	args := flag.Args()
	file := args[0]
	_, err := os.Stat(file)
	if err != nil {
		fmt.Println("error : firebase key file not found")
		return
	}

	// Use a service account
	ctx := context.Background()
	sa := option.WithCredentialsFile(file)
	app, err := firebase.NewApp(ctx, nil, sa)
	if err != nil {
		fmt.Printf("error initializing app: %v", err)
		return
	}

	client, err := app.Firestore(ctx)
	if err != nil {
		fmt.Printf("error create Firestore client: %v", err)
		return
	}
	defer client.Close()

	// load json file
	bytes, err := ioutil.ReadFile("path/to/magicalGirls.json")
	if err != nil {
		fmt.Printf("error load magicalGirls.json : %v", err)
	}
	magicalGirls, err := UnmarshalMagicalGirls(bytes)
	if err != nil {
		fmt.Printf("error unmarshal magicalGirls.json : %v", err)
	}

	for i := range magicalGirls {
		_, err = client.Collection("private/v1/magicalGirls").Doc(magicalGirls[i].Key).Set(ctx, magicalGirls[i])
		if err != nil {
			fmt.Printf("error adding magicalGirl: %v", err)
			return
		}
	}

	iter := client.Collection("private/v1/magicalGirls").Documents(ctx)
	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			fmt.Printf("error get magicalGirl documents : %v", err)
		}
		fmt.Println(doc.Data())
	}
}
```

# 実際にデータを投入してみる
実行するまえに、一応go getをやっておきます。
（GO111MODULEはonに設定します。）

```shell
go get
go ran main.go firebasekeyfile.json
```

実行すると以下のようになります。
データ投入後にコレクション内にあるドキュメントが表示されます
![スクリーンショット 2020-06-11 1.22.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/f0a4450d-29e3-d35f-7720-472c5dd610cd.png)

Firebaseの画面からFirestoreをみるとこんな感じです。
privateとかv1といったコレクション、ドキュメントも自動で生成してくれます。

![スクリーンショット 2020-06-11 1.30.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66035/61292221-a0d6-bc44-552c-417e11d4d534.png)

# さいごに
jsonファーストでデータを投入する機会はあまりないかなとおもいます。
ですが、マスタデータの投入、メンテナンスを画面なしでできる、一括でできるってのは思っていた以上に楽でした。
トランザクションデータを扱わなないサイト（一覧や計算ツールなど）であれば使いどころはあるのかなと思います。

# おまけ
jsonからデータ投入できたので、いろいろと試していてへーとおもったこと。
自分用の備忘録として残しておきます

- ドキュメント名にはスペースを含めることが可能
- ドキュメント名に日本語を使ってもOK
- ドキュメント名のみで、ドキュメントの中身がないものは生成できない

# 参考
これを作るまでにいろいろと参考にさせていただきました。ありがとうございます。
[GolangでFirebaseのRealtime Databaseにデータを書き込む \- Qiita](https://qiita.com/croquette0212/items/f40dbad5dfbad16fc76d)
[遂にFirebase Admin SDK Goが登場！ \- Qiita](https://qiita.com/koki_cheese/items/2d111b2b074bfa697776)