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. <Font color="red">できる限りGo自体の学習も進める</Font>

## 前提条件

過去の記事「[WindowsのEclipseでGo+AppEngineの開発環境を構築](http://qiita.com/silverfox/items/3a50cafc44c25e8c52e3 "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系メソッドだけ用意。

```go: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の利用も兼ねています。
詳細はソースコメントを参照。

```go: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コーディング時はコメントアウト推奨。

```go: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レスポンスを返す勉強くらいにはなったかも。
（リクエストパラメタによってフォーマットを変えたりする場合は色々頑張りがいがありそう）

```go: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`を呼んでいます。
こちらも詳しくはソースコメントを参照。

```go: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でリクエストしているだけです。

```html: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](https://qiita-image-store.s3.amazonaws.com/0/144652/d6c3acbd-a42b-b048-65cb-7194c8009082.png)

### 項目の追加(PUT)

PUTの項目に、項目名「サンプル」金額「500」を入力しEnter。
> 「サンプル」の登録を行いました。

という表示がされればOK。
http://localhost:8000 の「Datastore Viewer」を選択すると追加したレコードが確認できる。
![datastore_02.PNG](https://qiita-image-store.s3.amazonaws.com/0/144652/5881dbd4-67c3-6c38-eff6-2efd300b5e9c.png)

### 項目の取得(GET)

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

と表示されればOK。

### 項目の削除(DELETE)

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

と表示されればOK。

## 感想

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

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

## 参考文献

* 以下の書籍の第六章はかなり参考にしました。
[Go言語によるWebアプリケーション開発]("https://www.amazon.co.jp/gp/product/4873117526/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4873117526&linkCode=as2&tag=booklog02e9-22" "Go言語によるWebアプリケーション開発 - Amazon")

* Go言語の基礎的な部分としては以下を参考にしています。
[スターティングGo言語]("https://www.amazon.co.jp/gp/product/B01FH3KRTI/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=B01FH3KRTI&linkCode=as2&tag=booklog02e9-22" "スターティングGo言語 - Amazon")