えいのうにっき

a-knowの日記です

mercari/datastore を使った GAE datastore への基本的な CRUD を試してみた

GAE(Standard Environment)で動作するWebアプリケーションをGoで書いている。サインアップしたユーザーの情報などを永続化しようとした場合、GAE で素直にやるとすると Datastore を使うことになる。昔 Java で GAE アプリケーションを書いていたときには Slim3 というフレームワークを使って Datastore に対する操作をおこなっていたが、現代・Goで、となると「これかな」と思っていたのが、mercari/datastore。

github.com

自分の言葉で「つらい」といえるほど、標準ライブラリでの 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 にアクセスしてみる。

f:id:a-know:20180225173336p:plain

f:id:a-know:20180225173405p:plain

良さそう。

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 セットしたけど使い道なかったわ......。。

f:id:a-know:20180225173445p:plain

表示されているタイムスタンプが、 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.Updatees.Createes.Put のような同じメソッドにできるはず。

f:id:a-know:20180225173538p:plain

f:id:a-know:20180225173550p:plain

タイムスタンプが変わっている。

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)
}

f:id:a-know:20180225173640p:plain

f:id:a-know:20180225173706p:plain

消えた。

おわり

ということで、mercari/datastore を使った、トランザクションもなにもない、単一エンティティの CRUD を試してみた。今後どんどん mercari/datastore はパワーアップしていくと思うので、これを使っていればきっとハッピーになれるはず。

上記のCRUDを試せるGAE アプリケーションコードを以下の場所に作っておいたので、よろしければどうぞ。

github.com

参考