Go言語を使って、Firebase Realtime Databaseを読んだり書いたりします。
was updated last. 6 months ago
Go言語でFirebase Realtime Databaseを操作する

今回はGo言語のFirebaseライブラリであるfirebase-admin-goを利用して、
Firebase Realtime Databaseを操作してみようと思います。

現状、日本語で書かれた記事がほとんどなく、英語記事のコード断片やGoDoc、公式リポジトリのテストコードなどから使い方を調査している状況です。
(2018年8月4日現在)

Go言語でFirebase Realtime Databaseを操作してみたいという方の手助けになれたらいいなと思います!

事前準備

1. プロジェクトを設定する
  • https://console.firebase.google.com/ にアクセス
  • プロジェクトを作成or選択してプロジェクト設定へ
  • サービスアカウント→新しい秘密鍵の生成(jsonファイルがダウンロードされる)
  • 秘密鍵ファイルはソースコードから参照できる場所に置いておく(後述のサンプルではmain.goと同じディレクトリに置いてあります)
2. ライブラリを導入する
$ go get firebase.google.com/go
$ go get google.golang.org/api/option #option.WithCredentialsFile()を使わない場合は不要

ライブラリとデータベースの初期化

import (
    firebase "firebase.google.com/go"
    "golang.org/x/net/context"
    "google.golang.org/api/option"
    "log"
)

//...

// パスも指定可能
opt := option.WithCredentialsFile("project-name-firebase-adminsdk-xxxxx-xxxxxxxxxx.json")
config := &firebase.Config{
    DatabaseURL: "https://project-database.firebaseio.com/",
}

ctx := context.Background()
app, err := firebase.NewApp(ctx, config, opt)
if err != nil {
    log.Fatal(err)
}
client, err := app.Database(ctx)
if err != nil {
    log.Fatal(err)
}
log.Println(client)
// &{0xc42019c560 https://project-database.firebaseio.com }

ノードへの参照を操作する

  • 基本的にデータを操作する始めの一歩となります
  • 操作したい対象のノードにパス文字列やChildParentなどを使って参照します
  • ノード参照(*firebase.Ref)を得ることのできる関数は、NewRefChildParentPushです
ref := client.NewRef("messages/room_key1/message_id/text")
log.Println(ref)
// &{text /messages/room_key1/message_id/text [messages room_key1 message_id text] 0xc420192b10}

ref = client.NewRef("messages").Child("room_key1").Child("message_id").Child("text")
log.Println(ref)
// &{text /messages/room_key1/message_id/text [messages room_key1 message_id text] 0xc420192b10}

ref = client.NewRef("messages/room_key1/message_id/text").Parent().Child("text")
log.Println(ref)
// &{text /messages/room_key1/message_id/text [messages room_key1 message_id text] 0xc420192b10}

ref, err := ref.Push(context.Background(), "(」・ω ・)」うー!")
if err != nil {
    log.Fatal(err)
}
log.Println(ref)
// &{-LIIkSxbJSQFvNkBEXr1 /messages/room_key1/message_id/text/-LIIkSxbJSQFvNkBEXr1 [messages room_key1 message_id text -LIIkSxbJSQFvNkBEXr1] 0xc420192b10}

データを追加する(Push)

  • 指定したノードの参照に構造体を追加します
  • 指定したノードの参照に一意のキーで追加される為、既存データを壊しません
type Message struct {
    Text string `json:"text"`
}

ref := client.NewRef("messages/room_key1")
message1 := Message{
    Text: "text1",
}
childRef1, err := ref.Push(context.Background(), message1)
if err != nil {
    log.Fatal(err)
}
log.Println(childRef1)
// &{-LIIgXQ6nJRdDf_fGlZk /messages/room_key1/-LIIgXQ6nJRdDf_fGlZk [messages room_key1 -LIIgXQ6nJRdDf_fGlZk] 0xc420192b10}

message2 := Message{
    Text: "text2",
}
childRef2, err := ref.Push(context.Background(), message2)
if err != nil {
    log.Fatal(err)
}
log.Println(childRef2)
// &{-LIIgXU4lnSKpnHYJRqs /messages/room_key1/-LIIgXU4lnSKpnHYJRqs [messages room_key1 -LIIgXU4lnSKpnHYJRqs] 0xc420192b10}

データをセットする(Set)

  • 指定したノードにデータをセットします
  • Pushと違い一意なキーは割り当てられない為、既存のデータを上書きします
type Message struct {
    MessageId int `json:"message_id"`
    Text string `json:"text"`
}

ref := client.NewRef("messages/room_key1")
message1 := Message{
    MessageId: 1,
    Text: "text1",
}
err := ref.Set(context.Background(), message1)
if err != nil {
    log.Fatal(err)
}

message2 := Message{
    MessageId: 2,
    Text: "text2",
}
err = ref.Set(context.Background(), message2)
if err != nil {
    log.Fatal(err)
}

データを更新する(Update)

  • 指定したノードのデータを更新します
  • 受け付けるデータ型はmap[string]interface{}です

以下のようなデータ構造を用意しておきます。

ref := client.NewRef("rooms/user1")
err := ref.Update(context.Background(), map[string]interface{}{
    "last_message_text": "updated!",
})
if err != nil {
    log.Fatal(err)
}

