2011年6月26日日曜日

Creating an original REST API using Google App Engine and Go (4)

前回まででコメントを送信する部分はできたので、今回はそのコメントを格納する部分をやる。

Datastore

Google App Engineは自前でDatastoreというデータベースを持っていて、何らかのデータを格納しようと思ったらそこを使うことになる。詳しくは公式のドキュメントを読んでもらうとして、ざっくりと特徴をまとめると以下のようになる。

  • 超スケールする
  • 色んな場所に自動で複製をつくる
  • スキーマという概念が無い。なので、事前に定義したテーブルの型に縛られるようなことなく好きな形のデータを格納できる。
  • SQLでやりとりするとかはできない

DatastoreとのやりとりはQuotaと、ひいては課金と深く関わってくるので注意したほうがいいけど、今回はローカルでしか実行しないつもりなのでひとまずその辺は無視する。

サーバーサイド

以前、APIにとって重要な点はインタフェースが変わらないことである! と偉そうなことを言いながら、早々にAPIを変えることにした。といっても変わったのはURLだけだけど。どうもsendだと他のAPIと統一感が無い点が気になった。ということで、前回のsendからpostに変更して、更にコメントの格納処理を追加したコードが以下。

package hello

import (
    "fmt"
    "http"
    "template"
    "time"
    "appengine"
    "appengine/datastore"
)

const (
    GET string = "GET"
    POST string = "POST"
    DELETE string = "DELETE"
    REMARK_FIELD string = "remark"
    REMARK_KIND string = "Remark"
)

type Remark struct {
    Content string // the remark itself
    Time datastore.Time // the time the remark arrived
}

var (
    errorTemplate  = template.MustParseFile("error.html", nil)
)


func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post", errorHandler(postRemark))
}

func root(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "At root!")
}

// Handle a remark that was sent
func postRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    
    switch r.Method {
    case POST:
        r.ParseForm()
        remark, present := r.Form[REMARK_FIELD]
        if !present { // required field does not exist
            msg := "Required field does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusBadRequest)
        } else { // Got required field
            c.Infof("Remark: %s\n", remark)

            // Store remark to datastore
            
            // Since we only have a single thread now, we specify a constant string as the kind.
            // In the future, we may use a unique string that is tied to the thread.
            kind := REMARK_KIND
            
            currentTime := time.Seconds() // now
            store := Remark {
                Content : remark[0],
                Time : datastore.SecondsToTime(currentTime), 
            }

            // In the future, we may assign a key for each thread,
            // and give that key as parent for each remark in that thread.
            var parentKey *datastore.Key = nil
            var stringID string = "" // using the intID, so we set an empty string for the stringID
            intID := currentTime // using the current time as an int ID
            key := datastore.NewKey(kind, stringID, intID, parentKey)
            _, err := datastore.Put(c, key, &store)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
            }

            c.Infof("Stored remark as id: %d", intID)
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                http.Error(w, "Unknown error", http.StatusInternalServerError)
                errorTemplate.Execute(w, err)
            }
        }()
        fn(w, r)
    }
}

上のコードのなかでも、特にキモとなるのは以下の部分。

// Since we only have a single thread now, we specify a constant string as the kind.
            // In the future, we may use a unique string that is tied to the thread.
            kind := REMARK_KIND
            
            currentTime := time.Seconds() // now
            store := Remark {
                Content : remark[0],
                Time : datastore.SecondsToTime(currentTime), 
            }

            // In the future, we may assign a key for each thread,
            // and give that key as parent for each remark in that thread.
            var parentKey *datastore.Key = nil
            var stringID string = "" // using the intID, so we set an empty string for the stringID
            intID := currentTime // using the current time as an int ID
            key := datastore.NewKey(kind, stringID, intID, parentKey)
            _, err := datastore.Put(c, key, &store)

処理の内容は下の感じ。

  1. datastore格納用の構造体にデータをつめる
  2. datastore格納に必要な一意の鍵をつくる
  3. 鍵とデータを一緒に格納する

鍵の生成が少しとまどうところで、調べた感じ基本的には2種類のIDが必要だということがわかった。一つはkindという文字列、あとは数値のIDか文字列のIDのどちらか(もしくは両方)を用意しないといけない。

kindは格納する情報が持つタグのようなもので、後程データを取得するクエリを書くときにもkindを指定することになる。今回は想定しているスレッドが一つだけなので固定の文字列だけど、将来的に複数のスレッドごとにコメントを管理するとなったら(例えば)そのスレッドのタイトルをkindとして指定するとクエリが出しやすいはず。

もう一つの文字列ID、あるいは数値IDは文字通りのIDで、上のサンプルでは現在時刻をそのコメントの数値IDとして設定している。

ちなみに鍵には親鍵を指定することもできて、将来的にスレッドごとにコメントを格納して管理するとなった場合、そのスレッドの鍵を各コメントの親鍵として設定しておくとやっぱりクエリが出しやすいと思う。まだ試してはいないけど。

クライアントサイド

クライアント側は、前回のソースのうちPOSTする先のURLをhttp://local.../sendからhttp://local.../postに変更すればそのまま使える。

サーバーサイド実行結果

さてこの状態で実行をすると、下のような結果が返ってくる。ログを見る限りではちゃんと格納できてるっぽいけど、ちゃんと確認するには実際に取得できるかをみないといけない。ということで次回はその取得する部分を書いていく。

INFO     2011-06-26 02:43:01,856 __init__.py:324] building _go_app
INFO     2011-06-26 02:43:02,649 __init__.py:316] running _go_app
2011/06/26 02:43:02 INFO: Remark: [aardvark]
2011/06/26 02:43:02 INFO: Stored remark as id: 1309056182
INFO     2011-06-26 02:43:02,760 dev_appserver.py:4217] "POST /1/remark/post HTTP/1.1" 200 -
(続く)

0 件のコメント:

コメントを投稿