2
1

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 5 years have passed since last update.

【Foon】Firestore でも GAE Memcacheの管理を自動でしてくれるライブラリを作った

Last updated at Posted at 2019-07-18

概要

本当はWebアプリを作ってたはずだったのに間違えてライブラリを作って時間溶かしちゃったんで後悔(公開)しました。

Foon
https://github.com/brbranch/foon

Foonの概要

Go言語で、DataStoreでGAE Memcacheの管理をしてくれるライブラリとかはあるんですが (goon とか有名ですね)、Firestoreのネイティブモードでうまいこと連携してくれるのが見当たらなかったので作成しました。そんなわけで、こちらで使い方を軽く紹介します。

多分ライブラリがない理由はFirestoreがリアルタイムデータベースだし、その機能を利用しようとする場合Memcacheは邪魔になるだろうからって気もするんですが、その機能を利用しない場合はコスト削減のためにも連携させたいなということで。
(普通にFirestoreのDatabaseモードで運用すればいいだけの話ですけどね…)

機能の概要

Create / Read / Update / Deleteした際、そのデータをMemcacheにも登録します。
また、Readする際にはMemcacheに存在するかどうか確認し、あればそれを返却します。キャッシュはQueryの結果も保持します。ただQuery結果は、内包される可能性のあるデータが更新されたタイミングで全て破棄されるので、ダーティリードやファントムリードのようなのは起きない(はず)。

内部ではFirestore Native API (Godoc) をラップしており、現在以下のAPIを実装してます。

対応機能v0.9(2019/07現在)

  • Put / PutMulti (追加 or 更新)
  • Insert / InsertMulti (追加 or 重複の場合エラー)
  • Get / GetMulti / GetAll / GetByQuery (それぞれキャッシュ使わないメソッドもあり)
  • RunInTransaction
  • Batch
  • Delete

※ GetByQueryは現在一部のみ対応

未対応

  • DeleteMulti
  • Firestore API の Updateに該当する機能
  • GetByQueryの一部検索方法
  • CollectionGroupでの検索
  • ぼくがまだよくわかってないFirestore API
  • リアルタイム通知系
  • IDを数値でも管理したいなって思ってたり思ってなかったり

コレクションの定義

foon タグを入れることで、FirestoreのCollectionやDocumentと関連付けします。
下記の例だと、Usersというコレクションの中に、Userドキュメントが格納されていきます。

type User struct {
	__kind    string    `foon:"collection,Users"`
	ID        string    `foon:"id"`
	Name      string    `firestore:"name"`
	CreatedAt time.Time `foon:"createdAt" firestore:"createdAt"`
	UpdatedAt time.Time `foon:"updatedAt" firestore:"updatedAt"`
}

foon:"id" は識別するために必須です。

サブコレクションの定義

Firestoreでは、Documentの中にもSubCollectionを定義できます。
それは、Foonでは以下のように定義します。

type Device struct {
	__kind     string    `foon:"collection,Devices"`
	ID         string    `foon:"id"`
	Parent     *foon.Key `foon:"parent"`
	DeviceName string    `firestore:"deviceName"`
}

foon:"id" は識別するために必須です。

操作

新規追加

こんな感じに、goonライクに行えます。 Insertを使うと、IDが重複している場合はエラーとなります。

新規追加
f := foon.Must(appEngineContext)
user := &User{ ID: "user0001", Name: "username01" }
if err := f.Insert(user); err != nil {
    // エラー処理
}

// ID指定なしの新規追加もできます。その場合、自動でランダムな文字列が払い出され、セットされます
user := &User{ Name: "username01" }
if err := f.Insert(user); err != nil {
    // エラー処理
}

// まとめて追加
users := []*User{
   { Name: "user001" }
   { ID: "user01", Name: "user002"}
}
if err := f.InsertMulti(&users); err != nil {
    // エラー処理
}

更新

更新の場合、IDが重複していたら上書きされます。

更新
f := foon.Must(appEngineContext)
user := &User{ ID: "user0001", Name: "username01" }
if err := f.Put(user); err != nil {
    // エラー処理
}