データを取得する(Get)

以下のようなデータ構造を用意しておきます。

まずはシンプルに指定したノードの子にある全要素を取得してみます。
この書き方だと並び順は保証されません。

type Message struct {
    UserId int    `json:"user_id"`
    Text   string `json:"text"`
}

var messages map[string]Message
ref := client.NewRef("messages/room_key")
err := ref.Get(context.Background(), &messages)
if err != nil {
    log.Fatal(err)
}
for key, message := range messages {
    log.Printf("%s: %+v\n", key, message)
}
// -LJ2VK2ZWNSUr5fNrI1g: {UserId:1 Text:text1}
// -LJ2VK8snPUJbC-9RIhU: {UserId:3 Text:text3}
// -LJ2VK5pggdELVG4Pkol: {UserId:2 Text:text2}

次に並び順を指定して、全要素を取得してみます。
並び順を指定するにはOrderByChild()GetOrdered()を利用します。
GetOrdered()関数の戻り値は[]QueryNodeになります。
QueryNodeKey()関数とUnmarshal()関数を持ったインターフェースです。
https://godoc.org/firebase.google.com/go/db#QueryNode

type Message struct {
    UserId int    `json:"user_id"`
    Text   string `json:"text"`
}

ref := client.NewRef("messages/room_key")
nodes, err := ref.OrderByChild("user_id").GetOrdered(context.Background())
if err != nil {
    log.Fatal(err)
}

for _, node := range nodes {
    var message Message
    err := node.Unmarshal(&message)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%s: %+v\n", node.Key(), message)
}
// -LJ2VK2ZWNSUr5fNrI1g: {UserId:1 Text:text1}
// -LJ2VK5pggdELVG4Pkol: {UserId:2 Text:text2}
// -LJ2VK8snPUJbC-9RIhU: {UserId:3 Text:text3}

さて、次に条件を指定して特定ノードを取得してみましょう。
条件を指定するには指定する子キーに対してインデックスの作成が必要になります。
以下を参考にルールに.indexOnを追加してください。
https://firebase.google.com/docs/database/security/indexing-data?hl=ja

http error status: 400; reason: Index not defined, \
add ".indexOn": "user_id", for path "/messages/room_key", to the rules
type Message struct {
    UserId int    `json:"user_id"`
    Text   string `json:"text"`
}

var messages map[string]Message
ref := client.NewRef("messages/room_key")
err := ref.OrderByChild("user_id").EqualTo(2).Get(context.Background(), &messages)
if err != nil {
    log.Fatal(err)
}

 for key, message := range messages {
    log.Printf("%s: %+v\n", key, message)
}
// -LJ2VK5pggdELVG4Pkol: {UserId:2 Text:text2}

トランザクションを使う(Transaction)

  • Firebaseのトランザクションはノードの参照に対して行われます
  • トランザクションの処理中に対象のノード参照に対して書き込み(例えばPushとかSetとか)があった場合、
    Updatefn()が終了した時点で、最新のTransactionNodeを取得してトランザクションが再試行されます
  • トランザクションの再試行は最大25回行われ、その後Transaction()のエラーとして送出されます
  • Updatefn()の第一戻値によって、対象のノード直下が上書きされます
    (return nil, nilなどするとトランザクションしているノード直下が全消えします、それでハマりました)
  • つまり、最新である事が保証されたデータに対して書き込みや取得を行うことができます
  • ただし、様々なクライアントが書き込みを行うようなFirebase Realtime Databaseではトランザクションの再試行の起きる可能性が非常に高くなります
  • 再試行の多発によって処理が意図した時間内で完了せず、パフォーマンスに影響を与える事があります。上位ノードでのトランザクションはできる限り避けるべきです

以下のようなデータ構造を用意しておきます。

type Room struct {
    PartnerId       int    `json:"partner_id"`
    LastMessageText string `json:"last_message_text"`
}

ref := client.NewRef("rooms")

transaction := func(node firebasedb.TransactionNode) (interface{}, error) {
    // トランザクション実行時点での最新の要素データが入ります
    var rooms map[string]Room
    node.Unmarshal(&rooms)

    // 要素に変更を加える場合は、Set()やPush()ではなく、
    // TransactionNodeをUnmarshalした構造体に対して行います。
    // ※Set()やPush()をすると、トランザクションノードに変更があったと判定されて、
    // Updatefn()の完了後、トランザクションが再試行します。
    newRoom := Room{
        PartnerId:       14,
        LastMessageText: "これはトランザクションで追加されたデータ",
    }
    rooms["user4"] = newRoom
    // ここでreturnした後、トランザクションノードに変更があった場合、トランザクションが再試行されます。
    // 変更がなければ返り値として戻したデータでノードが上書きされます。
    return rooms, nil
}

// トランザクションを実行します
err := ref.Transaction(context.Background(), transaction)
if err != nil {
    log.Fatal(err)
}

結果は以下のようになります。

さいごに

  • こちらで紹介したもの以外にも沢山の関数が用意されています
    https://godoc.org/firebase.google.com/go/db
  • 記事に書かれているサンプルコードは手元で動作確認しながら記事に書いているので、ミスで動作しない可能性があります
  • 間違っているコードや記載、もっと良い書き方などありましたら、コメントまたは編集リクエストを頂けると嬉しいです