Series: go-firestore

Go で Cloud Firestore を使ってみる

NoSQL を使ったことがなくて未だにわからないので使ってみる。

開発環境

  • go version go1.16.5 linux/amd64

本文

NoSQL を使ったことがなくてよくわかっていないので使ってみる。

何か作るものを想定しないと試すのもできないため、短文投稿しかできないブログ(要するに自分一人しかいない Twitter)を想定する。

まずDB設計なのだが、一旦下記の記事を参考にしようと思って読んだ。

しかし使ったことがないと理解が難しいので、とりあえず下記のようなデータ構造を考えて Firestore のコンソールから投入した。

/posts/:post_id/
{
  "text": "短文投稿の内容",
  "created": "2021-07-18 16:18:00",
  "updated": "2021-07-18 16:18:00"
}

これを Go から読み取るので、公式のガイドに沿ってソースコードを書く。

「Cloud Firestore を初期化する」のところは「各自のサーバーで初期化する」の方法でやる。
詰まるかもしれない点は

sa := option.WithCredentialsFile("path/to/serviceAccount.json")

ここのパスがカレントディレクトリからのパスなのか、ルートディレクトリからのパスなのかわからない。
ルートディレクトリで実行してればルートディレクトリからのパスでいいと思う。
少なくともこのコードが書いてあるファイルからの相対パスではないことは確か。

次にデータの登録は飛ばして読み取りに進むが

iter := client.Collection("users").Documents(ctx)
for {
        doc, err := iter.Next()
        if err == iterator.Done {
                break
        }
        if err != nil {
                log.Fatalf("Failed to iterate: %v", err)
        }
        fmt.Println(doc.Data())
}

この公式サンプルのためには import に
“google.golang.org/api/iterator”
を追加しないといけないので注意。

doc.Data() の型は map[string]interface{} になっていた。

ここまで踏まえて「Cloud Firestore から読み取ったデータを html に出力する」というサンプルが下記のようになる。
(見通しを良くすることとかエラー処理とかは考えてない。)

package main

import (
	"context"
	"fmt"
	"html/template"
	"log"
	"net/http"

	firebase "firebase.google.com/go"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

var ctx context.Context
var app *firebase.App

type Page struct {
	Title   string
	Content []map[string]interface{}
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
	client, err := app.Firestore(ctx)
	if err != nil {
		log.Fatalln(err)
	}
	defer client.Close()
	iter := client.Collection("posts").Documents(ctx)
	var posts []map[string]interface{}
	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatalf("Failed to iterate: %v", err)
		}
		fmt.Println(doc.Data())
		posts = append(posts, doc.Data())
	}

	p := Page{
		Title:   "Test",
		Content: posts,
	}
	t, err := template.ParseFiles("./web/layout.html")
	if err != nil {
		panic(err)
	}
	err = t.Execute(w, p)
	if err != nil {
		panic(err)
	}
}

func main() {
	ctx = context.Background()
	sa := option.WithCredentialsFile("path/to/serviceAccount.json")
	app, err = firebase.NewApp(ctx, nil, sa)
	if err != nil {
		log.Fatalln(err)
	}
	http.HandleFunc("/", rootHandler)
	http.ListenAndServe(":3000", nil)
}

この結果、下記のようなテンプレートファイル(web/layout.html)を使うと

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.Title}}</title>
  </head>
  <body>
    <ul>
    {{ range .Content }}
      <li>
      text: {{ .text }}<br>
      created: {{ .created }}<br>
      updated: {{ .updated }}
      </li>
    {{ end }}
  </ul>
  </body>
</html>

出力が下記のようになる。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test</title>
  </head>
  <body>
    <ul>
    
      <li>
      text: テスト<br>
      created: 2021-07-18 07:18:00 &#43;0000 UTC<br>
      updated: 2021-07-18 07:18:00 &#43;0000 UTC
      </li>
    
      <li>
      text: テスト\nテスト<br>
      created: 2021-07-18 07:28:00 &#43;0000 UTC<br>
      updated: 2021-07-18 07:28:00 &#43;0000 UTC
      </li>
    
  </ul>
  </body>
</html>

時刻についてはコンソール上だと JST で入力・表示できるのだが、特に何も気にせず読み取ると UTC になった。
ここについては後で調べることにする。

以上