久しぶりにGAE/Goで自分用サービス作ったとき、第1世代(Go 1.9まで)と全く違っていて混乱したので自分用メモ。
DatastoreがCloud Firestoreになった
以前はAppEngine専用のDatastore APIを使っていたが、Cloud Datastoreを経て現在はCloud Firestoreを使うようになっているようだった。
- Datastore (AppEngine)
- AppEngine環境専用
- appengine.Contextに依存している
- Cloud Datastore
- AppEngine以外からも使える
- DatastoreモードのCloud Firestore
- 下回りはCloud FirestoreになったがCloud Datastoreエミュレーション層が仲介する
- Cloud Datastoreと同じパッケージ(cloud.google.com/go/datastore)を使う
- ネイティブモードのCloud Firestore
2020年1月現在、新しくGCPプロジェクトを作成するとDatastoreモードのCloud FirestoreまたはネイティブモードのCloud Firestoreどちらかを選ぶ必要がある。既存のプロジェクトでCloud Datastoreを利用している場合は将来的に自動でDatastoreモードのCloud Firestoreにマイグレーションされるらしい。
一度選択すると、同じプロジェクトでは変更ができない*1ので、よほどの理由がない限りはネイティブモードを選べば良いと思う。ネイティブモードを選択しても、FirestoreのためにFirebaseコンソールとGCPコンソールを使い分ける必要はなく、GCPコンソールからFirestoreにアクセスできるし、今回は使わなかったがgocloud.dev/docstore(pkg.go.dev)はcloud.google.com/go/firestoreを使わずFirestore v1 APIを叩いているのでネイティブモードで慣れておいて損はない。
- DatastoreとFirestoreとApp Engineの関連 - Qiita
- Cloud Firestoreの勘所 パート1 — 概要 - google-cloud-jp - Medium
- Cloud Firestoreの勘所 パート2 — データ設計 - google-cloud-jp - Medium
firestore.Client.Docでnil DocumentRefエラーになる
以下のコードでnilが返ってきてしまう。
doc := firestore.Client.Doc("a/b/c")
docがnilなので、このメソッドを呼び出すと以下のエラーが発生する。
firestore: nil DocumentRef
原因はパスの要素数で、ドキュメントとして参照する場合のパスは偶数個の要素でなければ扱えない。なのでDoc("a/b/c/d")
なら偶数個なので正しいDocumentRefを取得できる。
コレクションとドキュメントが交互になるよう注意してください。コレクションとドキュメントは常にこのパターンに従う必要があります。コレクション内のコレクションや、ドキュメント内のドキュメントは参照できません。
同じ名前でCollectionとDocumentが存在できるのか
c, _ := firestore.NewClient(ctx, projectID) articleRef := c.Doc("Articles/<id>") commentRef := c.Doc("Articles/<id>/Comments/<n>")
のように、ドキュメントと名前が重複するコレクションは作れるのか?という話。Cloud Firestore データモデルに同じような構造のサンプルコードが書かれているので奇妙な設計というわけではなさそうだった。
カーソルはどうするの
ドキュメントのIDを使って、firestore.Query.StartAtまたはfirestore.Query.StartAfterを使うと途中から読める。
q := c.Collection("a/b/c").Where("is_draft", "==", false) q = q.OrderBy(firestore.DocumentID, firestore.Asc) q = q.StartAfter("<id>") // 最後に読んだID iter := q.Documents(ctx) defer iter.Stop()
保存したDocumentのIDを調べたい
ドキュメントへのパスを指定してDocumentRefを生成する場合は、パスに使った値を使えば良いが、保存されているドキュメントをイテレータで読み出す場合に困った。結局はfirestore.DocumentRef型にIDがあるのでそれを使うと良い。firestore.DocumentSnapshot型はRefフィールドにDocumentRefを持っている。
iter := c.Collection("Articles").Documents(ctx) defer iter.Stop() for { doc, err := iter.Next() if err == iterator.Done { return m, nil } fmt.Println(doc.Ref.ID) }
まとめて書き込みしたい
firestore.Client.Batchを使うと良いが、これは1回のコミットで最大500件までの制限がある。制限を超えると、以下のエラーが発生する。
maximum 500 writes allowed per request
この場合は単純に、500件ごとにfirestore.WriteBatchを作り直せば良い。
for _, a := range requests { // 500件ごとに分割してある b := c.Batch() for _, p := range a { b.Create(c.Doc(p.Key), p) } if err := b.Commit(); err != nil { return err } }
ただし、firestore.WriteBatchを作り直さずにそのまま使いまわすと、同じエラーが発生する。
Firestoreのテストはどうするの
gcloud beta emulators firestoreにエミュレータが用意されている。
cronとタスクキュー
cronはCloud Schedulerに移行する
これはまだAppEngineのcronが使えるので以前のままcron.yamlを使った。今ならCloud SchedulerとCloud Functionsで作れば良さそうに思う。
AppEngineタスクキューはCloud Tasksに移行
AppEngineのタスクキューとだいたい同じ感覚で使えるが、突然Protocol Buffersの型が出てきてつらみがある。これも今ならCloud Functionsの方が良いかもしれない。
import ( cloudtasks "cloud.google.com/go/cloudtasks/apiv2" taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2" ) ... c, err := cloudtasks.NewClient(ctx) if err != nil { return err } defer c.Close() queuePath := path.Join( "projects", projectID, "locations", locationID, "queues", "default", ) req := &taskspb.CreateTaskRequest{ Parent: queuePath, Task: &taskspb.Task{ MessageType: &taskspb.Task_AppEngineHttpRequest{ AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ HttpMethod: taskspb.HttpMethod_POST, RelativeUri: "/_ah/tasksadd", }, }, }, } for _, task := range tasks { body, err := json.Marshal(task) if err != nil { return err } req.Task.GetAppEngineHttpRequest().Body = body if _, err := c.CreateTask(ctx, req); err != nil { return err } }
デプロイ
無視するファイルはgcloudignoreに書く
node_modulesなどデプロイする必要のないファイルは.gcloudignoreに書くと無視できる。
node_modules/
staticでルーティングしたファイルが404 not found
Go 1.11以降(2nd gen以降?)は、go.modファイルのある場所がカレントディレクトリになる。そのため、
app/ app.yaml static/ bundle.js index.html go.mod go.sum
このようなファイル階層のとき、app/app.yamlからの相対パスを書いても読めない。
# ダメな例 handlers: - url: /api/.* script: auto - url: /bundle.js static_files: static/bundle.js upload: static/bundle.js - url: /(.*) static_files: static/index.html upload: static/index.html
これだと、go.modの位置にはstatic/ディレクトリは存在しないので参照できない。
上の記事にもあるが、app.yamlを以下のように変更するか、またはgo.modとapp.yamlを同じディレクトリに置くと良い。
handlers: - url: /api/.* script: auto - url: /bundle.js static_files: app/static/bundle.js upload: app/static/bundle.js - url: /(.*) static_files: app/static/index.html upload: app/static/index.html
Go 1.13を使いたい
app.yamlに設定すれば普通に使える。
runtime: go113
無料枠(Always-Free)はスケーリングとインスタンスクラスによって異なる
GAEの場合、Google Cloudの無料枠では
としか書かれていないが、実際は割り当てに書かれているように、インスタンスクラスによって
という制限がある。スケーリングについては以下のリンクが詳しい。
Cloud IAP
app.yamlのlogin: required
が使えなくなったので、お手軽に認証したければCloud Identity-Aware Proxyを使うと良い。Googleアカウント以外にも対応する必要があるなら、Identity Platformというサービスが使えるらしい。
Cloud IAPでOwnerを持っているユーザなのにアクセスできない
オーナーは設定変更する権限だけで、アクセス権は持っていない。アクセスするためにIAP-Secured Web App Userの追加が必要だった。セキュリティ→Identity-Aware Proxyと進んで、情報パネルにアカウントを追加する。
*1:別のプロジェクトには影響しない