3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

私はとある魔法少女のソシャゲをやっているのですが、魔法少女の数も増えてきて検索が大変になってきました。
ゲーム内の検索機能ではすこし物足りない部分もあるため、勉強も兼ねて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を作成します
サンプルは1人ですが、実際には複数人登録するため配列にしています。

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

Firestoreの準備

いろんな人がFirestoreに関する記事をあげているので、ここでは割愛します。
最終的には空のFirestoreが作成できていればOKです。
スクリーンショット 2020-06-10 1.22.38.png

Firestoreの構造について

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

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

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

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

golangでFirestoreにデータを投入する。

jsonデータをgolangのtypeに変換する

まずは投入したいデータをgolang側で読取できるよう準備します。

以下サイトでjsonファイルを元に他言語に変換が可能です。
Instantly parse JSON in any language | quicktype

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

スクリーンショット 2020-06-10 1.12.59.png

Firestoreの接続キーを取得する

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

Firebase Projectの画面でプロジェクトの概要の隣にある歯車マークを選択し、「プロジェクトを設定」を選択します。
スクリーンショット 2020-06-10 1.32.52.png

サービスアカウントを選択します。
スクリーンショット 2020-06-10 1.34.27.png

様々な言語での接続方法があるので、Goを選択します。
スクリーンショット 2020-06-10 1.35.37.png

新しい秘密鍵の生成をクリックすると、秘密鍵を含んだjsonファイルをダウンロードします。
このファイルは秘密鍵なので大切に、他人に渡らないよう厳重に保管してください。
スクリーンショット 2020-06-10 1.38.07.png

golangでFirestoreに接続する

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

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

go get -u firebase.google.com/go

Firebaseへの認証

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

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関数内に追加します。

	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内に貼り付けます。

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名で投入します。

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のデータを取得、確認できます。

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に接続するための秘密鍵は外部から受け取るようにしています。

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に設定します。)

go get
go ran main.go firebasekeyfile.json

実行すると以下のようになります。
データ投入後にコレクション内にあるドキュメントが表示されます
スクリーンショット 2020-06-11 1.22.22.png

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

スクリーンショット 2020-06-11 1.30.34.png

さいごに

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

おまけ

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

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

参考

これを作るまでにいろいろと参考にさせていただきました。ありがとうございます。
GolangでFirebaseのRealtime Databaseにデータを書き込む - Qiita
遂にFirebase Admin SDK Goが登場! - Qiita

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?