post Image
Go+GAE+Cloud Datastoreで簡単なREST APIを構築

今回の対象はCloud Datastoreです。
まだ手探り状態なので超初級編。
将来的にはechoフレームワークを利用する予定ですが、Goのhttp/net周りの学習をするためにしばらくはフレームワークは利用しません。

今回やりたいこと

  1. Cloud Datastoreに対して「項目名(string)、金額(int)」を持ったエンティティの登録・更新・取得・削除を行う
  2. http://localhost:8080/ でindexページが表示され、ajaxリクエストでCRUDが行える(templateを利用する)
  3. http://localhost:8080/api/{種別}/{項目名}/{金額} でアクセスするとREST APIが使える
  4. 今回の種別は「Expense(費用)」のみ
  5. 対応methodはGET(取得)、PUT(登録・更新)、DELETE(削除)の3つ
  6. PUT同時リクエスト時の「エンティティの項目の一意性担保」については考慮しない(次回以降にやる)
  7. できる限りGo自体の学習も進める

前提条件

過去の記事「WindowsのEclipseでGo+AppEngineの開発環境を構築」で構築した環境を利用しています。

事前準備

とりあえずAppEngineのライブラリを入れる。

コンソールで実行
>go get google.golang.org/appengine

前回と同じ手順で、Eclipseのプロジェクトとして「hello_datastore」を作成。

コーディング開始

param.go

まずはURLとして渡された文字列をパラメタに分解するパーサーを用意します。

3. http://localhost:8080/api/{種別}/{項目名}/{金額} でアクセスするとREST APIが使える

汎用的なものではなく、今回の目的である上記を満たすことに取ったしたパーサーを用意しました。
「特定のKeyを持つか」や「特定のValueを持つか」という用途はなかったので、KeyやValueそのものが存在するかのHas系メソッドだけ用意。

src/hello_datastore/param.go
package hello_datastore

import (
    "net/url"
    "strings"
)

// 今回のサンプルに特化した構造体。
// URLをパースし、必要な値を保持。
// ※handler系から呼ばれる事を想定
// [使い方]
// p := NewParam(r.URL)
type Param struct {
    Kind  string
    Key   string
    Value string
}

// URLのGET値をベースにParamを作成。
// 1番目はindex or api、2番目は種別、3番目はキー、4番目は値、5番目以降は破棄。
func NewParam(url *url.URL) *Param {
    path := strings.Trim(url.Path, "/")
    s := strings.Split(path, "/")
    param := new(Param)
    if len(s) >= 2 {
        param.Kind = s[1]
    }
    if len(s) >= 3 {
        param.Key = s[2]
    }
    if len(s) >= 4 {
        param.Value = s[3]
    }
    return param
}
func (p *Param) HasKey() bool {
    return len(p.Key) > 0
}
func (p *Param) HasValue() bool {
    return len(p.Value) > 0
}

expense_datastore.go

次に、今回のメイン処理となるDatastoreへのCRUDを行う処理を用意。
構造体やメソッドの学習、logの利用も兼ねています。
詳細はソースコメントを参照。

src/hello_datastore/expense_datastore.go
package hello_datastore

import (
    "errors"
    "golang.org/x/net/context"
    "google.golang.org/appengine"
    "google.golang.org/appengine/datastore"
    "log"
    "net/http"
)

const ENTITYNAME = "Expense"

// 今回のサンプルに特化した、Datastoreに格納するエンティティ用の構造体
// jsonレスポンスとして利用するためタグ名を指定
type ExpenseEntiry struct {
    Name  string `json:"string"`
    Price int    `json:"price"`
}

// ExpenseをDatastoreにCRUDするための構造体と手続き群。
// (名前が適当すぎてDatastoreの費用を管理してそうな名前に…)
// [使い方]
//  ed := NewExpenseDatasotre(r)
//  ed.build("項目名", 1080)
//  ed.Put()
type ExpenseDatasotre struct {
    Ctx     context.Context
    Keys    []*datastore.Key
    Expense *ExpenseEntiry
}

