92
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GoAdvent Calendar 2014

Day 23

GAE/Goの自動キャッシュ付きDatastoreライブラリGoonを使う

Last updated at Posted at 2014-12-24

完全にアドベントカレンダーの存在を忘れて遅れてしまいました。
関係者各位様本当に申し訳ございません。。。。

ということで。

今年のAdvent CalenderはGAE/Goネタが多いですね、とても素敵です。
願わくば早くβの文字が外れてほしい限りです。

今回はGAE/GoのDatastore周りを楽にするgoonの紹介をしたいと思います。
ちょこちょこ昔書いたQiitaにも登場させているのですがあまーつ使っている人を見たことがないので...

GAE/Goの標準Datastoreパッケージとちょっとした不満

GAE/GoのDatastore周りは、appengine SDKのdatastoreパッケージが結構素敵で、
structを定義すればかなり簡単にDatastoreを扱うことができます。

datastore.go

//GAE/Goのdatastore周り
//https://cloud.google.com/appengine/docs/go/datastore/より
type Employee struct {
    Name     string
    Role     string
    HireDate time.Time
    Account  string
}

func handle(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)

    e1 := Employee{
        Name:     "Joe Citizen",
        Role:     "Manager",
        HireDate: time.Now(),
        Account:  user.Current(c).String(),
    }

    key, err := datastore.Put(c, datastore.NewIncompleteKey(c, "employee", nil), &e1)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var e2 Employee
    if err = datastore.Get(c, key, &e2); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "Stored and retrieved the Employee named %q", e2.Name)
}

ただ幾つか面倒臭いこともあって

  • memcacheへの自動的にキャッシュして欲しい
  • keyとか特定プロパティから自動生成して欲しい
  • Kind名とかType名と同じで良いからわざわざ指定して作りたくない

あたりの小さなストレスが有ります。

そこでGoonの登場です。

Goon

GoonはGAE/Go用の自動キャッシュ付きDatastoreライブラリで、NDBを参考にしており以下の様な特徴を持ちます。

  • DatastoreのKeyをキャッシュキーとしたin-memoryとmemcacheへの自動キャッシュ
  • Type名を利用したKind名の自動解決
  • Datastore KeyやParent KeyをTypeの特定タグが付けられているフィールドを利用して自動生成

殆どの場合において標準のDatastoreパッケージを利用するよりGoonを利用したほうがDatastore周りのコードは短くなります。

使い方

では使い方を書いていきます。
今回は昔会社向けに作った勤務時間の入力アプリで使っていた「稼働時間」を表すEntityを例に上げていきたいと思います。

goonのインストール

とりあえずgo getします。

$ go get github.com/mjibson/goon

Typeの作成

まずEntity用のTypeを作ります。

go.go

//稼働時間Entity
type Worktime struct {
	Ymd       string         `datastore:"-" goon:"id"`
	MemberKey *datastore.Key `datastore:"-" goon:"parent"`

	StartTime     string     `datastore:"startTime,noindex"`
	EndTime       string     `datastore:"endTime,noindex"`
	RestTime      string     `datastore:"lateRestTime,noindex"`
	Status        WorkStatus `datastore:"status"`
	Etc           string     `datastore:"etc,noindex"`
	Worktime      int        `datastore:"worktime,noindex"`
}

通常のdatastoreに加えて、YmdフィールドとMemberKeyフィールドにgoonタグがついています。
goon:"id"タグはそのフィールドがdatastore.KeyのName値として利用することを宣言しています。
goon:"parent"タグはそのフィールドがdatastore.KeyのParent Keyとして利用することを宣言しています。
共にDatastoreのEntityとしては保存しないため(取得する場合はKeyから取得する)datastore:"-"タグが付与されています。

今回の場合はMemberKey(メンバーを表す)をParent KeyとしてYmd(日付)をKey Nameとした*datastore.Keyを作成し、それをEntityのKeyとしています。

Goonインスタンスの作成