// ID指定なしの新規追加もできます。その場合、自動でランダムな文字列が払い出され、セットされます
user := &User{ Name: "username01" }
if err := f.Put(user); err != nil {
    // エラー処理
}

// まとめて追加
users := []*User{
   { Name: "user001" }
   { ID: "user01", Name: "user002"}
}
if err := f.PutMulti(&users); err != nil {
    // エラー処理
}

読み取り

読み取りの場合、Get / GetMultiではID指定が必須になります。

user := &User{ ID: "user001" }
f := foon.Must(appEngineContext)
if err := f.Get(user); err != nil {
    // エラー処理
}
// user内にデータは格納できてる

users := []*User{
    { ID: "user001" }, { ID: "user002" }
}
if err := f.GetMulti(&users); err != nil {
     // エラー処理
}

// Usersコレクションの全てのUserドキュメントを取得
users := []*User{}
if err := f.GetAll(foon.NewKey(&User{}), &users); err != nil {
    // エラー処理
}

// クエリで取得
users := []*User{}
conditions := foon.NewConditions().Where("name" , "==" , "user001").Limit(3)
if err := f.GetByQuery(foon.NewKey(&User{}), &users, conditions); err != nil {
    // エラー処理
}

削除

user := &User{ ID: "user001" }
f := foon.Must(appEngineContext)
if err := f.Delete(user); err != nil {
   // エラー処理
}

トランザクション

やり方はほとんど同じです。引数内に入ってくるfoonを利用して読み書きします。
※ トランザクション内ではキャッシュは読み込まれません。ただ、書き込みはされますので現状はダーティリードなどなど発生するかも(どうしようか悩み中...最後成功したら全部キャッシュに書き込むとかやるべきなんだろなぁ。。)

f := foon.Must(appEngineContext)
err := f.RunInTransaction(func(fn *Foon) error {
   // 内部処理
   // Firestoreの制約で、書き込みを行った後読み込みを行うとエラーになるよ
});

バッチ処理

書き込みだけやる際の楽でアトミックなやり方です(PutMultiなども、内部的にはこれ使ってます)

f := foon.Must(appEngineContext)
batch , _ := f.Batch()
batch.Create(&User{ ID: "user001" })
batch.Create(&User{ ID: "user002" })
batch.Delete(&User{ ID: "user003" })
if err := batch.Commit(); err != nil {
   // エラー処理
}

サブコレクションの扱い

FirestoreにはDocumentにもCollectionを持たせることができます。
これは、Datastore風にいうとAncestorの言い方が変わっただけで、実際はあんま変わらないです。
というわけで、Foonでは Parent として表現をしてます。

以下のようなデータで、 ID: Device002 を取得したい場合、

Users (collection)
  ├ User001 (Document)
  └ User002 (Document)
     └ Devices (Collection)
        ├ Device001 (Document)
        ├ Device002 (Document)
        ︰

以下のようにできます。要は foon:"parent" で定義した箇所に、親となるDocumentのKeyを入れることで表現できます。

user := &User{ ID: "User002" }
f := foon.Must(appEngineContext)
parentKey := foon.NewKey(user)
device := &Device{ ID: "Device002" , Parent: parentKey }

// 取得
if err := f.Get(device); err != nil {
   // エラー処理
}
device.DeviceName = "iPhoneX"

// 更新
if err := f.Put(device); err != nil {
    // エラー処理
}

// User002に新規にデバイスを登録
newDevice := &Device{ Parent: parentKey }
if err := f.Insert(newDevice); err != nil {
    // エラー処理
}

Devicesのコレクションにアクセスして、クエリを発行したい場合も要領は同じです。

user := &User{ ID: "User002" }
f := foon.Must(appEngineContext)
parentKey := foon.NewKey(user)
deviceCollectionKey := foon.NewKey(&Device{ Parent: parentKey})

devices := []*Device{}
if err := f.GetAll(deviceCollectionKey, &users); err != nil {
    // エラー処理
}

まとめ

Webアプリ作るの再開しよう・・・
ただ、ここ とか参考にしてもっとちゃんと公開する感じにもしたい熱が・・・

参考

Firestore(GoDoc)
https://godoc.org/cloud.google.com/go/firestore

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?