// コンストラクタ的な存在
func NewExpenseDatasotre(r *http.Request) *ExpenseDatasotre {
    ed := new(ExpenseDatasotre)
    ed.Ctx = appengine.NewContext(r)
    return ed
}

// datastoreに渡す用の構造体の作成と、そのNameに一致するkeysの取得を行う
func (ed *ExpenseDatasotre) build(name string, price int) error {
    ed.Expense = &ExpenseEntiry{
        Name:  name,
        Price: price,
    }
    // Entityの中身の確認用に標準出力にログ出力。↓な感じのログが出ます。(フォーマットは%#vがお気に入り)
    // 2016/11/06 15:38:32 &hello_datastore.ExpenseEntiry{Name:"test", Price:55}
    log.Printf("%#v", ed.Expense)
    var err error
    ed.Keys, err = getKeysByName(ed)
    return err
}

// 構造体のポインタにエンティティが読み込まれる
// 今回の簡易サンプルでは問題ないが、フィールドが一致しない項目は上書きされないため注意
func (ed *ExpenseDatasotre) Get() error {
    if len(ed.Keys) == 0 {
        return errors.New("取得対象の項目が存在しません")
    }
    return datastore.Get(ed.Ctx, ed.Keys[0], ed.Expense)
}

// このput処理ではputの複数同時リクエストがNameの一意性が失われてしまうが、今回のサンプルでは考慮しない
// 次回以降にgoのvarsLockの勉強と合わせて対応してみる予定
func (ed *ExpenseDatasotre) Put() error {
    // keyがある場合は上書き、ない場合は新規作成
    if len(ed.Keys) == 0 {
        keys := make([]*datastore.Key, 1)
        // 今回のサンプルでは、新規作成時のStringIDはランダムでいいためIncompleteKeyを利用
        keys[0] = datastore.NewIncompleteKey(ed.Ctx, ENTITYNAME, nil)
        ed.Keys = keys
    }

    _, err := datastore.Put(ed.Ctx, ed.Keys[0], ed.Expense)
    return err
}

// 削除のみmultiに対応。putでnameの重複を発生させることで実験可能。
// ※トランザクションの勉強用
func (ed *ExpenseDatasotre) Delete() error {
    if len(ed.Keys) == 0 {
        return errors.New("削除対象の項目が存在しません")
    }

    // 今回のサンプルではエンティティは全てROOTエンティティなため、複数削除にはクロスグループトランザクションを指定
    option := &datastore.TransactionOptions{XG: true}
    return datastore.RunInTransaction(ed.Ctx, func(c context.Context) error {
        return datastore.DeleteMulti(c, ed.Keys)
    }, option)
}

// Nameと完全一致するEntityのKeyリストを取得
func getKeysByName(ed *ExpenseDatasotre) ([]*datastore.Key, error) {
    q := datastore.NewQuery(ENTITYNAME).Filter("Name =", ed.Expense.Name)

    var expenses []ExpenseEntiry
    return q.GetAll(ed.Ctx, &expenses)

}

template.go

レスポンスでhtmlを返す場合のHandler。
今回は値のbind等はせずに、単純なhtmlを出力するためだけに利用。
学習用にsyncのonceを使ったりしてますが、これがあるとhtmlの修正が即時で確認できないのでhtmlコーディング時はコメントアウト推奨。

src/hello_datastore/template.go
package hello_datastore

import (
    "html/template"
    "net/http"
    "path/filepath"
    "sync"
)

// template用の構造体の定義
type templateHandler struct {
    once     sync.Once
    filename string
    tpl      *template.Template
}

func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    //templateをコンパイルするのは1回だけで良いのでonceを利用。
    t.once.Do(func() {
    t.tpl = template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
    })
    t.tpl.Execute(w, nil)
}