次にGetやPutをするためのGoonインスタンスを作成します。
Goonインスタンスは*http.Requestか、appengine.Contextから作成します。

make_goon.go
func makeGoon(r *http.Request) goon.Goon {
  g := goon.NewGoon(r)
  return g
}

なおaetest(GAE/GoのUnitTest)などと利用する場合はappengine.Contextから直接作成します。

by_context.go
func Test() error {
	if c, err = aetest.NewContext(&aetest.Options{}); err != nil {
		panic(err.Error())
	}

	g = goon.FromContext(c)
}

Put

では上記を利用して実際にDatastoreへPutしたいと思います。

基本的な手順は

  1. Goonインスタンスを作る
  2. 対象のEntityに値を詰める
  3. GoonインスタンスのPutメソッドでPutする

です。

put_worktime.go
func PutWorktime(r *http.Request, memberKey *datastore.Key) (*Worktime, error) {
	g := goon.NewGoon(r)
	
	//goonタグの値以外を作る ※サンプルコードのため省略
	w, err := createWorktime(r)
	
	if err != nil {
		return nil, err
	}
	
	//Parent Keyを詰める
	w.MemberKey = memberKey
	
	//Datastore KeyのNameを詰める
	w.Ymd = "2014-12-24"
	
	//g.PutでPut KeyやKind名はGoon側で解決
	//またPut時にMemcacheやIn-Memory Cacheしてくれる
	//なお返却値は *datastore.Key, error
	if _, err := g.Put(w); err != nil {
		return w, err
	}
	
	return w, nil
}

通常だとKeyの作成や、Kind名の直打ち、その後のMemcacheへ詰めたりとがありますが
その辺りが無くなって割とすっきりします。

Get

次にGetです。
Getも基本的に*datastore.Keyは意識する必要がなくgoon:"id"が付いているフィードに値を抓るだけでよいです。

基本的な手順は

  1. Goonインスタンスを作る
  2. 対象のEntityのgoon:"id"フィールドにKey名を詰める
  3. GoonインスタンスのGetメソッドでGetする
get_worktime.go
func GetWorktime(r *http.Request, ymd string, memberKey *datastore.Key) (*Worktime, error) {
	g := goon.NewGoon(r)
	
	//goon:"id"とgoon:"parent"(ある場合)に値を詰める
	w := &Worktime{
		Ymd : ymd,
		MemberKey : memberKey
	}
	
	//Getは自動的にmemory→memcache→datastoreの順で検索してくれます。
	//時々memcacheが恐ろしく遅い場合もある程度のところでtimeoutしてdatastoreを検索しに行ってくれます。
	if err := g.Get(w); err != nil {
		return nil, err
	}
	
	return w, nil
}

GetMultiを使うと複数のKeyに対して一気にGetを行うことができ、それ自体もcacheからの取得をサポートしています。
割とDatastoreの定番技ですが、
今回のような「日付」をKeyとして使っていて、日付の範囲検索を行いたい場合、(例えば2014年5月のデータを全て取得など)
下手なdatastore.Queryを使ったIndex検索より、対象期間全ての日付を作成してGetMultiで検索するとmemoryに乗っているものはmemoryから、Memcacheに乗っているものはMemcacheから、残りをDatastoreから取得のようなことができ、
検索コストを抑えることができたりします。

Cursor

コード自体はgoonのドキュメントに乗っているので、掲載しませんが、
goonではCursorを使っている場合でもキャッシュ上に存在するデータはキャッシュから取得してくれるため検索コストを抑えることができます。

まとめ

GAE定番のDatastoreの結果はとりあえずMemcacheへというのが楽にできるgoonは結構素敵です。
元々のdatastoreパッケージからそこまでコードも変わらないため、移行も結構楽にできます。

また開発自体も活発ではないですがほそぼそと続けているようです。
※というより薄いライブラリなのでそこまで発展していかない?
是非GAE/Goを使うときは使ってみてください。

92
90
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
92
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?