GAE(Standard Environment)で動作するWebアプリケーションをGoで書いている。サインアップしたユーザーの情報などを永続化しようとした場合、GAE で素直にやるとすると Datastore を使うことになる。昔 Java で GAE アプリケーションを書いていたときには Slim3 というフレームワークを使って Datastore に対する操作をおこなっていたが、現代・Goで、となると「これかな」と思っていたのが、mercari/datastore。
自分の言葉で「つらい」といえるほど、標準ライブラリでの datastore 操作をやってきたわけではないし、その他のなにかと比べてみたわけでもない。......のだけど、GAEに強いあんな人やこんな人が集まるメルカリ謹製のラッパーなら大丈夫なんだろう、ということで、いったんはこれを使って開発を進めていくことに決めた。ただ、上記リポジトリのREADME内 How To Use
がまだ書かれていなかったので、まずは自分で mercari/datastore を使った基本的な CRUD を試してみた。今回のエントリはそのメモ。
「GAEを始めたい」「Datastoreの操作がよくわからない」「mercari/datastore を使ってみようかな」という人はどうぞ。将来上記の How To Use
に書かれるであろう内容に近いものにはなっているんじゃなかろうかと思いつつ、Best Practice
ではきっとない、という気持ちもあるので、そういうつもりで見てもらえたらと。
下準備編
go get
go get
しましょう。これは README にも書いてあります。
$ go get -u go.mercari.io/datastore
go.mercari.io
かっこいい。
Datastore に突っ込む構造体を定義
Datastore に登録したいレコード的なものを、struct で定義する。
type SampleRecord struct { KeyName string `datastore:"-"` Timestamp int64 }
datastore:"-"
というのは、各プロパティ(RDBでいうカラム的なもの)に付けられるタグというもの。今回のように datastore:"-"
とすると、そのプロパティは無視されて Datastore には登録されなくなる。なんでそんなことを、という疑問は、後ほど回収する。
また、RDB でいうプライマリキー的なプロパティは、定義する構造体に持つ必要はない。ということで、ここで定義しこれからDatastoreに登録しようとしている SampleRecord
は、Timestamp
というプロパティしか持たないとてもシンプルなエンティティ、ということになる。
Datastore Client を取り回すための構造体を定義
type SampleRecordStore struct { DatastoreClient datastore.Client }
こんなかんじ。加えて、こいつの実体を返してくれるメソッドも定義しておく。
func NewSampleRecordStore(ctx context.Context) (*SampleRecordStore, error) { ds, err := aedatastore.FromContext(ctx) if err != nil { log.Errorf(ctx, "failed Datastore New Client: %+v", err) return nil, err } return &SampleRecordStore{ds}, nil }
mercari/datastore は、Google Cloud Datastore にも対応している。aedatastore.FromContext()
というのは、appengine Datastore の client を返してくれるもの。go.mercari.io/datastore/aedatastore
を import しておく必要がある。
init
func init() { http.HandleFunc("/create", sampleCreateHandler) http.HandleFunc("/read", sampleReadHandler) http.HandleFunc("/update", sampleUpdateHandler) http.HandleFunc("/delete", sampleDeleteHandler) }
こんなかんじで。今回はブラウザでリクエストしてCRUDさせてみることにする。
本編・CRUD を試す
Create
まず、Datastore Client を取り回すための構造体・SampleRecordStore
に以下の2つのメソッドを生やしておく。
func (store *SampleRecordStore) NewKey(uuid string, ctx context.Context, ds datastore.Client) datastore.Key { return ds.NameKey("SampleRecord", uuid, nil) } func (store *SampleRecordStore) Create(ctx context.Context, e *SampleRecord) (*SampleRecord, error) { ds := store.DatastoreClient uuid := uuid.New().String() key := store.NewKey(uuid, ctx, ds) _, err := ds.Put(ctx, key, e) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("failed put record to Datastore. key=%v", key)) } e.KeyName = uuid return e, nil }
Datastore にエンティティを登録するためには Key が必要。key の作成方法として、
- 一意となるID(keyname)を渡して key を作るための
NameKey
- Datastore に登録した時点で初めて key が作成される(不完全な key を取得する)
IncompleteKey
の2種類の方法がある。今回は前者を用いる。
あと、Create の最後で e.KeyName = uuid
している。今回は Create の呼び出し元で、keyname となった uuid を知りたかったので、SampleRecord
strcut を返す前に、その KeyName
プロパティにセットしている。処理上の都合で struct にプロパティを持たせているが、永続化する必要はないので datastore:"-"
としていたわけだ。疑問回収。
そして、これらのメソッドを使って Datastore にエンティティを登録するような sampleCreateHandler
を、こんなかんじで書いてみた。
func sampleCreateHandler(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) es, err := NewSampleRecordStore(ctx) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to init store: %s", err.Error()) } record := &SampleRecord{ Timestamp: time.Now().Unix(), } record, err = es.Create(ctx, record) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to put record: %s", err.Error()) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Success to create record. key: %s", record.KeyName) }
これをデプロイして /create
にアクセスしてみる。
良さそう。
Read
今回も、SampleRecordStore
に以下のメソッドをあらかじめ生やしておく。
func (store *SampleRecordStore) Get(ctx context.Context, key datastore.Key) (*SampleRecord, error) { ds := store.DatastoreClient var record SampleRecord err := ds.Get(ctx, key, &record) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("failed get record from Datastore. key=%s", key.Name())) } record.KeyName = key.Name() return &record, nil }
ここでも record.KeyName = key.Name()
している。Datastore から取得したエンティティそのままでは、key の情報が付随していないので、無事取得できた場合には取得に用いた key の keyname を別途セットしてやっている。
これを使う sampleReadHandler
を書いてみた。
func sampleReadHandler(w http.ResponseWriter, r *http.Request) { param := r.URL.Query().Get("uuid") ctx := appengine.NewContext(r) es, err := NewSampleRecordStore(ctx) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to init store: %s", err.Error()) } record, err := es.Get(ctx, es.DatastoreClient.NameKey("SampleRecord", param, nil)) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to put record: %s", err.Error()) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Success to read record: %d", record.Timestamp) }
/read?uuid=xxxx...
というリクエストを期待している。.......あ、せっかく keyname セットしたけど使い道なかったわ......。。
表示されているタイムスタンプが、 create
でできたものと一致している。よし。
Update
やはり今回も、SampleRecordStore
にメソッドを追加する。
func (store *SampleRecordStore) Update(ctx context.Context, e *SampleRecord) (*SampleRecord, error) { ds := store.DatastoreClient key := store.NewKey(e.KeyName, ctx, ds) _, err := ds.Put(ctx, key, e) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("failed put record to Datastore.")) } return e, nil }
そして Handler。
func sampleUpdateHandler(w http.ResponseWriter, r *http.Request) { param := r.URL.Query().Get("uuid") ctx := appengine.NewContext(r) es, err := NewSampleRecordStore(ctx) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to init store: %s", err.Error()) } key := es.DatastoreClient.NameKey("SampleRecord", param, nil) record, err := es.Get(ctx, key) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to get record: %s", err.Error()) } record.Timestamp = time.Now().Unix() _, err = es.Update(ctx, record) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to put record: %s", err.Error()) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Success to update record") }
/update?uuid=xxxx...
というリクエストを期待している。リクエストされたときのタイムスタンプで存在するエンティティを更新する、という動作になる。更新だろうが新規の登録だろうが、Datastore 的には同じ Put
という操作になる(渡された key に対応するエンティティが存在すれば更新、なければ登録)ので、es.Update
と es.Create
は es.Put
のような同じメソッドにできるはず。
タイムスタンプが変わっている。
Delete
最後、Delete。SampleRecordStore
にメソッドを追加する。
func (store *SampleRecordStore) Delete(ctx context.Context, key datastore.Key) error { ds := store.DatastoreClient err := ds.Delete(ctx, key) if err != nil { return errors.Wrap(err, fmt.Sprintf("failed delete record from Datastore. key=%s", key.Name)) } return nil }
sampleDeleteHandler
。
func sampleDeleteHandler(w http.ResponseWriter, r *http.Request) { param := r.URL.Query().Get("uuid") ctx := appengine.NewContext(r) es, err := NewSampleRecordStore(ctx) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to init store: %s", err.Error()) } err = es.Delete(ctx, es.DatastoreClient.NameKey("SampleRecord", param, nil)) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to delete record: %s", err.Error()) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Success to delete record: %s", param) }
消えた。
おわり
ということで、mercari/datastore を使った、トランザクションもなにもない、単一エンティティの CRUD を試してみた。今後どんどん mercari/datastore はパワーアップしていくと思うので、これを使っていればきっとハッピーになれるはず。
上記のCRUDを試せるGAE アプリケーションコードを以下の場所に作っておいたので、よろしければどうぞ。