respond.go

REST APIのレスポンス用の処理。JSONで返すことのみを考慮した作りになっている。
特に難しいことはしておらず、josnレスポンスを返す勉強くらいにはなったかも。
(リクエストパラメタによってフォーマットを変えたりする場合は色々頑張りがいがありそう)

src/hello_datastore/respond.go
package hello_datastore

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// 今回はすべてjsonで返却
func respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) {
    w.WriteHeader(status)
    if data != nil {
        json.NewEncoder(w).Encode(data)
    }
}

func respondErr(w http.ResponseWriter, r *http.Request, status int, args ...interface{}) {
    respond(w, r, status, map[string]interface{}{
        "error": map[string]interface{}{
            "message": fmt.Sprint(args...),
        },
    })
}

main.go

今回のメインとなるファイル。とはいえ真のメインはexpense_datastore.goであって、このファイルは各処理を呼び出しているだけのもの。
init()の通りですが、/api/*でのリクエスト時にはrestHandler、それ以外の場合にはindex.htmlを出力するためのtemplateHandlerを呼んでいます。
こちらも詳しくはソースコメントを参照。

src/hello_datastore/main.go
package hello_datastore

import (
    "net/http"
    "strconv"
)

// 初期化
func init() {
    http.Handle("/", &templateHandler{filename: "index.html"})
    http.HandleFunc("/api/", restHandler)
}

// REST用のハンドラ
func restHandler(w http.ResponseWriter, r *http.Request) {

    p := NewParam(r.URL)
    if p.Kind != "expense" {
        respondErr(w, r, http.StatusBadRequest, "expense以外の種別には対応してません")
    }

    // PUT以外の場合はprice用にダミーの0を入れる
    if r.Method != "PUT" && !p.HasValue() {
        p.Value = "0"
    }

    // kindがexpenseの場合はvalueをintと定義したのでキャスト
    price, err := strconv.Atoi(p.Value)
    if err != nil {
        respondErr(w, r, http.StatusBadRequest, err.Error())
        return
    }

    ed := NewExpenseDatasotre(r)
    if err := ed.build(p.Key, price); err != nil {
        respondErr(w, r, http.StatusInternalServerError, err.Error())
    }

    switch r.Method {
    case "GET":
        handleGet(ed, w, r)
        return
    case "PUT":
        handlePut(ed, w, r)
        return
    case "DELETE":
        handleDelete(ed, w, r)
        return
    default:
        respondErr(w, r, http.StatusNotFound, "未対応のHTTPメソッドです")
    }
}

type SuccessResponse struct {
    Expense ExpenseEntiry `json:"entity"`
    Message string        `json:"message"`
}

// GET用のhandler。エンティティの取得を行う
func handleGet(ed *ExpenseDatasotre, w http.ResponseWriter, r *http.Request) {
    if err := ed.Get(); err != nil {
        respondErr(w, r, http.StatusBadRequest, err.Error())
        return
    }
    message := "「" + ed.Expense.Name + "」の金額は" + strconv.Itoa(ed.Expense.Price) + "円です。"
    respond(w, r, http.StatusOK, SuccessResponse{*ed.Expense, message})
}

// PUT用のhandler。エンティティの作成・更新を行う
func handlePut(ed *ExpenseDatasotre, w http.ResponseWriter, r *http.Request) {
    if err := ed.Put(); err != nil {
        respondErr(w, r, http.StatusInternalServerError, err.Error())
        return
    }
    message := "「" + ed.Expense.Name + "」の登録を行いました。"
    respond(w, r, http.StatusOK, SuccessResponse{*ed.Expense, message})
}

// DELETE用のhandler。エンティティの削除を行う
func handleDelete(ed *ExpenseDatasotre, w http.ResponseWriter, r *http.Request) {
    if err := ed.Delete(); err != nil {
        respondErr(w, r, http.StatusInternalServerError, err.Error())
        return
    }
    message := "「" + ed.Expense.Name + "」の削除を行いました。"
    respond(w, r, http.StatusOK, SuccessResponse{*ed.Expense, message})
}

index.html

上記の処理が通れば、あとは http://localhost:8080/ でアクセスした際に表示するHTMLを記述。
かなり簡単な処理ですが、formで記述されたGET,PUT,DELETEの各処理のsubmit時にフックして非同期化&GET値の/化を行い、ajaxでリクエストしているだけです。

src/templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>sample</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript">
jQuery(function($) {
    $("form").submit(function(event){
        event.preventDefault();
                var $form = $(this);
        var $url = $form.attr('action');
        $($form.serializeArray()).each(function(){
            if(this.value){
                $url += '/' + this.value;
            }
        });
        $.ajax({
            url: $url,
            type: $form.attr('method'),
        }).done(function(data) {
            alert($.parseJSON(data).message);
        }).fail(function(data) {
            alert($.parseJSON(data.responseText).error.message);
        });
    });
});
</script>
</head>
<body>
<section>
<h2>GET</h2>
<form action="/api/expense" method="get">
<label>項目名 : <input type="text" value="" name="key"></label>
<input type="submit" value="金額を取得">
</form>
</section>
<section>
<h2>PUT</h2>
<form id="put-expense" action="/api/expense" method="put">
<label>項目名 : <input type="text" value="" name="key"></label>
<label>金額 : <input type="text" value="" name="value"></label>
<input type="submit" value="項目を登録・更新">
</form>
</section>
<section>
<h2>DELETE</h2>
<form action="/api/expense" method="delete">
<label>項目名 : <input type="text" value="" name="key"></label>
<input type="submit" value="項目を削除">
</form>
</section>
</body>
</html>

実行

ローカル環境でのGAEエミュレータを起動

上記の準備がそろったらAppEngine SDKのgaeサーバー起動する。

コンソールかEclipseから実行
goapp serve {eclipse_project}\src

{eclipse_project}は筆者の場合はC:\works\eclipse\workspace\gae-sample

実行時ログ
>goapp serve src
INFO     2016-11-06 22:54:52,542 devappserver2.py:769] Skipping SDK update check.
INFO     2016-11-06 22:54:52,760 api_server.py:205] Starting API server at: http://localhost:57742
INFO     2016-11-06 22:54:52,769 dispatcher.py:197] Starting module "default" running at: http://localhost:8080
INFO     2016-11-06 22:54:52,773 admin_server.py:116] Starting admin server at: http://localhost:8000

表示確認

http://localhost:8080 へアクセス。
以下のようなページが表示されればOK。

datastore_01.PNG

項目の追加(PUT)

PUTの項目に、項目名「サンプル」金額「500」を入力しEnter。

「サンプル」の登録を行いました。

という表示がされればOK。
http://localhost:8000 の「Datastore Viewer」を選択すると追加したレコードが確認できる。
datastore_02.PNG

項目の取得(GET)

GETの項目に、項目名「サンプル」を入力しEnterを押下すると、先ほど登録したエンティティが取得可能。

「サンプル」の金額は500円です。

と表示されればOK。

項目の削除(DELETE)

DELETEの項目に、項目名「サンプル」を入力しEnterを押下すると、URLで渡したNameと一致するエンティティの一括削除が可能。

「サンプル」の削除を行いました。

と表示されればOK。

感想

前回までは環境構築がメインだったので、やっと「GO言語に触れてる」感が出てきました。
構造体、メソッド、インターフェイス、Goのポインタ、この辺の理解を徐々に深めている状況。
とりあえず悩みながら学ぶしかないので、まだしばらくは簡易的なサンプルを作りながら学習する予定です。

次回はTaskQueueと組み合わせてみる予定。
その前にゴルーチン/チャネルだったり、そもそもの記述別パフォーマンス調査を行うべきかもしれない。

参考文献


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。